@kungfu-tech/buildchain 3.0.4-alpha.0 → 3.0.4-alpha.10
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/AGENTS.md +14 -5
- package/README.md +18 -0
- package/bin/buildchain.mjs +136 -73
- package/bin/internal/trust-release-cli.mjs +15 -537
- package/bin/internal/trust-release-command-handlers.mjs +14 -0
- package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
- package/bin/internal/trust-release-release-handlers.mjs +317 -0
- package/bin/internal/trust-release-verification-handlers.mjs +306 -0
- package/dist/site/buildchain-contract.json +36 -25
- package/dist/site/buildchain-site.json +1474 -61
- package/dist/site/capability-registry.json +8 -5
- package/dist/site/cli-registry.json +1869 -0
- package/dist/site/controller-registry.json +16 -3
- package/dist/site/kfd-claims.json +84 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +49 -6
- package/dist/site/node-api-registry.json +17802 -11
- package/dist/site/page-registry.json +1441 -59
- package/dist/site/public-surface-audit.json +2863 -358
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +7 -0
- package/dist/site/site-manifest.json +34 -10
- package/dist/site/workflow-registry.json +13 -5
- package/docs/MAP.md +21 -6
- package/docs/cli-reference.md +1936 -0
- package/docs/cli.md +15 -1
- package/docs/getting-started.md +167 -0
- package/docs/install.md +7 -8
- package/docs/node-api-reference.md +1952 -0
- package/docs/release-propagation.md +166 -8
- package/docs/site-bundle-contract.md +10 -4
- package/docs/versioning.md +5 -4
- package/package.json +8 -5
- package/packages/core/buildchain-agent-manuals.js +37 -0
- package/packages/core/buildchain-kfd-claims.js +4 -37
- package/packages/core/controller-evidence.js +8 -1
- package/packages/core/index.js +1 -13
- package/packages/core/paper-agent-entry.js +14 -11
- package/packages/core/paper-fleet.js +34 -7
- package/packages/core/paper-npm-bootstrap.js +492 -0
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper.js +356 -674
- package/packages/core/public-surface-cli.js +12 -1
- package/packages/core/publication-package.js +7 -0
- package/packages/core/release-passport.js +67 -71
- package/packages/core/release-propagation-common.js +64 -0
- package/packages/core/release-propagation-execution-profile.js +59 -0
- package/packages/core/release-propagation-release.js +196 -0
- package/packages/core/release-propagation-stage-evidence.js +364 -0
- package/packages/core/release-propagation-work-capture.js +64 -0
- package/packages/core/release-propagation-work-constants.js +34 -0
- package/packages/core/release-propagation-work-control.js +203 -0
- package/packages/core/release-propagation-work-transitions.js +145 -0
- package/packages/core/release-propagation-work.js +517 -0
- package/packages/core/release-propagation.js +34 -158
- package/scripts/aggregate-build-summary.mjs +67 -4
- package/scripts/auditable-demo-renditions.mjs +131 -0
- package/scripts/auditable-demo.mjs +88 -91
- package/scripts/aws-windows-jit-controller-core.mjs +269 -0
- package/scripts/aws-windows-jit-controller.mjs +502 -0
- package/scripts/check-internal-architecture.mjs +178 -52
- package/scripts/check-inventory.mjs +2 -2
- package/scripts/check-maintainability.mjs +76 -17
- package/scripts/generate-public-reference.mjs +68 -0
- package/scripts/generate-site-bundle.mjs +35 -41
- package/scripts/maintainability-metrics.mjs +24 -4
- package/scripts/paper-work-fleet-cli.mjs +22 -3
- package/scripts/public-reference.mjs +557 -0
- package/scripts/release-propagation.mjs +126 -0
- package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
- package/scripts/site-reference-registry.mjs +174 -0
- package/scripts/verify-golden-path.mjs +169 -0
- package/scripts/web-surface-core.mjs +46 -207
- package/scripts/web-surface-routing.mjs +286 -0
|
@@ -1,37 +1,23 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
assertPlainObject,
|
|
5
|
+
assertString,
|
|
6
|
+
RELEASE_PROPAGATION_PLAN_CONTRACT,
|
|
7
|
+
normalizeChannel,
|
|
8
|
+
optionalString,
|
|
9
|
+
sha256Json,
|
|
10
|
+
stableJson,
|
|
11
|
+
} from "./release-propagation-common.js";
|
|
12
|
+
import { normalizeExecutionProfile } from "./release-propagation-execution-profile.js";
|
|
13
|
+
import { normalizeUpstreamRelease } from "./release-propagation-release.js";
|
|
4
14
|
|
|
5
15
|
export const RELEASE_PROPAGATION_GRAPH_CONTRACT = "kungfu-buildchain-release-propagation-graph";
|
|
6
|
-
export
|
|
16
|
+
export { RELEASE_PROPAGATION_PLAN_CONTRACT };
|
|
7
17
|
export const RELEASE_PROPAGATION_LOCK_CONTRACT = "kungfu-buildchain-release-propagation-lock";
|
|
8
18
|
export const RELEASE_PROPAGATION_RECEIPT_CONTRACT = "kungfu-buildchain-release-propagation-receipt";
|
|
9
|
-
|
|
10
|
-
const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
|
|
11
19
|
const SUPPORTED_CHANNEL_POLICIES = new Set(["preserve", "explicit"]);
|
|
12
20
|
|
|
13
|
-
function stableJson(value) {
|
|
14
|
-
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function sortJson(value) {
|
|
18
|
-
if (Array.isArray(value)) {
|
|
19
|
-
return value.map(sortJson);
|
|
20
|
-
}
|
|
21
|
-
if (!value || typeof value !== "object") {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
return Object.fromEntries(
|
|
25
|
-
Object.entries(value)
|
|
26
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
27
|
-
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sha256Json(value) {
|
|
32
|
-
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
21
|
function releaseVersion(upstreamRelease) {
|
|
36
22
|
return upstreamRelease.package?.version || upstreamRelease.publicationArtifact?.version || "";
|
|
37
23
|
}
|
|
@@ -75,32 +61,6 @@ function propagationBranch({ upstreamRelease, targetNode }) {
|
|
|
75
61
|
return `buildchain/release-propagation/${repository}/${version}-${upstreamRelease.channel}-${digest}`;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
function assertPlainObject(value, label) {
|
|
79
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
80
|
-
throw new Error(`${label} must be an object`);
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function assertString(value, label) {
|
|
86
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
87
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
88
|
-
}
|
|
89
|
-
return value.trim();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function optionalString(value) {
|
|
93
|
-
return value === undefined || value === null ? "" : String(value).trim();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function normalizeChannel(value, label) {
|
|
97
|
-
const channel = assertString(value, label);
|
|
98
|
-
if (!SUPPORTED_CHANNELS.has(channel)) {
|
|
99
|
-
throw new Error(`${label} must be alpha or release`);
|
|
100
|
-
}
|
|
101
|
-
return channel;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
64
|
function normalizeChannelMap(edge, label) {
|
|
105
65
|
const policy = edge.channelPolicy || "preserve";
|
|
106
66
|
if (!SUPPORTED_CHANNEL_POLICIES.has(policy)) {
|
|
@@ -130,6 +90,7 @@ function normalizeNode(node, index) {
|
|
|
130
90
|
lockPath: optionalString(node.lockPath || node.lock_path),
|
|
131
91
|
baseRef: optionalString(node.baseRef || node.base_ref),
|
|
132
92
|
workflow: optionalString(node.workflow),
|
|
93
|
+
executionProfile: normalizeExecutionProfile(node.executionProfile || node.execution_profile, `nodes[${index}].executionProfile`),
|
|
133
94
|
};
|
|
134
95
|
if (!/^[^/\s]+\/[^/\s]+$/.test(normalized.repository)) {
|
|
135
96
|
throw new Error(`nodes[${index}].repository must be owner/repo`);
|
|
@@ -235,112 +196,6 @@ export function resolvePropagationChannel(edge, upstreamChannel) {
|
|
|
235
196
|
return downstreamChannel ? normalizeChannel(downstreamChannel, `edge ${edge.id} downstream channel`) : "";
|
|
236
197
|
}
|
|
237
198
|
|
|
238
|
-
function normalizeReleasePassport(passport = {}) {
|
|
239
|
-
if (!passport || typeof passport !== "object" || Array.isArray(passport)) {
|
|
240
|
-
throw new Error("upstreamRelease.releasePassport must be an object");
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
url: assertString(passport.url, "upstreamRelease.releasePassport.url"),
|
|
244
|
-
sha256: assertString(passport.sha256 || passport.digest, "upstreamRelease.releasePassport.sha256"),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function normalizePackageFact(pkg = {}) {
|
|
249
|
-
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
250
|
-
throw new Error("upstreamRelease.package must be an object");
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
name: assertString(pkg.name, "upstreamRelease.package.name"),
|
|
254
|
-
version: assertString(pkg.version, "upstreamRelease.package.version"),
|
|
255
|
-
integrity: assertString(pkg.integrity, "upstreamRelease.package.integrity"),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function normalizeDigestUrlFact(value = {}, label) {
|
|
260
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
261
|
-
throw new Error(`${label} must be an object`);
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
url: assertString(value.url, `${label}.url`),
|
|
265
|
-
sha256: assertString(value.sha256 || value.digest, `${label}.sha256`),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function normalizeOptionalPathDigestUrlFact(value = undefined, label) {
|
|
270
|
-
if (value === undefined || value === null) {
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
const normalized = normalizeDigestUrlFact(value, label);
|
|
274
|
-
return {
|
|
275
|
-
path: optionalString(value.path),
|
|
276
|
-
bytes: value.bytes === undefined ? undefined : Number(value.bytes),
|
|
277
|
-
...normalized,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function normalizePublicationArtifactFact(value = undefined) {
|
|
282
|
-
if (value === undefined || value === null) {
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
const artifact = assertPlainObject(value, "upstreamRelease.publicationArtifact");
|
|
286
|
-
return {
|
|
287
|
-
id: optionalString(artifact.id),
|
|
288
|
-
kind: optionalString(artifact.kind),
|
|
289
|
-
version: assertString(artifact.version, "upstreamRelease.publicationArtifact.version"),
|
|
290
|
-
canonicalUrl: assertString(
|
|
291
|
-
artifact.canonicalUrl || artifact.canonical_url,
|
|
292
|
-
"upstreamRelease.publicationArtifact.canonicalUrl",
|
|
293
|
-
),
|
|
294
|
-
latestUrl: assertString(
|
|
295
|
-
artifact.latestUrl || artifact.latest_url,
|
|
296
|
-
"upstreamRelease.publicationArtifact.latestUrl",
|
|
297
|
-
),
|
|
298
|
-
latestEvidenceUrl: optionalString(artifact.latestEvidenceUrl || artifact.latest_evidence_url),
|
|
299
|
-
immutableVersionUrl: assertString(
|
|
300
|
-
artifact.immutableVersionUrl || artifact.immutable_version_url || artifact.immutableVersionPrefix || artifact.immutable_version_prefix,
|
|
301
|
-
"upstreamRelease.publicationArtifact.immutableVersionUrl",
|
|
302
|
-
),
|
|
303
|
-
immutableVersionPrefix: optionalString(artifact.immutableVersionPrefix || artifact.immutable_version_prefix),
|
|
304
|
-
registry: normalizeDigestUrlFact(artifact.registry, "upstreamRelease.publicationArtifact.registry"),
|
|
305
|
-
manifest: normalizeDigestUrlFact(artifact.manifest, "upstreamRelease.publicationArtifact.manifest"),
|
|
306
|
-
passport: normalizeDigestUrlFact(artifact.passport, "upstreamRelease.publicationArtifact.passport"),
|
|
307
|
-
primaryArtifact: normalizeOptionalPathDigestUrlFact(
|
|
308
|
-
artifact.primaryArtifact || artifact.primary_artifact,
|
|
309
|
-
"upstreamRelease.publicationArtifact.primaryArtifact",
|
|
310
|
-
),
|
|
311
|
-
sourceBundle: normalizeOptionalPathDigestUrlFact(
|
|
312
|
-
artifact.sourceBundle || artifact.source_bundle,
|
|
313
|
-
"upstreamRelease.publicationArtifact.sourceBundle",
|
|
314
|
-
),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function normalizeUpstreamRelease(input) {
|
|
319
|
-
const release = assertPlainObject(input, "upstreamRelease");
|
|
320
|
-
const publicationArtifact = normalizePublicationArtifactFact(release.publicationArtifact || release.publication_artifact);
|
|
321
|
-
const packageFact = release.package === undefined ? undefined : normalizePackageFact(release.package);
|
|
322
|
-
if (!packageFact && !publicationArtifact) {
|
|
323
|
-
throw new Error("upstreamRelease requires package or publicationArtifact");
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
repository: assertString(release.repository, "upstreamRelease.repository"),
|
|
327
|
-
channel: normalizeChannel(release.channel, "upstreamRelease.channel"),
|
|
328
|
-
tag: assertString(release.tag, "upstreamRelease.tag"),
|
|
329
|
-
sourceSha: assertString(release.sourceSha || release.source_sha, "upstreamRelease.sourceSha"),
|
|
330
|
-
package: packageFact,
|
|
331
|
-
publicationArtifact,
|
|
332
|
-
releasePassport: normalizeReleasePassport(release.releasePassport || release.release_passport),
|
|
333
|
-
siteBundle: release.siteBundle || release.site_bundle
|
|
334
|
-
? {
|
|
335
|
-
manifestSha256: assertString(
|
|
336
|
-
release.siteBundle?.manifestSha256 || release.site_bundle?.manifest_sha256,
|
|
337
|
-
"upstreamRelease.siteBundle.manifestSha256",
|
|
338
|
-
),
|
|
339
|
-
}
|
|
340
|
-
: undefined,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
199
|
export function createReleasePropagationLock({
|
|
345
200
|
graph,
|
|
346
201
|
edge,
|
|
@@ -359,6 +214,7 @@ export function createReleasePropagationLock({
|
|
|
359
214
|
repository: upstreamRelease.repository,
|
|
360
215
|
channel: upstreamRelease.channel,
|
|
361
216
|
tag: upstreamRelease.tag,
|
|
217
|
+
tagTargetSha: upstreamRelease.tagTargetSha,
|
|
362
218
|
sourceSha: upstreamRelease.sourceSha,
|
|
363
219
|
package: upstreamRelease.package,
|
|
364
220
|
publicationArtifact: upstreamRelease.publicationArtifact,
|
|
@@ -371,6 +227,7 @@ export function createReleasePropagationLock({
|
|
|
371
227
|
channel: downstreamChannel,
|
|
372
228
|
baseRef: edge.prBaseRef || targetNode.baseRef,
|
|
373
229
|
lockPath: edge.lockPath || targetNode.lockPath || ".buildchain/upstream-release.lock.json",
|
|
230
|
+
executionProfile: targetNode.executionProfile,
|
|
374
231
|
},
|
|
375
232
|
propagation: {
|
|
376
233
|
graphContract: graph.contract,
|
|
@@ -423,6 +280,7 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
|
|
|
423
280
|
channel: downstreamChannel,
|
|
424
281
|
baseRef: lock.downstream.baseRef,
|
|
425
282
|
lockPath: lock.downstream.lockPath,
|
|
283
|
+
executionProfile: lock.downstream.executionProfile,
|
|
426
284
|
propagationKey: lock.propagation.propagationKey,
|
|
427
285
|
branch: lock.propagation.branch,
|
|
428
286
|
lock,
|
|
@@ -548,6 +406,7 @@ export function createReleasePropagationReceipt({
|
|
|
548
406
|
name: upstreamRelease.package.name,
|
|
549
407
|
version: upstreamRelease.package.version,
|
|
550
408
|
integrity: upstreamRelease.package.integrity,
|
|
409
|
+
gitHead: upstreamRelease.package.gitHead,
|
|
551
410
|
}
|
|
552
411
|
: null,
|
|
553
412
|
},
|
|
@@ -576,6 +435,7 @@ export function createReleasePropagationReceipt({
|
|
|
576
435
|
upstream: {
|
|
577
436
|
repository: upstreamRelease.repository,
|
|
578
437
|
tag: upstreamRelease.tag,
|
|
438
|
+
tagTargetSha: upstreamRelease.tagTargetSha,
|
|
579
439
|
sourceSha: upstreamRelease.sourceSha,
|
|
580
440
|
releasePassport: upstreamRelease.releasePassport,
|
|
581
441
|
},
|
|
@@ -595,3 +455,19 @@ export function createReleasePropagationReceipt({
|
|
|
595
455
|
receiptSha256: sha256Json(body),
|
|
596
456
|
};
|
|
597
457
|
}
|
|
458
|
+
|
|
459
|
+
export {
|
|
460
|
+
RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT,
|
|
461
|
+
RELEASE_PROPAGATION_WORK_CONTRACT,
|
|
462
|
+
RELEASE_PROPAGATION_WORK_STAGES,
|
|
463
|
+
createReleasePropagationStageReceipt,
|
|
464
|
+
createReleasePropagationWork,
|
|
465
|
+
resumeReleasePropagationWork,
|
|
466
|
+
verifyReleasePropagationWork,
|
|
467
|
+
} from "./release-propagation-work.js";
|
|
468
|
+
export {
|
|
469
|
+
claimReleasePropagationWork,
|
|
470
|
+
completeReleasePropagationWork,
|
|
471
|
+
recordReleasePropagationStage,
|
|
472
|
+
repairReleasePropagationWork,
|
|
473
|
+
} from "./release-propagation-work-transitions.js";
|
|
@@ -8,6 +8,51 @@ function readEnv(name, fallback = "") {
|
|
|
8
8
|
return process.env[name] || fallback;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function parsePlatformIds(name) {
|
|
12
|
+
const raw = readEnv(name, "").trim();
|
|
13
|
+
if (!raw) return [];
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!Array.isArray(parsed)) {
|
|
16
|
+
throw new Error(`${name} must be a JSON array`);
|
|
17
|
+
}
|
|
18
|
+
const ids = parsed.map((entry) => String(
|
|
19
|
+
typeof entry === "string" ? entry : entry?.id || "",
|
|
20
|
+
).trim());
|
|
21
|
+
if (ids.some((id) => !id)) {
|
|
22
|
+
throw new Error(`${name} entries must declare non-empty platform ids`);
|
|
23
|
+
}
|
|
24
|
+
if (new Set(ids).size !== ids.length) {
|
|
25
|
+
throw new Error(`${name} must not contain duplicate platform ids`);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readManifest(file) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectPlatformManifests({ inputRoot, expectedPlatformIds }) {
|
|
39
|
+
const candidates = findJsonFiles(inputRoot)
|
|
40
|
+
.filter((file) => path.basename(file) === "manifest.json")
|
|
41
|
+
.sort()
|
|
42
|
+
.map((file) => ({ file, manifest: readManifest(file) }))
|
|
43
|
+
.filter(({ manifest }) => manifest?.contract === "kungfu-buildchain-artifact");
|
|
44
|
+
if (expectedPlatformIds.length === 0) return candidates;
|
|
45
|
+
return expectedPlatformIds.map((platformId) => {
|
|
46
|
+
const matches = candidates.filter(({ manifest }) => manifest.platform?.id === platformId);
|
|
47
|
+
if (matches.length !== 1) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`expected exactly one platform manifest for ${platformId}, found ${matches.length} under ${inputRoot}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return matches[0];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
11
56
|
export function aggregateBuildSummaryCli() {
|
|
12
57
|
const inputRoot = path.resolve(readEnv("BUILDCHAIN_SUMMARY_INPUT", ".buildchain/downloaded-manifests"));
|
|
13
58
|
const outputPath = path.resolve(readEnv("BUILDCHAIN_SUMMARY_OUTPUT", ".buildchain/artifacts/build-summary.json"));
|
|
@@ -18,10 +63,28 @@ export function aggregateBuildSummaryCli() {
|
|
|
18
63
|
throw new Error("BUILDCHAIN_ADDITIONAL_PLATFORM_COUNT must be a non-negative integer");
|
|
19
64
|
}
|
|
20
65
|
const expectedManifestCount = expectedPlatformCount + additionalPlatformCount;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
66
|
+
const platformIds = parsePlatformIds("BUILDCHAIN_EXPECTED_PLATFORMS_JSON");
|
|
67
|
+
const additionalPlatformIds = parsePlatformIds("BUILDCHAIN_ADDITIONAL_PLATFORM_IDS_JSON");
|
|
68
|
+
if (platformIds.length > 0 && platformIds.length !== expectedPlatformCount) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`BUILDCHAIN_EXPECTED_PLATFORMS_JSON declares ${platformIds.length} platforms, expected ${expectedPlatformCount}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (additionalPlatformIds.length !== additionalPlatformCount) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`BUILDCHAIN_ADDITIONAL_PLATFORM_IDS_JSON declares ${additionalPlatformIds.length} platforms, expected ${additionalPlatformCount}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const expectedPlatformIds = [...platformIds, ...additionalPlatformIds];
|
|
79
|
+
if (new Set(expectedPlatformIds).size !== expectedPlatformIds.length) {
|
|
80
|
+
throw new Error("declared platform ids must be unique across primary and additional platforms");
|
|
81
|
+
}
|
|
82
|
+
const selected = selectPlatformManifests({
|
|
83
|
+
inputRoot,
|
|
84
|
+
expectedPlatformIds,
|
|
85
|
+
});
|
|
86
|
+
const manifestFiles = selected.map(({ file }) => file);
|
|
87
|
+
const manifests = selected.map(({ manifest }) => manifest);
|
|
25
88
|
if (expectedManifestCount > 0 && manifests.length !== expectedManifestCount) {
|
|
26
89
|
throw new Error(
|
|
27
90
|
`expected ${expectedManifestCount} platform manifests, found ${manifests.length} under ${inputRoot}`,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TERMINAL_CAPTURE_NON_AUTHORITIES = [
|
|
5
|
+
"first-party-identity",
|
|
6
|
+
"system-identity",
|
|
7
|
+
"kfd-compliance",
|
|
8
|
+
"product-system-metadata",
|
|
9
|
+
"package-metadata",
|
|
10
|
+
"registry-history",
|
|
11
|
+
"scan-output",
|
|
12
|
+
"standalone-generation",
|
|
13
|
+
];
|
|
14
|
+
const RENDITION_SET_NON_AUTHORITIES = [
|
|
15
|
+
"publication-authority",
|
|
16
|
+
"runtime-authority",
|
|
17
|
+
...TERMINAL_CAPTURE_NON_AUTHORITIES,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function validateTerminalCapture(value, scene, helpers) {
|
|
21
|
+
const { decodeBase64, digestPattern, exactKeys, integer, invariant, maxBytes, maxEvents, text } = helpers;
|
|
22
|
+
exactKeys(
|
|
23
|
+
value,
|
|
24
|
+
["schema", "command", "dimensions", "durationMs", "encoding", "events", "completion", "exitCode", "authority"],
|
|
25
|
+
[],
|
|
26
|
+
"terminalCapture",
|
|
27
|
+
);
|
|
28
|
+
invariant(value.schema === "kungfu.terminal-capture/v1", "unsupported terminal capture schema");
|
|
29
|
+
text(value.command, 1, 160, "terminalCapture.command");
|
|
30
|
+
exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
|
|
31
|
+
integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
|
|
32
|
+
integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
|
|
33
|
+
const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
|
|
34
|
+
invariant(
|
|
35
|
+
durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
|
|
36
|
+
"terminal capture duration must end within two seconds of the scene",
|
|
37
|
+
);
|
|
38
|
+
invariant(value.encoding === "base64", "terminalCapture.encoding must be base64");
|
|
39
|
+
invariant(
|
|
40
|
+
Array.isArray(value.events) && value.events.length > 0 && value.events.length <= maxEvents,
|
|
41
|
+
`terminalCapture.events must contain 1 through ${maxEvents} events`,
|
|
42
|
+
);
|
|
43
|
+
let previousAtMs = -1;
|
|
44
|
+
let totalBytes = 0;
|
|
45
|
+
for (const [index, event] of value.events.entries()) {
|
|
46
|
+
exactKeys(event, ["atMs", "data"], [], `terminalCapture.events[${index}]`);
|
|
47
|
+
const atMs = integer(event.atMs, 0, durationMs - 1, `terminalCapture.events[${index}].atMs`);
|
|
48
|
+
invariant(atMs >= previousAtMs, "terminal capture event timestamps must be monotonic");
|
|
49
|
+
invariant(index > 0 || atMs === 0, "the first terminal capture event must start at zero");
|
|
50
|
+
previousAtMs = atMs;
|
|
51
|
+
totalBytes += decodeBase64(event.data, `terminalCapture.events[${index}].data`).length;
|
|
52
|
+
invariant(totalBytes <= maxBytes, "terminal capture exceeds the 4 MiB byte bound");
|
|
53
|
+
}
|
|
54
|
+
exactKeys(value.completion, ["schema", "status", "reportRoot", "eventCount"], [], "terminalCapture.completion");
|
|
55
|
+
invariant(
|
|
56
|
+
value.completion.schema === "kungfu.agent-work-lab.tui-autoplay/v1"
|
|
57
|
+
&& value.completion.status === "qualified"
|
|
58
|
+
&& digestPattern.test(value.completion.reportRoot),
|
|
59
|
+
"terminal capture completion sentinel is not a qualified Agent Work Lab autoplay",
|
|
60
|
+
);
|
|
61
|
+
integer(value.completion.eventCount, 1, 100_000, "terminalCapture.completion.eventCount");
|
|
62
|
+
invariant(value.exitCode === 0, "terminal capture exitCode must be zero");
|
|
63
|
+
exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "terminalCapture.authority");
|
|
64
|
+
invariant(value.authority.classification === "volatile-terminal-observation", "terminal capture authority classification must remain observation-only");
|
|
65
|
+
invariant(Array.isArray(value.authority.grants) && value.authority.grants.length === 0, "terminal capture must not grant authority");
|
|
66
|
+
invariant(
|
|
67
|
+
JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(TERMINAL_CAPTURE_NON_AUTHORITIES),
|
|
68
|
+
"terminal capture must declare every identity and metadata non-authority",
|
|
69
|
+
);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateRenditionSet(output, helpers) {
|
|
74
|
+
const { decodeUtf8, exactKeys, invariant, maxBytes, readJson, readRegular, sha256, validateProjection, validateScene } = helpers;
|
|
75
|
+
const manifestPath = path.join(output, "rendition-set.json");
|
|
76
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
77
|
+
const value = readJson(manifestPath, "rendition set");
|
|
78
|
+
exactKeys(value, ["schema", "renditions", "authority"], [], "renditionSet");
|
|
79
|
+
invariant(value.schema === "kungfu.auditable-demo.rendition-set/v1", "unsupported rendition set schema");
|
|
80
|
+
exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "renditionSet.authority");
|
|
81
|
+
invariant(
|
|
82
|
+
value.authority.classification === "capture-routing-metadata"
|
|
83
|
+
&& Array.isArray(value.authority.grants)
|
|
84
|
+
&& value.authority.grants.length === 0
|
|
85
|
+
&& JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(RENDITION_SET_NON_AUTHORITIES),
|
|
86
|
+
"rendition set authority boundary is invalid",
|
|
87
|
+
);
|
|
88
|
+
invariant(Array.isArray(value.renditions) && value.renditions.length === 2, "rendition set must contain exactly two captures");
|
|
89
|
+
const expected = [
|
|
90
|
+
{
|
|
91
|
+
id: "1080p", role: "primary", transcript: "complete-transcript.txt",
|
|
92
|
+
projection: "public-projection.json", scene: "scene.json", terminalCapture: "terminal-capture.json",
|
|
93
|
+
width: 1920, height: 1080,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "720p", role: "responsive", transcript: "complete-transcript-720p.txt",
|
|
97
|
+
projection: "public-projection-720p.json", scene: "scene-720p.json", terminalCapture: "terminal-capture-720p.json",
|
|
98
|
+
width: 1280, height: 720,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
const normalized = value.renditions.map((entry, index) => {
|
|
102
|
+
const label = `renditionSet.renditions[${index}]`;
|
|
103
|
+
exactKeys(entry, ["id", "role", "transcript", "projection", "scene", "terminalCapture", "captureRoot"], [], label);
|
|
104
|
+
const declaration = expected[index];
|
|
105
|
+
for (const key of ["id", "role", "transcript", "projection", "scene", "terminalCapture"]) {
|
|
106
|
+
invariant(entry[key] === declaration[key], `${label}.${key} is not the exact native rendition contract`);
|
|
107
|
+
}
|
|
108
|
+
const transcriptBytes = readRegular(path.join(output, entry.transcript), `${label} transcript`, 4 * 1024 * 1024);
|
|
109
|
+
const transcript = decodeUtf8(transcriptBytes, `${label} transcript`).replace(/\r\n/g, "\n");
|
|
110
|
+
invariant(transcript.trim().length > 0, `${label} transcript must not be empty`);
|
|
111
|
+
const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
|
|
112
|
+
const scene = validateScene(readJson(path.join(output, entry.scene), `${label} scene`));
|
|
113
|
+
invariant(scene.width === declaration.width && scene.height === declaration.height, `${label} scene dimensions are not native`);
|
|
114
|
+
const projection = validateProjection(readJson(path.join(output, entry.projection), `${label} projection`), scene, lines.length);
|
|
115
|
+
const captureBytes = readRegular(path.join(output, entry.terminalCapture), `${label} terminal capture`, maxBytes);
|
|
116
|
+
const capture = validateTerminalCapture(JSON.parse(decodeUtf8(captureBytes, `${label} terminal capture`)), scene, helpers);
|
|
117
|
+
invariant(entry.captureRoot === sha256(captureBytes), `${label}.captureRoot mismatch`);
|
|
118
|
+
return { ...entry, transcript, lines, scene, projection, capture };
|
|
119
|
+
});
|
|
120
|
+
invariant(normalized[0].captureRoot !== normalized[1].captureRoot, "native rendition capture roots must be distinct");
|
|
121
|
+
invariant(
|
|
122
|
+
JSON.stringify(normalized[0].capture.dimensions) !== JSON.stringify(normalized[1].capture.dimensions),
|
|
123
|
+
"native rendition PTY dimensions must be distinct",
|
|
124
|
+
);
|
|
125
|
+
invariant(
|
|
126
|
+
normalized[0].projection.evidenceClass === normalized[1].projection.evidenceClass
|
|
127
|
+
&& normalized[0].projection.claimBoundary === normalized[1].projection.claimBoundary,
|
|
128
|
+
"native rendition evidence boundaries must match",
|
|
129
|
+
);
|
|
130
|
+
return { schema: value.schema, renditions: normalized, authority: value.authority };
|
|
131
|
+
}
|