@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0
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 +31 -0
- package/actions/macos-credential-island/README.md +8 -0
- package/bin/buildchain.mjs +1 -1
- package/contracts/auditable-demo-media-profiles-v1.json +61 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
- package/dist/site/buildchain-contract.json +107 -26
- package/dist/site/buildchain-site.json +121 -49
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +9 -3
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +101 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +105 -29
- package/dist/site/public-surface-audit.json +184 -11
- package/dist/site/publication-authority-registry.json +62 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +118 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
- package/docs/cli-reference.md +35 -2
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +123 -72
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +39 -0
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +53 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +36 -1
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-pickup.js +387 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-release.js +61 -18
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +8 -4
- package/packages/core/release-propagation.js +27 -2
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +564 -0
- package/scripts/auditable-demo-renditions.mjs +7 -2
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/auditable-demo.mjs +27 -21
- package/scripts/aws-macos-jit-controller-core.mjs +389 -0
- package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
- package/scripts/aws-macos-jit-controller.mjs +558 -0
- package/scripts/aws-macos-jit-job-controller.mjs +401 -0
- package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
- package/scripts/aws-windows-jit-campaign.mjs +202 -0
- package/scripts/aws-windows-jit-controller-core.mjs +20 -2
- package/scripts/aws-windows-jit-controller.mjs +184 -109
- package/scripts/aws-windows-jit-core.mjs +21 -3
- package/scripts/aws-windows-jit.mjs +2 -0
- package/scripts/build-standalone-binary.mjs +6 -0
- package/scripts/buildchain-cli-help.mjs +2 -1
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/check-inventory.mjs +6 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/locked-source-checkout.mjs +17 -3
- package/scripts/release-propagation.mjs +150 -2
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
- package/scripts/workflow-call-contract.mjs +133 -0
|
@@ -0,0 +1,564 @@
|
|
|
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
|
+
const MAX_EXECUTABLE_FILES = 32;
|
|
29
|
+
|
|
30
|
+
function durationPolicy(value = "standard") {
|
|
31
|
+
requireValue(value === "standard" || value === "long-form", "scenario duration class is invalid");
|
|
32
|
+
return {
|
|
33
|
+
durationClass: value,
|
|
34
|
+
maximumSeconds: value === "long-form" ? LONG_FORM_MAX_SECONDS : STANDARD_MAX_SECONDS,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fail(message) {
|
|
39
|
+
throw new Error(`auditable demo platform: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireValue(condition, message) {
|
|
43
|
+
if (!condition) fail(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stableValue(value) {
|
|
47
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
48
|
+
if (value && typeof value === "object") {
|
|
49
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stableJson(value) {
|
|
55
|
+
return `${JSON.stringify(stableValue(value), null, 2)}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rootBytes(value) {
|
|
59
|
+
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rootJson(value) {
|
|
63
|
+
return rootBytes(Buffer.from(stableJson(value)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function regular(file, label, maximum = 8 * 1024 * 1024) {
|
|
67
|
+
const metadata = fs.lstatSync(file);
|
|
68
|
+
requireValue(metadata.isFile() && !metadata.isSymbolicLink() && metadata.size <= maximum, `${label} must be a bounded regular file`);
|
|
69
|
+
return fs.readFileSync(file);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readJson(file, label) {
|
|
73
|
+
try {
|
|
74
|
+
const value = JSON.parse(regular(file, label).toString("utf8"));
|
|
75
|
+
requireValue(value && typeof value === "object" && !Array.isArray(value), `${label} must contain an object`);
|
|
76
|
+
return value;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof SyntaxError) fail(`${label} is invalid JSON`);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
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
|
+
function inside(root, relative, label) {
|
|
118
|
+
requireValue(typeof relative === "string" && relative && !path.isAbsolute(relative), `${label} must be relative`);
|
|
119
|
+
const resolvedRoot = path.resolve(root);
|
|
120
|
+
const resolved = path.resolve(resolvedRoot, relative);
|
|
121
|
+
requireValue(resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`), `${label} escapes its root`);
|
|
122
|
+
return resolved;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function exactKeys(value, required, optional, label) {
|
|
126
|
+
requireValue(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
|
127
|
+
const allowed = new Set([...required, ...optional]);
|
|
128
|
+
for (const key of required) requireValue(Object.hasOwn(value, key), `${label}.${key} is required`);
|
|
129
|
+
for (const key of Object.keys(value)) requireValue(allowed.has(key), `${label}.${key} is not allowed`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateProduct(product) {
|
|
133
|
+
exactKeys(product, ["id", "displayName", "binaryName"], [], "scenario.product");
|
|
134
|
+
requireValue(SAFE_ID.test(product.id), "scenario.product.id is invalid");
|
|
135
|
+
requireValue(typeof product.displayName === "string" && product.displayName.length > 0 && product.displayName.length <= 80, "scenario.product.displayName is invalid");
|
|
136
|
+
requireValue(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(product.binaryName), "scenario.product.binaryName is invalid");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateArtifact(artifact) {
|
|
140
|
+
exactKeys(artifact, ["platformId", "binaryPath", "metadataPath", "metadataContract", "runtimeDependencies"], [], "scenario.artifact");
|
|
141
|
+
requireValue(artifact.platformId === "linux-x64", "scenario artifact must be linux-x64");
|
|
142
|
+
for (const key of ["binaryPath", "metadataPath"]) inside("/scenario-root", artifact[key], `scenario.artifact.${key}`);
|
|
143
|
+
requireValue(typeof artifact.metadataContract === "string" && artifact.metadataContract.length > 0, "scenario artifact metadataContract is invalid");
|
|
144
|
+
requireValue(Array.isArray(artifact.runtimeDependencies) && artifact.runtimeDependencies.length === 0, "scenario artifact must be standalone");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateTransportSmoke(smoke) {
|
|
148
|
+
exactKeys(smoke, ["argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes"], [], "scenario.transportSmoke");
|
|
149
|
+
requireValue(Array.isArray(smoke.argv) && smoke.argv.length >= 1 && smoke.argv.length <= 64 && smoke.argv.every((item) => typeof item === "string" && !item.includes("\0") && item.length <= 512), "scenario transport smoke argv is invalid");
|
|
150
|
+
requireValue(Number.isInteger(smoke.timeoutSeconds) && smoke.timeoutSeconds >= 1 && smoke.timeoutSeconds <= STANDARD_MAX_SECONDS, "scenario transport smoke timeout is invalid");
|
|
151
|
+
requireValue(Array.isArray(smoke.expectedExitCodes) && smoke.expectedExitCodes.length >= 1 && smoke.expectedExitCodes.length <= 4 && smoke.expectedExitCodes.every((item) => Number.isInteger(item) && item >= 0 && item <= 255), "scenario transport smoke expected exits are invalid");
|
|
152
|
+
requireValue(Array.isArray(smoke.stdoutIncludes) && smoke.stdoutIncludes.length <= 32 && smoke.stdoutIncludes.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 256), "scenario transport smoke stdout assertions are invalid");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateExecutableClosure({ artifactRoot, scenario, requireExecutable }) {
|
|
156
|
+
const root = path.resolve(artifactRoot);
|
|
157
|
+
const artifact = scenario.artifact;
|
|
158
|
+
const metadata = readJson(inside(root, artifact.metadataPath, "artifact metadata path"), "binary metadata");
|
|
159
|
+
requireValue(metadata.contract === artifact.metadataContract, "binary metadata contract mismatch");
|
|
160
|
+
requireValue((metadata.platformId || metadata.platform) === artifact.platformId, "binary metadata platform mismatch");
|
|
161
|
+
requireValue(JSON.stringify(metadata.runtimeDependencies) === JSON.stringify(artifact.runtimeDependencies), "binary metadata runtime dependency mismatch");
|
|
162
|
+
requireValue(Array.isArray(metadata.executableFiles) && metadata.executableFiles.length >= 1 && metadata.executableFiles.length <= MAX_EXECUTABLE_FILES, "binary metadata executableFiles must be a bounded non-empty array");
|
|
163
|
+
const declared = new Set();
|
|
164
|
+
const files = metadata.executableFiles.map((entry, index) => {
|
|
165
|
+
const label = `binary metadata executableFiles[${index}]`;
|
|
166
|
+
exactKeys(entry, ["path", "sha256"], [], label);
|
|
167
|
+
requireValue(!declared.has(entry.path), `${label}.path is repeated`);
|
|
168
|
+
declared.add(entry.path);
|
|
169
|
+
requireValue(/^[0-9a-f]{64}$/u.test(entry.sha256), `${label}.sha256 is invalid`);
|
|
170
|
+
const file = inside(root, entry.path, `${label}.path`);
|
|
171
|
+
const bytes = regular(file, label, 256 * 1024 * 1024);
|
|
172
|
+
requireValue(rootBytes(bytes) === `sha256:${entry.sha256}`, `${label} digest differs from exact artifact metadata`);
|
|
173
|
+
if (requireExecutable) requireValue((fs.statSync(file).mode & 0o111) !== 0, `${label} is not executable`);
|
|
174
|
+
return { path: entry.path, file, sha256: entry.sha256 };
|
|
175
|
+
});
|
|
176
|
+
requireValue(declared.has(artifact.binaryPath), "binary metadata executableFiles must include scenario.artifact.binaryPath");
|
|
177
|
+
const binary = files.find((entry) => entry.path === artifact.binaryPath);
|
|
178
|
+
const metadataBinarySha256 = String(metadata.sha256 || "").replace(/^sha256:/u, "");
|
|
179
|
+
requireValue(/^[0-9a-f]{64}$/u.test(metadataBinarySha256) && metadataBinarySha256 === binary.sha256, "binary digest differs from exact artifact metadata");
|
|
180
|
+
return { metadata, files };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function prepareArtifact({ artifactRoot, scenarioPath }) {
|
|
184
|
+
const scenario = validateScenario(readJson(path.resolve(scenarioPath), "scenario"));
|
|
185
|
+
const closure = validateExecutableClosure({ artifactRoot, scenario, requireExecutable: false });
|
|
186
|
+
for (const entry of closure.files) fs.chmodSync(entry.file, 0o755);
|
|
187
|
+
validateExecutableClosure({ artifactRoot, scenario, requireExecutable: true });
|
|
188
|
+
return {
|
|
189
|
+
executableFiles: closure.files.map(({ path: relative, sha256 }) => ({ path: relative, sha256 })),
|
|
190
|
+
metadataRoot: rootJson(closure.metadata),
|
|
191
|
+
authority: { classification: "artifact-mode-restoration", grants: [], nonAuthorities: NON_AUTHORITIES },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateExecution(execution) {
|
|
196
|
+
exactKeys(execution, ["deterministic", "network", "secrets", "totalTimeoutSeconds", "environment"], ["durationClass"], "scenario.execution");
|
|
197
|
+
requireValue(execution.deterministic === true && execution.network === "none" && execution.secrets === "none", "scenario execution must be deterministic, network-disabled, and secret-free");
|
|
198
|
+
const policy = durationPolicy(execution.durationClass);
|
|
199
|
+
requireValue(Number.isInteger(execution.totalTimeoutSeconds) && execution.totalTimeoutSeconds >= 1 && execution.totalTimeoutSeconds <= policy.maximumSeconds, "scenario total timeout is invalid");
|
|
200
|
+
requireValue(execution.environment && typeof execution.environment === "object" && !Array.isArray(execution.environment), "scenario environment must be an object");
|
|
201
|
+
for (const [key, item] of Object.entries(execution.environment)) {
|
|
202
|
+
requireValue(/^[A-Z][A-Z0-9_]{0,63}$/u.test(key) && typeof item === "string" && item.length <= 256, `scenario environment entry is invalid: ${key}`);
|
|
203
|
+
}
|
|
204
|
+
return policy;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateStep(step, stepLabel, stepIds, maximumSeconds) {
|
|
208
|
+
exactKeys(step, ["id", "argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes", "fileAssertions"], [], stepLabel);
|
|
209
|
+
requireValue(SAFE_ID.test(step.id) && !stepIds.has(step.id), `${stepLabel}.id is invalid or repeated`);
|
|
210
|
+
stepIds.add(step.id);
|
|
211
|
+
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`);
|
|
212
|
+
requireValue(!Object.hasOwn(step, "command"), `${stepLabel} must not use a shell command string`);
|
|
213
|
+
requireValue(Number.isInteger(step.timeoutSeconds) && step.timeoutSeconds >= 1 && step.timeoutSeconds <= maximumSeconds, `${stepLabel}.timeoutSeconds is invalid`);
|
|
214
|
+
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`);
|
|
215
|
+
requireValue(Array.isArray(step.stdoutIncludes) && step.stdoutIncludes.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 256), `${stepLabel}.stdoutIncludes is invalid`);
|
|
216
|
+
requireValue(Array.isArray(step.fileAssertions) && step.fileAssertions.length <= 32, `${stepLabel}.fileAssertions is invalid`);
|
|
217
|
+
for (const assertion of step.fileAssertions) {
|
|
218
|
+
exactKeys(assertion, ["path", "jsonEquals"], [], `${stepLabel}.fileAssertions[]`);
|
|
219
|
+
inside("/workspace", assertion.path, `${stepLabel} assertion path`);
|
|
220
|
+
requireValue(assertion.jsonEquals && typeof assertion.jsonEquals === "object" && !Array.isArray(assertion.jsonEquals), `${stepLabel} jsonEquals is invalid`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateDemo(demo, index, demoIds, maximumSeconds) {
|
|
225
|
+
const label = `scenario.demos[${index}]`;
|
|
226
|
+
exactKeys(demo, ["id", "title", "claimBoundary", "steps"], [], label);
|
|
227
|
+
requireValue(SAFE_ID.test(demo.id) && !demoIds.has(demo.id), `${label}.id is invalid or repeated`);
|
|
228
|
+
demoIds.add(demo.id);
|
|
229
|
+
requireValue(typeof demo.title === "string" && demo.title.length > 0 && demo.title.length <= 120, `${label}.title is invalid`);
|
|
230
|
+
requireValue(typeof demo.claimBoundary === "string" && demo.claimBoundary.length > 0 && demo.claimBoundary.length <= 500, `${label}.claimBoundary is invalid`);
|
|
231
|
+
requireValue(Array.isArray(demo.steps) && demo.steps.length >= 1 && demo.steps.length <= 12, `${label}.steps is invalid`);
|
|
232
|
+
const stepIds = new Set();
|
|
233
|
+
demo.steps.forEach((step, stepIndex) => validateStep(step, `${label}.steps[${stepIndex}]`, stepIds, maximumSeconds));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function validateScenario(value) {
|
|
237
|
+
exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], ["transportSmoke"], "scenario");
|
|
238
|
+
requireValue(value.schema === "buildchain.declarative-binary-demo/v1", "unsupported scenario schema");
|
|
239
|
+
validateProduct(value.product);
|
|
240
|
+
validateArtifact(value.artifact);
|
|
241
|
+
const executionPolicy = validateExecution(value.execution);
|
|
242
|
+
requireValue(JSON.stringify(value.renditions) === JSON.stringify(RENDITIONS), "scenario must declare both native rendition profiles exactly");
|
|
243
|
+
requireValue(Array.isArray(value.demos) && value.demos.length >= 1 && value.demos.length <= 8, "scenario requires 1 through 8 demos");
|
|
244
|
+
const demoIds = new Set();
|
|
245
|
+
value.demos.forEach((demo, index) => validateDemo(demo, index, demoIds, executionPolicy.maximumSeconds));
|
|
246
|
+
if (value.transportSmoke) validateTransportSmoke(value.transportSmoke);
|
|
247
|
+
exactKeys(value.publication, ["evidencePath", "readmePath", "marker"], [], "scenario.publication");
|
|
248
|
+
inside("/repository", value.publication.evidencePath, "scenario.publication.evidencePath");
|
|
249
|
+
inside("/repository", value.publication.readmePath, "scenario.publication.readmePath");
|
|
250
|
+
requireValue(SAFE_MARKER.test(value.publication.marker), "scenario publication marker is invalid");
|
|
251
|
+
exactKeys(value.authority, ["grants", "nonAuthorities"], [], "scenario.authority");
|
|
252
|
+
requireValue(JSON.stringify(value.authority) === JSON.stringify({ grants: [], nonAuthorities: NON_AUTHORITIES }), "scenario authority boundary is invalid");
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function validateCapture(capture, rendition, summaryRoot, durationClass) {
|
|
257
|
+
const policy = durationPolicy(durationClass);
|
|
258
|
+
requireValue(capture.schema === "buildchain.declarative-terminal-capture/v1", "capture schema mismatch");
|
|
259
|
+
requireValue(JSON.stringify(capture.dimensions) === JSON.stringify({ columns: rendition.columns, rows: rendition.rows }), "capture dimensions mismatch");
|
|
260
|
+
requireValue(capture.completion?.status === "qualified" && capture.completion?.reportRoot === summaryRoot, "capture completion mismatch");
|
|
261
|
+
requireValue(capture.exitCode === 0 && capture.authority?.classification === "volatile-terminal-observation", "capture authority or exit mismatch");
|
|
262
|
+
requireValue(JSON.stringify(capture.authority) === JSON.stringify({ classification: "volatile-terminal-observation", grants: [], nonAuthorities: NON_AUTHORITIES }), "capture grants authority");
|
|
263
|
+
requireValue(Array.isArray(capture.events) && capture.events.length === capture.completion.eventCount && capture.events.length > 0, "capture events mismatch");
|
|
264
|
+
requireValue(Number.isInteger(capture.durationMs) && capture.durationMs >= 500 && capture.durationMs <= policy.maximumSeconds * 1000, "capture duration exceeds its declared class");
|
|
265
|
+
let previousAtMs = -1;
|
|
266
|
+
for (const [index, event] of capture.events.entries()) {
|
|
267
|
+
requireValue(Number.isInteger(event.atMs) && event.atMs >= 0 && event.atMs < capture.durationMs && event.atMs >= previousAtMs, "capture event timeline is invalid");
|
|
268
|
+
requireValue(index > 0 || event.atMs === 0, "capture event timeline must start at zero");
|
|
269
|
+
previousAtMs = event.atMs;
|
|
270
|
+
}
|
|
271
|
+
return capture;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function projection(capture, transcript, demo, rendition, durationClass, sharedCaptureDurationMs) {
|
|
275
|
+
const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
|
|
276
|
+
const policy = durationPolicy(durationClass);
|
|
277
|
+
const durationMs = Math.min(policy.maximumSeconds * 1000, sharedCaptureDurationMs + 1000);
|
|
278
|
+
const projected = {
|
|
279
|
+
schema: "kungfu.terminal-capture/v1",
|
|
280
|
+
command: capture.command,
|
|
281
|
+
dimensions: capture.dimensions,
|
|
282
|
+
durationMs: sharedCaptureDurationMs,
|
|
283
|
+
encoding: capture.encoding,
|
|
284
|
+
events: capture.events,
|
|
285
|
+
completion: capture.completion,
|
|
286
|
+
exitCode: capture.exitCode,
|
|
287
|
+
authority: capture.authority,
|
|
288
|
+
};
|
|
289
|
+
const scene = {
|
|
290
|
+
schema: "build-images.demo-scene/v1",
|
|
291
|
+
id: `${demo.id}-${rendition.id}`.slice(0, 64),
|
|
292
|
+
width: rendition.width,
|
|
293
|
+
height: rendition.height,
|
|
294
|
+
fps: policy.durationClass === "long-form" ? 10 : 15,
|
|
295
|
+
...(policy.durationClass === "long-form" ? { durationClass: "long-form" } : {}),
|
|
296
|
+
durationMs,
|
|
297
|
+
title: demo.title,
|
|
298
|
+
commandLabel: capture.command,
|
|
299
|
+
background: "#0B1020",
|
|
300
|
+
accent: "#67E8A5",
|
|
301
|
+
};
|
|
302
|
+
const publicProjection = {
|
|
303
|
+
schema: "build-images.demo-projection/v1",
|
|
304
|
+
evidenceClass: "exact-standalone-binary-declarative-demo/v1",
|
|
305
|
+
claimBoundary: demo.claimBoundary,
|
|
306
|
+
cues: [{ startMs: 0, endMs: durationMs, transcriptLines: lines.slice(0, 80).map((_, index) => index + 1), annotation: "declared exact-binary scenario" }],
|
|
307
|
+
};
|
|
308
|
+
return { projected, scene, publicProjection };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function prepareOutput(output) {
|
|
312
|
+
if (!fs.existsSync(output)) return fs.mkdirSync(output, { recursive: true });
|
|
313
|
+
const metadata = fs.lstatSync(output);
|
|
314
|
+
requireValue(metadata.isDirectory() && !metadata.isSymbolicLink() && fs.readdirSync(output).length === 0, "output must be an empty directory");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function adaptCapture({ artifactRoot, output }) {
|
|
318
|
+
const root = path.resolve(artifactRoot);
|
|
319
|
+
const manifest = readJson(path.join(root, "manifest.json"), "capture manifest");
|
|
320
|
+
requireValue(manifest.schema === "buildchain.declarative-demo-capture/v1" && manifest.status === "qualified", "capture manifest is not qualified");
|
|
321
|
+
const declaredRoot = manifest.root;
|
|
322
|
+
const { root: _root, ...manifestBody } = manifest;
|
|
323
|
+
requireValue(DIGEST.test(declaredRoot) && rootJson(manifestBody) === declaredRoot, "capture manifest root mismatch");
|
|
324
|
+
requireValue(manifest.authority?.grants?.length === 0 && JSON.stringify(manifest.authority?.nonAuthorities) === JSON.stringify(NON_AUTHORITIES), "capture manifest grants authority");
|
|
325
|
+
requireValue(Array.isArray(manifest.renditions) && manifest.renditions.length === 2, "capture rendition set is invalid");
|
|
326
|
+
const executionPolicy = durationPolicy(manifest.execution?.durationClass);
|
|
327
|
+
prepareOutput(output);
|
|
328
|
+
const set = [];
|
|
329
|
+
const loaded = RENDITIONS.map((expected, index) => {
|
|
330
|
+
const descriptor = manifest.renditions[index];
|
|
331
|
+
requireValue(descriptor.id === expected.id && descriptor.role === expected.role && descriptor.width === expected.width && descriptor.height === expected.height, `capture rendition ${index} mismatch`);
|
|
332
|
+
const transcriptBytes = regular(inside(root, descriptor.transcript, "capture transcript"), "capture transcript", 4 * 1024 * 1024);
|
|
333
|
+
const transcript = transcriptBytes.toString("utf8").replace(/\r\n/gu, "\n");
|
|
334
|
+
requireValue(transcript.trim().length > 0, "capture transcript is empty");
|
|
335
|
+
const summary = readJson(inside(root, descriptor.runSummary, "run summary"), "run summary");
|
|
336
|
+
requireValue(rootJson(summary) === descriptor.runSummaryRoot, "run summary root mismatch");
|
|
337
|
+
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);
|
|
339
|
+
requireValue(rootJson(capture) === descriptor.terminalCaptureRoot, "terminal capture root mismatch");
|
|
340
|
+
return { index, expected, descriptor, transcript, capture };
|
|
341
|
+
});
|
|
342
|
+
const sharedCaptureDurationMs = Math.max(...loaded.map((entry) => entry.capture.durationMs));
|
|
343
|
+
requireValue(sharedCaptureDurationMs <= executionPolicy.maximumSeconds * 1000, "native capture duration exceeds its declared class");
|
|
344
|
+
for (const { index, expected, transcript, capture } of loaded) {
|
|
345
|
+
const { projected, scene, publicProjection } = projection(
|
|
346
|
+
capture,
|
|
347
|
+
transcript,
|
|
348
|
+
manifest.demo,
|
|
349
|
+
expected,
|
|
350
|
+
executionPolicy.durationClass,
|
|
351
|
+
sharedCaptureDurationMs,
|
|
352
|
+
);
|
|
353
|
+
const suffix = index === 0 ? "" : "-720p";
|
|
354
|
+
fs.writeFileSync(path.join(output, `complete-transcript${suffix}.txt`), transcript);
|
|
355
|
+
fs.writeFileSync(path.join(output, `terminal-capture${suffix}.json`), stableJson(projected));
|
|
356
|
+
fs.writeFileSync(path.join(output, `scene${suffix}.json`), stableJson(scene));
|
|
357
|
+
fs.writeFileSync(path.join(output, `public-projection${suffix}.json`), stableJson(publicProjection));
|
|
358
|
+
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) });
|
|
359
|
+
}
|
|
360
|
+
requireValue(set[0].captureRoot !== set[1].captureRoot, "native rendition capture roots must differ");
|
|
361
|
+
fs.writeFileSync(path.join(output, "rendition-set.json"), stableJson({
|
|
362
|
+
schema: "kungfu.auditable-demo.rendition-set/v1",
|
|
363
|
+
renditions: set,
|
|
364
|
+
authority: { classification: "capture-routing-metadata", grants: [], nonAuthorities: ["publication-authority", "runtime-authority", ...NON_AUTHORITIES] },
|
|
365
|
+
}));
|
|
366
|
+
return { demoId: manifest.demo.id, captureRoot: declaredRoot, renditionRoots: set.map((entry) => entry.captureRoot) };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function copyRegular(source, destination, label) {
|
|
370
|
+
const bytes = regular(source, label, 64 * 1024 * 1024);
|
|
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) };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function replaceReadmeBlock(readme, marker, block) {
|
|
377
|
+
const start = `<!-- ${marker}:start -->`;
|
|
378
|
+
const end = `<!-- ${marker}:end -->`;
|
|
379
|
+
const first = readme.indexOf(start);
|
|
380
|
+
const last = readme.indexOf(end);
|
|
381
|
+
requireValue((first === -1) === (last === -1), "README materialization markers are incomplete");
|
|
382
|
+
if (first !== -1) {
|
|
383
|
+
requireValue(readme.indexOf(start, first + start.length) === -1 && readme.indexOf(end, last + end.length) === -1 && last > first, "README materialization markers are ambiguous");
|
|
384
|
+
return `${readme.slice(0, first)}${block}${readme.slice(last + end.length)}`;
|
|
385
|
+
}
|
|
386
|
+
const headingEnd = readme.indexOf("\n");
|
|
387
|
+
requireValue(headingEnd !== -1, "README must contain a title line");
|
|
388
|
+
return `${readme.slice(0, headingEnd + 1)}\n${block}\n${readme.slice(headingEnd + 1)}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function authorityBoundary() {
|
|
392
|
+
return {
|
|
393
|
+
grants: [],
|
|
394
|
+
nonAuthorities: NON_AUTHORITIES,
|
|
395
|
+
authorizationSources: [
|
|
396
|
+
"exact-release-passport",
|
|
397
|
+
"core-policy",
|
|
398
|
+
"work-or-warrant",
|
|
399
|
+
"explicit-capability-grant",
|
|
400
|
+
"runtime-isolation",
|
|
401
|
+
],
|
|
402
|
+
productSystemRole: "assembly-and-distribution-metadata-only",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function materializeDemo({ repositoryRoot, scenarioPath, demoId, captureRoot, gateBundle, mediaBundle, buildchainSha, rendererImage }) {
|
|
407
|
+
const repository = path.resolve(repositoryRoot);
|
|
408
|
+
const scenario = validateScenario(readJson(path.resolve(scenarioPath), "scenario"));
|
|
409
|
+
const demo = scenario.demos.find((entry) => entry.id === demoId);
|
|
410
|
+
requireValue(demo, `unknown demo id: ${demoId}`);
|
|
411
|
+
const captureManifest = readJson(path.join(path.resolve(captureRoot), "manifest.json"), "capture manifest");
|
|
412
|
+
requireValue(captureManifest.demo?.id === demoId && captureManifest.scenarioRoot === rootJson(scenario), "capture does not bind the exact scenario demo");
|
|
413
|
+
const gateReceipt = readJson(path.join(path.resolve(gateBundle), "gate-receipt.json"), "gate receipt");
|
|
414
|
+
const mediaReceipt = readJson(path.join(path.resolve(mediaBundle), "media-receipt.json"), "media receipt");
|
|
415
|
+
const gateRoot = verifyChecksums(gateBundle, "Gate bundle");
|
|
416
|
+
const mediaRoot = verifyChecksums(mediaBundle, "media bundle");
|
|
417
|
+
requireValue(gateReceipt.status === "passed" && mediaReceipt.status === "passed", "Gate and media receipts must pass");
|
|
418
|
+
requireValue(mediaReceipt.qualifiedGateRoot === gateRoot, "media receipt is not bound to the exact qualified Gate");
|
|
419
|
+
const sourceCoordinate = readJson(path.join(path.resolve(captureRoot), "source-coordinate.json"), "source coordinate");
|
|
420
|
+
requireValue(rootJson(sourceCoordinate) === captureManifest.sourceCoordinateRoot, "source coordinate root mismatch");
|
|
421
|
+
requireValue(DIGEST.test(buildchainSha) || /^[0-9a-f]{40}$/u.test(buildchainSha), "Buildchain runtime coordinate is invalid");
|
|
422
|
+
requireValue(/@sha256:[0-9a-f]{64}$/u.test(rendererImage), "renderer image must be immutable");
|
|
423
|
+
const evidencePreimage = {
|
|
424
|
+
schema: "buildchain.declarative-demo-evidence-root/v1",
|
|
425
|
+
scenarioRoot: captureManifest.scenarioRoot,
|
|
426
|
+
captureRoot: captureManifest.root,
|
|
427
|
+
gateRoot,
|
|
428
|
+
mediaRoot,
|
|
429
|
+
demoId,
|
|
430
|
+
};
|
|
431
|
+
const evidenceRoot = rootJson(evidencePreimage);
|
|
432
|
+
const evidenceDirectory = inside(repository, `${scenario.publication.evidencePath}/${evidenceRoot.slice(7)}/${demoId}`, "evidence directory");
|
|
433
|
+
fs.mkdirSync(evidenceDirectory, { recursive: true });
|
|
434
|
+
const publicFiles = [];
|
|
435
|
+
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"]) {
|
|
436
|
+
const source = path.join(path.resolve(mediaBundle), name);
|
|
437
|
+
if (fs.existsSync(source)) publicFiles.push(copyRegular(source, path.join(evidenceDirectory, name), `media ${name}`));
|
|
438
|
+
}
|
|
439
|
+
copyRegular(path.join(path.resolve(gateBundle), "gate-receipt.json"), path.join(evidenceDirectory, "qualified-gate-receipt.json"), "qualified Gate receipt");
|
|
440
|
+
copyRegular(path.join(path.resolve(captureRoot), "manifest.json"), path.join(evidenceDirectory, "capture-manifest.json"), "capture manifest");
|
|
441
|
+
copyRegular(path.join(path.resolve(captureRoot), "source-coordinate.json"), path.join(evidenceDirectory, "source-coordinate.json"), "source coordinate");
|
|
442
|
+
const passportBody = {
|
|
443
|
+
schema: "buildchain.declarative-demo-release-passport/v1",
|
|
444
|
+
status: "qualified",
|
|
445
|
+
product: scenario.product,
|
|
446
|
+
demo: { id: demo.id, title: demo.title, claimBoundary: demo.claimBoundary },
|
|
447
|
+
evidenceRoot,
|
|
448
|
+
scenarioRoot: captureManifest.scenarioRoot,
|
|
449
|
+
capture: { root: captureManifest.root, binary: captureManifest.artifact, networkIsolation: captureManifest.networkIsolation },
|
|
450
|
+
source: sourceCoordinate,
|
|
451
|
+
gate: { root: evidencePreimage.gateRoot },
|
|
452
|
+
media: { root: evidencePreimage.mediaRoot, profile: mediaReceipt.qualification?.profile?.id || "responsive-web-delivery-v1", qualificationRoot: mediaReceipt.qualificationRoot || mediaReceipt.qualification?.qualificationRoot || "" },
|
|
453
|
+
toolchain: { buildchainSha, rendererImage },
|
|
454
|
+
authority: authorityBoundary(),
|
|
455
|
+
};
|
|
456
|
+
const passport = { ...passportBody, passportRoot: rootJson(passportBody) };
|
|
457
|
+
fs.writeFileSync(path.join(evidenceDirectory, "release-passport.json"), stableJson(passport));
|
|
458
|
+
const publicEvidence = { ...evidencePreimage, evidenceRoot, passportRoot: passport.passportRoot, source: sourceCoordinate, files: publicFiles.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
459
|
+
fs.writeFileSync(path.join(evidenceDirectory, "public-evidence.json"), stableJson(publicEvidence));
|
|
460
|
+
const relative = path.relative(repository, evidenceDirectory).split(path.sep).join("/");
|
|
461
|
+
const marker = scenario.demos.length === 1
|
|
462
|
+
? scenario.publication.marker
|
|
463
|
+
: `${scenario.publication.marker}:${demo.id}`;
|
|
464
|
+
const commandLines = demo.steps.map((step) => `$ ${scenario.product.binaryName} ${step.argv.join(" ")}`.trim()).join("\n");
|
|
465
|
+
const block = [
|
|
466
|
+
`<!-- ${marker}:start -->`,
|
|
467
|
+
`## ${demo.title}`,
|
|
468
|
+
"",
|
|
469
|
+
`[](${relative}/public-evidence.json)`,
|
|
470
|
+
"",
|
|
471
|
+
"Animation scenario:",
|
|
472
|
+
"",
|
|
473
|
+
"```text",
|
|
474
|
+
commandLines,
|
|
475
|
+
"```",
|
|
476
|
+
"",
|
|
477
|
+
`Native renditions: [1080p MP4](${relative}/demo.mp4) · [1080p WebM](${relative}/demo.webm) · [720p MP4](${relative}/demo-720p.mp4) · [720p WebM](${relative}/demo-720p.webm)`,
|
|
478
|
+
"",
|
|
479
|
+
`[Static poster / reduced-motion fallback](${relative}/poster.png)`,
|
|
480
|
+
"",
|
|
481
|
+
"<details>",
|
|
482
|
+
"<summary>Evidence and claim boundary</summary>",
|
|
483
|
+
"",
|
|
484
|
+
`${demo.claimBoundary}`,
|
|
485
|
+
"",
|
|
486
|
+
`[Release Passport](${relative}/release-passport.json) · [auditable evidence](${relative}/public-evidence.json)`,
|
|
487
|
+
"",
|
|
488
|
+
"</details>",
|
|
489
|
+
`<!-- ${marker}:end -->`,
|
|
490
|
+
].join("\n");
|
|
491
|
+
const readmePath = inside(repository, scenario.publication.readmePath, "README path");
|
|
492
|
+
const readme = regular(readmePath, "README", 4 * 1024 * 1024).toString("utf8");
|
|
493
|
+
fs.writeFileSync(readmePath, replaceReadmeBlock(readme, marker, block));
|
|
494
|
+
return { ok: true, demoId, evidenceRoot, evidenceDirectory: relative, passportRoot: passport.passportRoot };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parseArgs(argv) {
|
|
498
|
+
const values = {};
|
|
499
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
500
|
+
const key = argv[index];
|
|
501
|
+
const value = argv[index + 1];
|
|
502
|
+
requireValue(key?.startsWith("--") && value !== undefined && !(key in values), `invalid argument near ${key || "<empty>"}`);
|
|
503
|
+
values[key.slice(2)] = value;
|
|
504
|
+
}
|
|
505
|
+
return values;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function main(argv = process.argv.slice(2)) {
|
|
509
|
+
const directAdapter = argv[0]?.startsWith("--");
|
|
510
|
+
const [command, ...rest] = directAdapter ? ["adapt", ...argv] : argv;
|
|
511
|
+
const args = parseArgs(rest);
|
|
512
|
+
if (command === "validate") {
|
|
513
|
+
const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
|
|
514
|
+
process.stdout.write(stableJson({ ok: true, scenarioRoot: rootJson(scenario), demoIds: scenario.demos.map((entry) => entry.id) }));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (command === "list") {
|
|
518
|
+
const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
|
|
519
|
+
process.stdout.write(`${scenario.demos.map((entry) => entry.id).join("\n")}\n`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (command === "publication") {
|
|
523
|
+
const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
|
|
524
|
+
process.stdout.write(stableJson(scenario.publication));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (command === "prepare-artifact") {
|
|
528
|
+
const result = prepareArtifact({
|
|
529
|
+
artifactRoot: path.resolve(args["artifact-root"]),
|
|
530
|
+
scenarioPath: path.resolve(args.scenario),
|
|
531
|
+
});
|
|
532
|
+
process.stdout.write(stableJson({ ok: true, ...result }));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (command === "adapt") {
|
|
536
|
+
const result = adaptCapture({ artifactRoot: path.resolve(args["artifact-root"]), output: path.resolve(args.output) });
|
|
537
|
+
process.stdout.write(stableJson({ ok: true, ...result }));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (command === "materialize") {
|
|
541
|
+
const result = materializeDemo({
|
|
542
|
+
repositoryRoot: path.resolve(args.repository),
|
|
543
|
+
scenarioPath: path.resolve(args.scenario),
|
|
544
|
+
demoId: args["demo-id"],
|
|
545
|
+
captureRoot: path.resolve(args.capture),
|
|
546
|
+
gateBundle: path.resolve(args.gate),
|
|
547
|
+
mediaBundle: path.resolve(args.media),
|
|
548
|
+
buildchainSha: args["buildchain-sha"],
|
|
549
|
+
rendererImage: args["renderer-image"],
|
|
550
|
+
});
|
|
551
|
+
process.stdout.write(stableJson(result));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
fail(`unknown command: ${command || "<empty>"}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
558
|
+
try {
|
|
559
|
+
main();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error(error.message);
|
|
562
|
+
process.exitCode = 1;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
@@ -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
|
|
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",
|
|
@@ -114,9 +115,13 @@ export function validateRenditionSet(output, helpers) {
|
|
|
114
115
|
const captureBytes = readRegular(path.join(output, entry.terminalCapture), `${label} terminal capture`, maxBytes);
|
|
115
116
|
const capture = validateTerminalCapture(JSON.parse(decodeUtf8(captureBytes, `${label} terminal capture`)), scene, helpers);
|
|
116
117
|
invariant(entry.captureRoot === sha256(captureBytes), `${label}.captureRoot mismatch`);
|
|
117
|
-
return { ...entry, transcript, lines, scene, projection, capture };
|
|
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",
|