@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.
Files changed (74) hide show
  1. package/AGENTS.md +14 -5
  2. package/README.md +18 -0
  3. package/bin/buildchain.mjs +136 -73
  4. package/bin/internal/trust-release-cli.mjs +15 -537
  5. package/bin/internal/trust-release-command-handlers.mjs +14 -0
  6. package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
  7. package/bin/internal/trust-release-release-handlers.mjs +317 -0
  8. package/bin/internal/trust-release-verification-handlers.mjs +306 -0
  9. package/dist/site/buildchain-contract.json +36 -25
  10. package/dist/site/buildchain-site.json +1474 -61
  11. package/dist/site/capability-registry.json +8 -5
  12. package/dist/site/cli-registry.json +1869 -0
  13. package/dist/site/controller-registry.json +16 -3
  14. package/dist/site/kfd-claims.json +84 -10
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +49 -6
  17. package/dist/site/node-api-registry.json +17802 -11
  18. package/dist/site/page-registry.json +1441 -59
  19. package/dist/site/public-surface-audit.json +2863 -358
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/release-model.json +7 -0
  22. package/dist/site/site-manifest.json +34 -10
  23. package/dist/site/workflow-registry.json +13 -5
  24. package/docs/MAP.md +21 -6
  25. package/docs/cli-reference.md +1936 -0
  26. package/docs/cli.md +15 -1
  27. package/docs/getting-started.md +167 -0
  28. package/docs/install.md +7 -8
  29. package/docs/node-api-reference.md +1952 -0
  30. package/docs/release-propagation.md +166 -8
  31. package/docs/site-bundle-contract.md +10 -4
  32. package/docs/versioning.md +5 -4
  33. package/package.json +8 -5
  34. package/packages/core/buildchain-agent-manuals.js +37 -0
  35. package/packages/core/buildchain-kfd-claims.js +4 -37
  36. package/packages/core/controller-evidence.js +8 -1
  37. package/packages/core/index.js +1 -13
  38. package/packages/core/paper-agent-entry.js +14 -11
  39. package/packages/core/paper-fleet.js +34 -7
  40. package/packages/core/paper-npm-bootstrap.js +492 -0
  41. package/packages/core/paper-repository.js +1 -0
  42. package/packages/core/paper.js +356 -674
  43. package/packages/core/public-surface-cli.js +12 -1
  44. package/packages/core/publication-package.js +7 -0
  45. package/packages/core/release-passport.js +67 -71
  46. package/packages/core/release-propagation-common.js +64 -0
  47. package/packages/core/release-propagation-execution-profile.js +59 -0
  48. package/packages/core/release-propagation-release.js +196 -0
  49. package/packages/core/release-propagation-stage-evidence.js +364 -0
  50. package/packages/core/release-propagation-work-capture.js +64 -0
  51. package/packages/core/release-propagation-work-constants.js +34 -0
  52. package/packages/core/release-propagation-work-control.js +203 -0
  53. package/packages/core/release-propagation-work-transitions.js +145 -0
  54. package/packages/core/release-propagation-work.js +517 -0
  55. package/packages/core/release-propagation.js +34 -158
  56. package/scripts/aggregate-build-summary.mjs +67 -4
  57. package/scripts/auditable-demo-renditions.mjs +131 -0
  58. package/scripts/auditable-demo.mjs +88 -91
  59. package/scripts/aws-windows-jit-controller-core.mjs +269 -0
  60. package/scripts/aws-windows-jit-controller.mjs +502 -0
  61. package/scripts/check-internal-architecture.mjs +178 -52
  62. package/scripts/check-inventory.mjs +2 -2
  63. package/scripts/check-maintainability.mjs +76 -17
  64. package/scripts/generate-public-reference.mjs +68 -0
  65. package/scripts/generate-site-bundle.mjs +35 -41
  66. package/scripts/maintainability-metrics.mjs +24 -4
  67. package/scripts/paper-work-fleet-cli.mjs +22 -3
  68. package/scripts/public-reference.mjs +557 -0
  69. package/scripts/release-propagation.mjs +126 -0
  70. package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
  71. package/scripts/site-reference-registry.mjs +174 -0
  72. package/scripts/verify-golden-path.mjs +169 -0
  73. package/scripts/web-surface-core.mjs +46 -207
  74. 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 const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
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 manifestFiles = findJsonFiles(inputRoot)
22
- .filter((file) => path.basename(file) === "manifest.json")
23
- .sort();
24
- const manifests = manifestFiles.map((file) => JSON.parse(fs.readFileSync(file, "utf8")));
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
+ }