@kungfu-tech/buildchain 3.0.6-alpha.0 → 3.0.6-alpha.2
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/README.md +4 -4
- package/actions/promote-buildchain-ref/README.md +8 -0
- package/bin/buildchain.mjs +13 -1
- package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
- package/dist/site/buildchain-contract.json +47 -27
- package/dist/site/buildchain-site.json +165 -44
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +40 -4
- package/dist/site/controller-registry.json +20 -4
- package/dist/site/kfd-claims.json +140 -19
- package/dist/site/kfd-upstream-aggregate.json +9 -9
- package/dist/site/manual-registry.json +9 -9
- package/dist/site/node-api-registry.json +1161 -180
- package/dist/site/page-registry.json +152 -31
- package/dist/site/public-surface-audit.json +386 -19
- package/dist/site/publication-authority-registry.json +61 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +151 -13
- package/docs/MAP.md +2 -0
- package/docs/auditable-demo.md +58 -11
- package/docs/aws-us-elastic-runner-burst-plane.md +114 -80
- package/docs/cli-reference.md +154 -0
- package/docs/dev-alpha-candidate-patrol.md +13 -5
- package/docs/dev-delivery-warrant.md +158 -0
- package/docs/node-api-reference.md +54 -15
- package/docs/publication-authority.md +11 -0
- package/docs/release-candidate.md +19 -2
- package/docs/release-governance.md +60 -1
- package/docs/reusable-build-surface.md +11 -1
- package/docs/shifu-gate-profiles.md +12 -1
- package/docs/versioning.md +2 -0
- package/package.json +4 -2
- package/packages/core/buildchain-publication-authority.js +3 -1
- package/packages/core/channel-candidate.js +2 -21
- package/packages/core/channel-promotion-baseline.js +199 -0
- package/packages/core/dev-delivery-candidate-identity.js +94 -0
- package/packages/core/dev-delivery-common.js +73 -0
- package/packages/core/dev-delivery-proof.js +252 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
- package/packages/core/dev-delivery-warrant-settlement.js +73 -0
- package/packages/core/dev-delivery-warrant.js +591 -0
- package/scripts/auditable-demo-bundle-verification.mjs +148 -0
- package/scripts/auditable-demo-platform.mjs +86 -50
- package/scripts/auditable-demo-presentation.mjs +83 -0
- package/scripts/auditable-demo-renditions.mjs +264 -0
- package/scripts/auditable-demo.mjs +24 -30
- package/scripts/aws-windows-jit-campaign-core.mjs +7 -8
- package/scripts/aws-windows-jit-controller.mjs +1 -0
- package/scripts/aws-windows-jit-core.mjs +1 -1
- package/scripts/build-contract-core.mjs +58 -3
- package/scripts/buildchain-cli-help.mjs +8 -0
- package/scripts/buildchain-patrol.mjs +9 -0
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
- package/scripts/dev-delivery-proof.mjs +193 -0
- package/scripts/dev-delivery-warrant.mjs +426 -0
- package/scripts/dev-pr-auto-merge.mjs +497 -55
- package/scripts/dev-pr-delivery-warrant.mjs +209 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +2 -4
- package/scripts/gate-profile-core.mjs +24 -0
- package/scripts/generate-site-bundle.mjs +2 -2
- package/scripts/git-fetch-process-tree.mjs +142 -0
- package/scripts/lifecycle-substage-evidence.mjs +274 -0
- package/scripts/locked-source-checkout.mjs +6 -3
- package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
- package/scripts/resolve-build-contract.mjs +7 -0
- package/scripts/route-offline-runners.mjs +1 -0
- package/scripts/run-lifecycle-core.mjs +9 -9
- package/scripts/shifu-gate-profile.mjs +10 -16
- package/scripts/site-capability-metadata.mjs +14 -0
|
@@ -0,0 +1,148 @@
|
|
|
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 { readRendererManifest } from "./auditable-demo-renditions.mjs";
|
|
8
|
+
|
|
9
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
10
|
+
const MAX_METADATA_MEMBER_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const MAX_LONG_FORM_MANIFEST_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
|
|
14
|
+
const DIGEST_BUFFER_BYTES = 64 * 1024;
|
|
15
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
16
|
+
|
|
17
|
+
function fail(message) {
|
|
18
|
+
throw new Error(`auditable demo platform: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireValue(condition, message) {
|
|
22
|
+
if (!condition) fail(message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rootBytes(value) {
|
|
26
|
+
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function boundedRegular(file, label, maximum) {
|
|
30
|
+
const metadata = fs.lstatSync(file);
|
|
31
|
+
requireValue(metadata.isFile() && !metadata.isSymbolicLink() && metadata.size <= maximum, `${label} must be a bounded regular file`);
|
|
32
|
+
return metadata;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readRegular(file, label, maximum = MAX_METADATA_MEMBER_BYTES) {
|
|
36
|
+
boundedRegular(file, label, maximum);
|
|
37
|
+
return fs.readFileSync(file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function digestRegular(file, label, maximum) {
|
|
41
|
+
const expected = boundedRegular(file, label, maximum);
|
|
42
|
+
const descriptor = fs.openSync(file, "r");
|
|
43
|
+
const hash = crypto.createHash("sha256");
|
|
44
|
+
const buffer = Buffer.allocUnsafe(DIGEST_BUFFER_BYTES);
|
|
45
|
+
let bytes = 0;
|
|
46
|
+
try {
|
|
47
|
+
while (true) {
|
|
48
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
49
|
+
if (count === 0) break;
|
|
50
|
+
bytes += count;
|
|
51
|
+
requireValue(bytes <= maximum, `${label} must be a bounded regular file`);
|
|
52
|
+
hash.update(buffer.subarray(0, count));
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
fs.closeSync(descriptor);
|
|
56
|
+
}
|
|
57
|
+
requireValue(bytes === expected.size, `${label} changed while it was verified`);
|
|
58
|
+
return { bytes, root: `sha256:${hash.digest("hex")}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decodeUtf8(bytes, label) {
|
|
62
|
+
try {
|
|
63
|
+
return UTF8.decode(bytes);
|
|
64
|
+
} catch {
|
|
65
|
+
fail(`${label} must be valid UTF-8`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function inside(root, relative, label) {
|
|
70
|
+
requireValue(typeof relative === "string" && relative && !path.isAbsolute(relative), `${label} must be relative`);
|
|
71
|
+
const resolvedRoot = path.resolve(root);
|
|
72
|
+
const resolved = path.resolve(resolvedRoot, relative);
|
|
73
|
+
requireValue(resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`), `${label} escapes its root`);
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function listBundleFiles(root, prefix = "") {
|
|
78
|
+
const entries = fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
|
|
79
|
+
.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
|
80
|
+
const files = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
requireValue(!entry.isSymbolicLink(), `bundle member must not be a symbolic link: ${path.join(prefix, entry.name)}`);
|
|
83
|
+
const relative = path.join(prefix, entry.name);
|
|
84
|
+
if (entry.isDirectory()) files.push(...listBundleFiles(root, relative));
|
|
85
|
+
else {
|
|
86
|
+
requireValue(entry.isFile(), `bundle member must be a regular file: ${relative}`);
|
|
87
|
+
files.push(relative.split(path.sep).join("/"));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const RENDERER_MANIFEST_HELPERS = {
|
|
94
|
+
decodeUtf8,
|
|
95
|
+
digestPattern: DIGEST,
|
|
96
|
+
invariant: requireValue,
|
|
97
|
+
maxBytes: MAX_TERMINAL_CAPTURE_BYTES,
|
|
98
|
+
maxEvents: MAX_TERMINAL_CAPTURE_EVENTS,
|
|
99
|
+
readRegular,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function verifyBundleChecksums(root, label, options = {}) {
|
|
103
|
+
const resolved = path.resolve(root);
|
|
104
|
+
const checksums = readRegular(path.join(resolved, "checksums.sha256"), `${label} checksums`);
|
|
105
|
+
const declared = new Set();
|
|
106
|
+
const members = [];
|
|
107
|
+
let bundleBytes = 0;
|
|
108
|
+
for (const row of checksums.toString("utf8").split("\n").filter(Boolean)) {
|
|
109
|
+
const match = /^([0-9a-f]{64}) ([^\0\r\n]+)$/u.exec(row);
|
|
110
|
+
requireValue(match, `${label} checksum row is invalid`);
|
|
111
|
+
const target = inside(resolved, match[2], `${label} checksum member`);
|
|
112
|
+
requireValue(!declared.has(match[2]), `${label} checksum member is repeated`);
|
|
113
|
+
declared.add(match[2]);
|
|
114
|
+
const metadataMember = match[2].endsWith(".json") || match[2].endsWith(".sha256");
|
|
115
|
+
const maximum = options.allowLongFormRendererManifest && match[2] === "manifest.json"
|
|
116
|
+
? MAX_LONG_FORM_MANIFEST_BYTES
|
|
117
|
+
: metadataMember
|
|
118
|
+
? MAX_METADATA_MEMBER_BYTES
|
|
119
|
+
: (options.maximumMemberBytes || MAX_METADATA_MEMBER_BYTES);
|
|
120
|
+
bundleBytes += boundedRegular(target, `${label} member`, maximum).size;
|
|
121
|
+
requireValue(bundleBytes <= options.maximumBundleBytes, `${label} exceeds its aggregate byte budget`);
|
|
122
|
+
members.push({ expectedRoot: `sha256:${match[1]}`, maximum, name: match[2], target });
|
|
123
|
+
}
|
|
124
|
+
for (const member of members) {
|
|
125
|
+
const verified = options.allowLongFormRendererManifest && member.name === "manifest.json"
|
|
126
|
+
? (() => {
|
|
127
|
+
const value = readRendererManifest(member.target, RENDERER_MANIFEST_HELPERS).bytes;
|
|
128
|
+
return { bytes: value.length, root: rootBytes(value) };
|
|
129
|
+
})()
|
|
130
|
+
: digestRegular(member.target, `${label} member`, member.maximum);
|
|
131
|
+
requireValue(verified.root === member.expectedRoot, `${label} checksum mismatch: ${member.name}`);
|
|
132
|
+
if (member.name === "manifest.json" && options.rendererManifestRoot) {
|
|
133
|
+
requireValue(verified.root === options.rendererManifestRoot, `${label} renderer manifest root mismatch`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const actual = listBundleFiles(resolved).filter((name) => name !== "checksums.sha256");
|
|
137
|
+
requireValue(JSON.stringify([...declared].sort()) === JSON.stringify(actual), `${label} checksum member set is not exact`);
|
|
138
|
+
return rootBytes(checksums);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function copyVerifiedRegular(source, destination, label, maximum) {
|
|
142
|
+
const sourceDigest = digestRegular(source, label, maximum);
|
|
143
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
144
|
+
fs.copyFileSync(source, destination);
|
|
145
|
+
const destinationDigest = digestRegular(destination, `${label} copy`, maximum);
|
|
146
|
+
requireValue(destinationDigest.bytes === sourceDigest.bytes && destinationDigest.root === sourceDigest.root, `${label} copy differs from its verified source`);
|
|
147
|
+
return { path: path.basename(destination), bytes: destinationDigest.bytes, root: destinationDigest.root };
|
|
148
|
+
}
|
|
@@ -5,6 +5,8 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { materializeDemoPresentation, validateDemoPresentation } from "./auditable-demo-presentation.mjs";
|
|
9
|
+
import { copyVerifiedRegular, verifyBundleChecksums } from "./auditable-demo-bundle-verification.mjs";
|
|
8
10
|
|
|
9
11
|
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
10
12
|
const SAFE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
@@ -25,7 +27,13 @@ const RENDITIONS = [
|
|
|
25
27
|
];
|
|
26
28
|
const STANDARD_MAX_SECONDS = 60;
|
|
27
29
|
const LONG_FORM_MAX_SECONDS = 180;
|
|
30
|
+
const PRESENTATION_FRAMED = "presentation-framed";
|
|
31
|
+
const TERMINAL_FILL = "terminal-fill";
|
|
28
32
|
const MAX_EXECUTABLE_FILES = 32;
|
|
33
|
+
const MAX_METADATA_MEMBER_BYTES = 8 * 1024 * 1024;
|
|
34
|
+
const MAX_MEDIA_MEMBER_BYTES = 64 * 1024 * 1024;
|
|
35
|
+
const MAX_GATE_BUNDLE_BYTES = 64 * 1024 * 1024;
|
|
36
|
+
const MAX_MEDIA_BUNDLE_BYTES = 128 * 1024 * 1024;
|
|
29
37
|
|
|
30
38
|
function durationPolicy(value = "standard") {
|
|
31
39
|
requireValue(value === "standard" || value === "long-form", "scenario duration class is invalid");
|
|
@@ -80,40 +88,6 @@ function readJson(file, label) {
|
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
function listBundleFiles(root, prefix = "") {
|
|
84
|
-
const entries = fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
|
|
85
|
-
.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
|
86
|
-
const files = [];
|
|
87
|
-
for (const entry of entries) {
|
|
88
|
-
requireValue(!entry.isSymbolicLink(), `bundle member must not be a symbolic link: ${path.join(prefix, entry.name)}`);
|
|
89
|
-
const relative = path.join(prefix, entry.name);
|
|
90
|
-
if (entry.isDirectory()) files.push(...listBundleFiles(root, relative));
|
|
91
|
-
else {
|
|
92
|
-
requireValue(entry.isFile(), `bundle member must be a regular file: ${relative}`);
|
|
93
|
-
files.push(relative.split(path.sep).join("/"));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return files;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function verifyChecksums(root, label) {
|
|
100
|
-
const resolved = path.resolve(root);
|
|
101
|
-
const bytes = regular(path.join(resolved, "checksums.sha256"), `${label} checksums`);
|
|
102
|
-
const rows = bytes.toString("utf8").split("\n").filter(Boolean);
|
|
103
|
-
const declared = new Set();
|
|
104
|
-
for (const row of rows) {
|
|
105
|
-
const match = /^([0-9a-f]{64}) ([^\0\r\n]+)$/u.exec(row);
|
|
106
|
-
requireValue(match, `${label} checksum row is invalid`);
|
|
107
|
-
const target = inside(resolved, match[2], `${label} checksum member`);
|
|
108
|
-
requireValue(!declared.has(match[2]), `${label} checksum member is repeated`);
|
|
109
|
-
declared.add(match[2]);
|
|
110
|
-
requireValue(rootBytes(regular(target, `${label} member`)) === `sha256:${match[1]}`, `${label} checksum mismatch: ${match[2]}`);
|
|
111
|
-
}
|
|
112
|
-
const actual = listBundleFiles(resolved).filter((name) => name !== "checksums.sha256");
|
|
113
|
-
requireValue(JSON.stringify([...declared].sort()) === JSON.stringify(actual), `${label} checksum member set is not exact`);
|
|
114
|
-
return rootBytes(bytes);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
91
|
function inside(root, relative, label) {
|
|
118
92
|
requireValue(typeof relative === "string" && relative && !path.isAbsolute(relative), `${label} must be relative`);
|
|
119
93
|
const resolvedRoot = path.resolve(root);
|
|
@@ -204,6 +178,16 @@ function validateExecution(execution) {
|
|
|
204
178
|
return policy;
|
|
205
179
|
}
|
|
206
180
|
|
|
181
|
+
function validatePlayback(playback, maximumSeconds) {
|
|
182
|
+
exactKeys(playback, ["schema", "mode", "activeDurationMs", "finalHoldMs"], [], "scenario.playback");
|
|
183
|
+
requireValue(playback.schema === "buildchain.declarative-demo-playback/v1", "scenario playback schema is unsupported");
|
|
184
|
+
requireValue(playback.mode === "deterministic-readable", "scenario playback mode is invalid");
|
|
185
|
+
requireValue(Number.isInteger(playback.activeDurationMs) && playback.activeDurationMs >= 1000, "scenario playback active duration is invalid");
|
|
186
|
+
requireValue(Number.isInteger(playback.finalHoldMs) && playback.finalHoldMs >= 250 && playback.finalHoldMs <= 5000, "scenario playback final hold is invalid");
|
|
187
|
+
requireValue(playback.activeDurationMs + playback.finalHoldMs <= maximumSeconds * 1000, "scenario playback exceeds its declared duration class");
|
|
188
|
+
return playback;
|
|
189
|
+
}
|
|
190
|
+
|
|
207
191
|
function validateStep(step, stepLabel, stepIds, maximumSeconds) {
|
|
208
192
|
exactKeys(step, ["id", "argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes", "fileAssertions"], [], stepLabel);
|
|
209
193
|
requireValue(SAFE_ID.test(step.id) && !stepIds.has(step.id), `${stepLabel}.id is invalid or repeated`);
|
|
@@ -234,12 +218,18 @@ function validateDemo(demo, index, demoIds, maximumSeconds) {
|
|
|
234
218
|
}
|
|
235
219
|
|
|
236
220
|
export function validateScenario(value) {
|
|
237
|
-
exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], ["transportSmoke"], "scenario");
|
|
221
|
+
exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], ["compositionMode", "playback", "transportSmoke", "presentation"], "scenario");
|
|
238
222
|
requireValue(value.schema === "buildchain.declarative-binary-demo/v1", "unsupported scenario schema");
|
|
223
|
+
const compositionMode = value.compositionMode ?? PRESENTATION_FRAMED;
|
|
224
|
+
requireValue(
|
|
225
|
+
compositionMode === PRESENTATION_FRAMED || compositionMode === TERMINAL_FILL,
|
|
226
|
+
"scenario composition mode is invalid",
|
|
227
|
+
);
|
|
239
228
|
validateProduct(value.product);
|
|
240
229
|
validateArtifact(value.artifact);
|
|
241
230
|
const executionPolicy = validateExecution(value.execution);
|
|
242
|
-
|
|
231
|
+
if (value.playback) validatePlayback(value.playback, executionPolicy.maximumSeconds);
|
|
232
|
+
requireValue(stableJson(value.renditions) === stableJson(RENDITIONS), "scenario must declare both native rendition profiles exactly");
|
|
243
233
|
requireValue(Array.isArray(value.demos) && value.demos.length >= 1 && value.demos.length <= 8, "scenario requires 1 through 8 demos");
|
|
244
234
|
const demoIds = new Set();
|
|
245
235
|
value.demos.forEach((demo, index) => validateDemo(demo, index, demoIds, executionPolicy.maximumSeconds));
|
|
@@ -248,12 +238,13 @@ export function validateScenario(value) {
|
|
|
248
238
|
inside("/repository", value.publication.evidencePath, "scenario.publication.evidencePath");
|
|
249
239
|
inside("/repository", value.publication.readmePath, "scenario.publication.readmePath");
|
|
250
240
|
requireValue(SAFE_MARKER.test(value.publication.marker), "scenario publication marker is invalid");
|
|
241
|
+
if (value.presentation) validateDemoPresentation({ presentation: value.presentation, demos: value.demos, publication: value.publication, exactKeys, inside, requireValue, safeMarker: SAFE_MARKER });
|
|
251
242
|
exactKeys(value.authority, ["grants", "nonAuthorities"], [], "scenario.authority");
|
|
252
243
|
requireValue(JSON.stringify(value.authority) === JSON.stringify({ grants: [], nonAuthorities: NON_AUTHORITIES }), "scenario authority boundary is invalid");
|
|
253
244
|
return value;
|
|
254
245
|
}
|
|
255
246
|
|
|
256
|
-
function validateCapture(capture, rendition, summaryRoot, durationClass) {
|
|
247
|
+
function validateCapture(capture, rendition, summaryRoot, durationClass, declaredPlayback) {
|
|
257
248
|
const policy = durationPolicy(durationClass);
|
|
258
249
|
requireValue(capture.schema === "buildchain.declarative-terminal-capture/v1", "capture schema mismatch");
|
|
259
250
|
requireValue(JSON.stringify(capture.dimensions) === JSON.stringify({ columns: rendition.columns, rows: rendition.rows }), "capture dimensions mismatch");
|
|
@@ -268,13 +259,36 @@ function validateCapture(capture, rendition, summaryRoot, durationClass) {
|
|
|
268
259
|
requireValue(index > 0 || event.atMs === 0, "capture event timeline must start at zero");
|
|
269
260
|
previousAtMs = event.atMs;
|
|
270
261
|
}
|
|
262
|
+
if (!declaredPlayback) {
|
|
263
|
+
requireValue(capture.playback === undefined, "legacy capture unexpectedly declares playback evidence");
|
|
264
|
+
return capture;
|
|
265
|
+
}
|
|
266
|
+
exactKeys(capture.playback, [
|
|
267
|
+
"schema", "mode", "timingSource", "activeDurationMs", "finalHoldMs", "presentedDurationMs",
|
|
268
|
+
"observedLastEventMs", "eventPayloadRoot", "eventOrder",
|
|
269
|
+
], [], "capture.playback");
|
|
270
|
+
requireValue(capture.playback.schema === "buildchain.declarative-terminal-playback/v1", "capture playback schema mismatch");
|
|
271
|
+
requireValue(capture.playback.mode === declaredPlayback.mode, "capture playback mode mismatch");
|
|
272
|
+
requireValue(capture.playback.timingSource === "declared-event-ordinal", "capture playback timing source mismatch");
|
|
273
|
+
requireValue(capture.playback.activeDurationMs === declaredPlayback.activeDurationMs && capture.playback.finalHoldMs === declaredPlayback.finalHoldMs, "capture playback duration mismatch");
|
|
274
|
+
requireValue(capture.playback.presentedDurationMs === capture.durationMs && capture.durationMs === declaredPlayback.activeDurationMs + declaredPlayback.finalHoldMs, "capture presented duration mismatch");
|
|
275
|
+
requireValue(Number.isInteger(capture.playback.observedLastEventMs) && capture.playback.observedLastEventMs >= 0, "capture observed duration is invalid");
|
|
276
|
+
requireValue(capture.playback.eventOrder === "preserved", "capture playback event order mismatch");
|
|
277
|
+
requireValue(capture.playback.eventPayloadRoot === rootJson(capture.events.map((event) => event.data)), "capture playback payload root mismatch");
|
|
278
|
+
const lastIndex = capture.events.length - 1;
|
|
279
|
+
for (const [index, event] of capture.events.entries()) {
|
|
280
|
+
const expectedAtMs = lastIndex === 0 ? 0 : Math.round((index * declaredPlayback.activeDurationMs) / lastIndex);
|
|
281
|
+
requireValue(event.atMs === expectedAtMs, "capture playback timeline is not deterministic");
|
|
282
|
+
}
|
|
271
283
|
return capture;
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
function projection(capture, transcript, demo, rendition, durationClass, sharedCaptureDurationMs) {
|
|
286
|
+
function projection(capture, transcript, demo, rendition, durationClass, sharedCaptureDurationMs, compositionMode) {
|
|
275
287
|
const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
|
|
276
288
|
const policy = durationPolicy(durationClass);
|
|
277
|
-
const durationMs =
|
|
289
|
+
const durationMs = capture.playback
|
|
290
|
+
? sharedCaptureDurationMs
|
|
291
|
+
: Math.min(policy.maximumSeconds * 1000, sharedCaptureDurationMs + 1000);
|
|
278
292
|
const projected = {
|
|
279
293
|
schema: "kungfu.terminal-capture/v1",
|
|
280
294
|
command: capture.command,
|
|
@@ -293,6 +307,7 @@ function projection(capture, transcript, demo, rendition, durationClass, sharedC
|
|
|
293
307
|
height: rendition.height,
|
|
294
308
|
fps: policy.durationClass === "long-form" ? 10 : 15,
|
|
295
309
|
...(policy.durationClass === "long-form" ? { durationClass: "long-form" } : {}),
|
|
310
|
+
compositionMode,
|
|
296
311
|
durationMs,
|
|
297
312
|
title: demo.title,
|
|
298
313
|
commandLabel: capture.command,
|
|
@@ -322,8 +337,11 @@ export function adaptCapture({ artifactRoot, output }) {
|
|
|
322
337
|
const { root: _root, ...manifestBody } = manifest;
|
|
323
338
|
requireValue(DIGEST.test(declaredRoot) && rootJson(manifestBody) === declaredRoot, "capture manifest root mismatch");
|
|
324
339
|
requireValue(manifest.authority?.grants?.length === 0 && JSON.stringify(manifest.authority?.nonAuthorities) === JSON.stringify(NON_AUTHORITIES), "capture manifest grants authority");
|
|
340
|
+
const scenario = validateScenario(readJson(path.join(root, "scenario.json"), "captured scenario"));
|
|
341
|
+
requireValue(rootJson(scenario) === manifest.scenarioRoot, "captured scenario root mismatch");
|
|
325
342
|
requireValue(Array.isArray(manifest.renditions) && manifest.renditions.length === 2, "capture rendition set is invalid");
|
|
326
343
|
const executionPolicy = durationPolicy(manifest.execution?.durationClass);
|
|
344
|
+
requireValue(stableJson(manifest.execution?.playback) === stableJson(scenario.playback), "capture manifest playback declaration mismatch");
|
|
327
345
|
prepareOutput(output);
|
|
328
346
|
const set = [];
|
|
329
347
|
const loaded = RENDITIONS.map((expected, index) => {
|
|
@@ -335,7 +353,7 @@ export function adaptCapture({ artifactRoot, output }) {
|
|
|
335
353
|
const summary = readJson(inside(root, descriptor.runSummary, "run summary"), "run summary");
|
|
336
354
|
requireValue(rootJson(summary) === descriptor.runSummaryRoot, "run summary root mismatch");
|
|
337
355
|
const captureBytes = regular(inside(root, descriptor.terminalCapture, "terminal capture"), "terminal capture", 4 * 1024 * 1024);
|
|
338
|
-
const capture = validateCapture(JSON.parse(captureBytes.toString("utf8")), expected, descriptor.runSummaryRoot, executionPolicy.durationClass);
|
|
356
|
+
const capture = validateCapture(JSON.parse(captureBytes.toString("utf8")), expected, descriptor.runSummaryRoot, executionPolicy.durationClass, scenario.playback);
|
|
339
357
|
requireValue(rootJson(capture) === descriptor.terminalCaptureRoot, "terminal capture root mismatch");
|
|
340
358
|
return { index, expected, descriptor, transcript, capture };
|
|
341
359
|
});
|
|
@@ -349,6 +367,7 @@ export function adaptCapture({ artifactRoot, output }) {
|
|
|
349
367
|
expected,
|
|
350
368
|
executionPolicy.durationClass,
|
|
351
369
|
sharedCaptureDurationMs,
|
|
370
|
+
scenario.compositionMode ?? PRESENTATION_FRAMED,
|
|
352
371
|
);
|
|
353
372
|
const suffix = index === 0 ? "" : "-720p";
|
|
354
373
|
fs.writeFileSync(path.join(output, `complete-transcript${suffix}.txt`), transcript);
|
|
@@ -367,10 +386,7 @@ export function adaptCapture({ artifactRoot, output }) {
|
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
function copyRegular(source, destination, label) {
|
|
370
|
-
|
|
371
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
372
|
-
fs.writeFileSync(destination, bytes);
|
|
373
|
-
return { path: path.basename(destination), bytes: bytes.length, root: rootBytes(bytes) };
|
|
389
|
+
return copyVerifiedRegular(source, destination, label, MAX_MEDIA_MEMBER_BYTES);
|
|
374
390
|
}
|
|
375
391
|
|
|
376
392
|
function replaceReadmeBlock(readme, marker, block) {
|
|
@@ -412,8 +428,14 @@ export function materializeDemo({ repositoryRoot, scenarioPath, demoId, captureR
|
|
|
412
428
|
requireValue(captureManifest.demo?.id === demoId && captureManifest.scenarioRoot === rootJson(scenario), "capture does not bind the exact scenario demo");
|
|
413
429
|
const gateReceipt = readJson(path.join(path.resolve(gateBundle), "gate-receipt.json"), "gate receipt");
|
|
414
430
|
const mediaReceipt = readJson(path.join(path.resolve(mediaBundle), "media-receipt.json"), "media receipt");
|
|
415
|
-
const gateRoot =
|
|
416
|
-
|
|
431
|
+
const gateRoot = verifyBundleChecksums(gateBundle, "Gate bundle", { maximumBundleBytes: MAX_GATE_BUNDLE_BYTES });
|
|
432
|
+
requireValue(DIGEST.test(mediaReceipt.rendererManifestRoot), "media receipt renderer manifest root is invalid");
|
|
433
|
+
const mediaRoot = verifyBundleChecksums(mediaBundle, "media bundle", {
|
|
434
|
+
allowLongFormRendererManifest: scenario.execution.durationClass === "long-form",
|
|
435
|
+
maximumBundleBytes: MAX_MEDIA_BUNDLE_BYTES,
|
|
436
|
+
maximumMemberBytes: MAX_MEDIA_MEMBER_BYTES,
|
|
437
|
+
rendererManifestRoot: mediaReceipt.rendererManifestRoot,
|
|
438
|
+
});
|
|
417
439
|
requireValue(gateReceipt.status === "passed" && mediaReceipt.status === "passed", "Gate and media receipts must pass");
|
|
418
440
|
requireValue(mediaReceipt.qualifiedGateRoot === gateRoot, "media receipt is not bound to the exact qualified Gate");
|
|
419
441
|
const sourceCoordinate = readJson(path.join(path.resolve(captureRoot), "source-coordinate.json"), "source coordinate");
|
|
@@ -462,11 +484,12 @@ export function materializeDemo({ repositoryRoot, scenarioPath, demoId, captureR
|
|
|
462
484
|
? scenario.publication.marker
|
|
463
485
|
: `${scenario.publication.marker}:${demo.id}`;
|
|
464
486
|
const commandLines = demo.steps.map((step) => `$ ${scenario.product.binaryName} ${step.argv.join(" ")}`.trim()).join("\n");
|
|
465
|
-
const
|
|
487
|
+
const imageLine = `[](${relative}/public-evidence.json)`;
|
|
488
|
+
const legacyBlock = [
|
|
466
489
|
`<!-- ${marker}:start -->`,
|
|
467
490
|
`## ${demo.title}`,
|
|
468
491
|
"",
|
|
469
|
-
|
|
492
|
+
imageLine,
|
|
470
493
|
"",
|
|
471
494
|
"Animation scenario:",
|
|
472
495
|
"",
|
|
@@ -488,10 +511,20 @@ export function materializeDemo({ repositoryRoot, scenarioPath, demoId, captureR
|
|
|
488
511
|
"</details>",
|
|
489
512
|
`<!-- ${marker}:end -->`,
|
|
490
513
|
].join("\n");
|
|
514
|
+
let block = legacyBlock;
|
|
515
|
+
let technicalSpecPath = "";
|
|
516
|
+
if (scenario.presentation) {
|
|
517
|
+
const materialized = materializeDemoPresentation({
|
|
518
|
+
repository, scenario, demo, evidenceDirectory, imageLine, commandLines,
|
|
519
|
+
inside, regular, replaceBlock: replaceReadmeBlock, requireValue,
|
|
520
|
+
});
|
|
521
|
+
block = [`<!-- ${marker}:start -->`, ...materialized.blockLines, `<!-- ${marker}:end -->`].join("\n");
|
|
522
|
+
technicalSpecPath = materialized.technicalSpecPath;
|
|
523
|
+
}
|
|
491
524
|
const readmePath = inside(repository, scenario.publication.readmePath, "README path");
|
|
492
525
|
const readme = regular(readmePath, "README", 4 * 1024 * 1024).toString("utf8");
|
|
493
526
|
fs.writeFileSync(readmePath, replaceReadmeBlock(readme, marker, block));
|
|
494
|
-
return { ok: true, demoId, evidenceRoot, evidenceDirectory: relative, passportRoot: passport.passportRoot };
|
|
527
|
+
return { ok: true, demoId, evidenceRoot, evidenceDirectory: relative, passportRoot: passport.passportRoot, technicalSpecPath };
|
|
495
528
|
}
|
|
496
529
|
|
|
497
530
|
function parseArgs(argv) {
|
|
@@ -521,7 +554,10 @@ function main(argv = process.argv.slice(2)) {
|
|
|
521
554
|
}
|
|
522
555
|
if (command === "publication") {
|
|
523
556
|
const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
|
|
524
|
-
process.stdout.write(stableJson(
|
|
557
|
+
process.stdout.write(stableJson({
|
|
558
|
+
...scenario.publication,
|
|
559
|
+
...(scenario.presentation ? { technicalSpecPath: scenario.presentation.materialization.technicalSpecPath } : {}),
|
|
560
|
+
}));
|
|
525
561
|
return;
|
|
526
562
|
}
|
|
527
563
|
if (command === "prepare-artifact") {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function boundedText(value, maximum, label, requireValue) {
|
|
5
|
+
requireValue(typeof value === "string" && value.length > 0 && value.length <= maximum, `${label} is invalid`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validateDemoPresentation({ presentation, demos, publication, exactKeys, inside, requireValue, safeMarker }) {
|
|
9
|
+
exactKeys(presentation, ["schema", "proofs", "materialization"], [], "scenario.presentation");
|
|
10
|
+
requireValue(presentation.schema === "buildchain.declarative-demo-presentation/v1", "scenario presentation schema is unsupported");
|
|
11
|
+
requireValue(Array.isArray(presentation.proofs) && presentation.proofs.length === demos.length, "scenario presentation must bind every demo exactly once");
|
|
12
|
+
const labels = new Set();
|
|
13
|
+
for (const [index, proof] of presentation.proofs.entries()) {
|
|
14
|
+
const label = `scenario.presentation.proofs[${index}]`;
|
|
15
|
+
exactKeys(proof, ["demoId", "label", "question", "summary"], ["transitionAfter"], label);
|
|
16
|
+
requireValue(proof.demoId === demos[index].id, `${label}.demoId must preserve demo order`);
|
|
17
|
+
boundedText(proof.label, 80, `${label}.label`, requireValue);
|
|
18
|
+
requireValue(!labels.has(proof.label), `${label}.label is repeated`);
|
|
19
|
+
labels.add(proof.label);
|
|
20
|
+
boundedText(proof.question, 120, `${label}.question`, requireValue);
|
|
21
|
+
requireValue(proof.question === demos[index].title, `${label}.question must equal the demo title used by capture and media`);
|
|
22
|
+
boundedText(proof.summary, 500, `${label}.summary`, requireValue);
|
|
23
|
+
if (proof.transitionAfter !== undefined) boundedText(proof.transitionAfter, 500, `${label}.transitionAfter`, requireValue);
|
|
24
|
+
}
|
|
25
|
+
const materialization = presentation.materialization;
|
|
26
|
+
exactKeys(materialization, ["readmeMode", "technicalSpecPath", "technicalSpecTitle", "technicalMarker"], [], "scenario.presentation.materialization");
|
|
27
|
+
requireValue(["full", "media-only"].includes(materialization.readmeMode), "scenario presentation readmeMode is invalid");
|
|
28
|
+
const technicalSpecPath = inside("/repository", materialization.technicalSpecPath, "scenario presentation technicalSpecPath");
|
|
29
|
+
const readmePath = inside("/repository", publication.readmePath, "scenario publication readmePath");
|
|
30
|
+
requireValue(technicalSpecPath !== readmePath, "scenario presentation technical spec must be separate from the README");
|
|
31
|
+
boundedText(materialization.technicalSpecTitle, 120, "scenario.presentation.materialization.technicalSpecTitle", requireValue);
|
|
32
|
+
requireValue(safeMarker.test(materialization.technicalMarker), "scenario presentation technicalMarker is invalid");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureOrderedMarkers(document, presentation, requireValue) {
|
|
36
|
+
let result = document;
|
|
37
|
+
for (const proof of presentation.proofs) {
|
|
38
|
+
const marker = `${presentation.materialization.technicalMarker}:${proof.demoId}`;
|
|
39
|
+
const start = `<!-- ${marker}:start -->`;
|
|
40
|
+
const end = `<!-- ${marker}:end -->`;
|
|
41
|
+
const first = result.indexOf(start);
|
|
42
|
+
const last = result.indexOf(end);
|
|
43
|
+
requireValue((first === -1) === (last === -1), "technical specification materialization markers are incomplete");
|
|
44
|
+
if (first !== -1) {
|
|
45
|
+
requireValue(result.indexOf(start, first + start.length) === -1 && result.indexOf(end, last + end.length) === -1 && last > first, "technical specification materialization markers are ambiguous");
|
|
46
|
+
} else {
|
|
47
|
+
result = `${result.trimEnd()}\n\n${start}\n${end}\n`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function materializeDemoPresentation({ repository, scenario, demo, evidenceDirectory, imageLine, commandLines, inside, regular, replaceBlock, requireValue }) {
|
|
54
|
+
const presentation = scenario.presentation;
|
|
55
|
+
const proof = presentation.proofs.find((entry) => entry.demoId === demo.id);
|
|
56
|
+
const technical = presentation.materialization;
|
|
57
|
+
const technicalSpecPath = inside(repository, technical.technicalSpecPath, "technical specification path");
|
|
58
|
+
const technicalRelative = path.relative(path.dirname(technicalSpecPath), evidenceDirectory).split(path.sep).join("/");
|
|
59
|
+
const technicalMarker = `${technical.technicalMarker}:${demo.id}`;
|
|
60
|
+
const technicalBlock = [
|
|
61
|
+
`<!-- ${technicalMarker}:start -->`, `## ${proof.label}: ${proof.question}`, "", proof.summary, "",
|
|
62
|
+
`[](${technicalRelative}/public-evidence.json)`, "", "Commands:", "", "```text", commandLines, "```", "",
|
|
63
|
+
`Native renditions: [1080p MP4](${technicalRelative}/demo.mp4) · [1080p WebM](${technicalRelative}/demo.webm) · [720p MP4](${technicalRelative}/demo-720p.mp4) · [720p WebM](${technicalRelative}/demo-720p.webm)`, "",
|
|
64
|
+
`Claim boundary: ${demo.claimBoundary}`, "", `[Release Passport](${technicalRelative}/release-passport.json) · [auditable evidence](${technicalRelative}/public-evidence.json)`,
|
|
65
|
+
...(proof.transitionAfter ? ["", proof.transitionAfter] : []), `<!-- ${technicalMarker}:end -->`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
fs.mkdirSync(path.dirname(technicalSpecPath), { recursive: true });
|
|
68
|
+
const existing = fs.existsSync(technicalSpecPath)
|
|
69
|
+
? regular(technicalSpecPath, "technical specification", 4 * 1024 * 1024).toString("utf8")
|
|
70
|
+
: `# ${technical.technicalSpecTitle}\n`;
|
|
71
|
+
const document = ensureOrderedMarkers(existing, presentation, requireValue);
|
|
72
|
+
fs.writeFileSync(technicalSpecPath, replaceBlock(document, technicalMarker, technicalBlock));
|
|
73
|
+
|
|
74
|
+
if (technical.readmeMode === "media-only") {
|
|
75
|
+
return { blockLines: [imageLine], technicalSpecPath: technical.technicalSpecPath };
|
|
76
|
+
}
|
|
77
|
+
const readmePath = inside(repository, scenario.publication.readmePath, "README path");
|
|
78
|
+
const technicalLink = path.relative(path.dirname(readmePath), technicalSpecPath).split(path.sep).join("/");
|
|
79
|
+
return {
|
|
80
|
+
blockLines: [`## ${proof.question}`, "", proof.summary, "", imageLine, "", `[Technical specification and evidence](${technicalLink})`, ...(proof.transitionAfter ? ["", proof.transitionAfter] : [])],
|
|
81
|
+
technicalSpecPath: technical.technicalSpecPath,
|
|
82
|
+
};
|
|
83
|
+
}
|