@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-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 (83) hide show
  1. package/README.md +4 -2
  2. package/actions/github-artifact-attestation/README.md +10 -0
  3. package/actions/promote-buildchain-ref/README.md +7 -0
  4. package/bin/buildchain.mjs +5 -0
  5. package/bin/internal/trust-release-cli.mjs +74 -3
  6. package/contracts/auditable-demo-media-profiles-v1.json +168 -0
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
  8. package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
  9. package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
  10. package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
  11. package/dist/site/artifact-schemas.json +5 -1
  12. package/dist/site/buildchain-contract.json +133 -34
  13. package/dist/site/buildchain-site.json +159 -40
  14. package/dist/site/capability-registry.json +14 -13
  15. package/dist/site/cli-registry.json +12 -0
  16. package/dist/site/controller-registry.json +72 -4
  17. package/dist/site/kfd-claims.json +338 -20
  18. package/dist/site/kfd-upstream-aggregate.json +1 -1
  19. package/dist/site/manual-registry.json +24 -9
  20. package/dist/site/node-api-registry.json +89 -11
  21. package/dist/site/page-registry.json +135 -25
  22. package/dist/site/public-surface-audit.json +203 -21
  23. package/dist/site/publication-authority-registry.json +72 -2
  24. package/dist/site/publication-registry.json +4 -4
  25. package/dist/site/release-model.json +2 -1
  26. package/dist/site/release-passport-check-manifest.json +1 -0
  27. package/dist/site/release-provenance.json +6 -0
  28. package/dist/site/schemas/release-passport-v1.schema.json +6 -0
  29. package/dist/site/site-manifest.json +21 -13
  30. package/dist/site/workflow-registry.json +167 -13
  31. package/docs/MAP.md +5 -1
  32. package/docs/auditable-demo.md +55 -3
  33. package/docs/binary-distribution.md +7 -0
  34. package/docs/cli.md +9 -0
  35. package/docs/dev-alpha-candidate-patrol.md +111 -0
  36. package/docs/github-artifact-attestation.md +219 -0
  37. package/docs/release-governance.md +32 -0
  38. package/docs/release-passport.md +13 -0
  39. package/docs/reusable-build-surface.md +83 -61
  40. package/docs/runtime-train-validation.md +21 -0
  41. package/docs/versioning.md +1 -0
  42. package/package.json +8 -1
  43. package/packages/core/artifact-signing-result.js +228 -0
  44. package/packages/core/artifact-signing.js +412 -0
  45. package/packages/core/buildchain-config.js +58 -0
  46. package/packages/core/buildchain-contract.js +14 -0
  47. package/packages/core/buildchain-kfd-claims.js +5 -0
  48. package/packages/core/buildchain-publication-authority.js +3 -0
  49. package/packages/core/cache-evidence.js +288 -0
  50. package/packages/core/channel-candidate.js +186 -0
  51. package/packages/core/detached-artifact-signature.js +121 -0
  52. package/packages/core/diagnostics.js +276 -10
  53. package/packages/core/github-artifact-attestation.js +642 -0
  54. package/packages/core/github-governance-authority.js +77 -16
  55. package/packages/core/index.js +62 -0
  56. package/packages/core/publication-authority.js +1 -1
  57. package/packages/core/release-passport-contract.js +2 -0
  58. package/packages/core/release-passport.js +60 -3
  59. package/scripts/auditable-demo.mjs +520 -28
  60. package/scripts/build-contract-core.mjs +31 -0
  61. package/scripts/buildchain-channel-router.mjs +8 -2
  62. package/scripts/check-inventory.mjs +9 -0
  63. package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
  64. package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
  65. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  66. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  67. package/scripts/generate-channel-promotion-workflow.mjs +6 -0
  68. package/scripts/generate-site-bundle.mjs +20 -0
  69. package/scripts/import-artifact-signing-results.mjs +76 -0
  70. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  71. package/scripts/locked-source-checkout.mjs +48 -0
  72. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  73. package/scripts/merge-artifact-signing-results.mjs +76 -0
  74. package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
  75. package/scripts/reconcile-github-governance.mjs +158 -25
  76. package/scripts/release-candidate-resolver.mjs +12 -0
  77. package/scripts/release-line-policy.mjs +27 -0
  78. package/scripts/runtime-ref-core.mjs +10 -6
  79. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  80. package/scripts/shifu-gate-profile.mjs +26 -20
  81. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  82. package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
  83. package/scripts/verify-artifact-signing-results.mjs +99 -0
@@ -11,6 +11,12 @@ import { fileURLToPath } from "node:url";
11
11
  const UTF8 = new TextDecoder("utf-8", { fatal: true });
12
12
  const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
13
13
  const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
14
+ const MAX_BUNDLE_MEMBER_BYTES = 8 * 1024 * 1024;
15
+ const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
16
+ const MEDIA_PROFILE_CATALOG = path.resolve(
17
+ SCRIPT_DIRECTORY,
18
+ "../contracts/auditable-demo-media-profiles-v1.json",
19
+ );
14
20
  const REQUIRED_ADAPTER_FILES = [
15
21
  "complete-transcript.txt",
16
22
  "public-projection.json",
@@ -119,7 +125,10 @@ function verifyChecksums(root, checksumName = "checksums.sha256") {
119
125
  const target = resolveInside(root, member, "checksum member");
120
126
  invariant(!declared.has(member), `duplicate checksum member: ${member}`);
121
127
  declared.add(member);
122
- invariant(sha256(readRegular(target, member)).slice(7) === match[1], `checksum mismatch: ${member}`);
128
+ invariant(
129
+ sha256(readRegular(target, member, MAX_BUNDLE_MEMBER_BYTES)).slice(7) === match[1],
130
+ `checksum mismatch: ${member}`,
131
+ );
123
132
  }
124
133
  const actual = listFiles(root).filter((name) => name !== checksumName);
125
134
  invariant(
@@ -164,7 +173,18 @@ function validateScene(value) {
164
173
  for (const key of ["background", "accent"]) {
165
174
  if (value[key] !== undefined) invariant(/^#[0-9a-fA-F]{6}$/.test(value[key]), `scene.${key} is invalid`);
166
175
  }
167
- return value;
176
+ return {
177
+ schema: value.schema,
178
+ id: value.id,
179
+ width: value.width,
180
+ height: value.height,
181
+ fps: value.fps,
182
+ durationMs: value.durationMs,
183
+ title: value.title,
184
+ commandLabel: value.commandLabel ?? "",
185
+ background: (value.background ?? "#10151f").toLowerCase(),
186
+ accent: (value.accent ?? "#67e8a5").toLowerCase(),
187
+ };
168
188
  }
169
189
 
170
190
  function validateProjection(value, scene, transcriptLineCount) {
@@ -190,7 +210,17 @@ function validateProjection(value, scene, transcriptLineCount) {
190
210
  }
191
211
  if (cue.annotation !== undefined) text(cue.annotation, 0, 200, `projection.cues[${index}].annotation`);
192
212
  }
193
- return value;
213
+ return {
214
+ schema: value.schema,
215
+ evidenceClass: value.evidenceClass,
216
+ claimBoundary: value.claimBoundary,
217
+ cues: value.cues.map((cue) => ({
218
+ startMs: cue.startMs,
219
+ endMs: cue.endMs,
220
+ transcriptLines: cue.transcriptLines,
221
+ annotation: cue.annotation ?? "",
222
+ })),
223
+ };
194
224
  }
195
225
 
196
226
  function validateSourceCoordinate(value) {
@@ -371,40 +401,465 @@ function prepareSmoke(values) {
371
401
  writeJson(path.join(output, "public-projection.json"), projection);
372
402
  }
373
403
 
374
- function verifyRendererOutput(renderOutput, expectedImage, expectedInputs) {
404
+ function semanticRoot(value) {
405
+ return sha256(Buffer.from(stableJson(value)));
406
+ }
407
+
408
+ function validateBudgetBasis(entry, label) {
409
+ exactKeys(
410
+ entry.budgetBasis,
411
+ ["evidence", "observedPath", "observedBytes", "multiplier", "rounding"],
412
+ [],
413
+ `${label}.budgetBasis`,
414
+ );
415
+ invariant(
416
+ entry.budgetBasis.evidence === "contracts/evidence/auditable-demo-web-delivery-v1.json",
417
+ `${label}.budgetBasis.evidence is unsupported`,
418
+ );
419
+ text(entry.budgetBasis.observedPath, 1, 128, `${label}.budgetBasis.observedPath`);
420
+ const observedBytes = integer(
421
+ entry.budgetBasis.observedBytes,
422
+ 1,
423
+ MAX_BUNDLE_MEMBER_BYTES,
424
+ `${label}.budgetBasis.observedBytes`,
425
+ );
426
+ const multiplier = integer(entry.budgetBasis.multiplier, 1, 64, `${label}.budgetBasis.multiplier`);
427
+ invariant(entry.budgetBasis.rounding === "next-power-of-two", `${label}.budgetBasis.rounding is unsupported`);
428
+ const expected = 2 ** Math.ceil(Math.log2(observedBytes * multiplier));
429
+ invariant(entry.maximumBytes === expected, `${label}.maximumBytes does not match its measured budget basis`);
430
+ }
431
+
432
+ function loadMediaProfile(profileId) {
433
+ const catalog = readJson(MEDIA_PROFILE_CATALOG, "auditable demo media profile catalog");
434
+ invariant(
435
+ catalog.schema === "buildchain.auditable-demo-media-profiles/v1",
436
+ "unsupported media profile catalog",
437
+ );
438
+ invariant(catalog.profiles && typeof catalog.profiles === "object", "media profile catalog is invalid");
439
+ const seen = new Set();
440
+ const resolve = (identifier) => {
441
+ invariant(!seen.has(identifier), `media profile inheritance cycle: ${identifier}`);
442
+ const declared = catalog.profiles[identifier];
443
+ invariant(declared && typeof declared === "object", `unsupported media profile: ${identifier}`);
444
+ seen.add(identifier);
445
+ const inherited = declared.extends ? resolve(declared.extends) : {
446
+ mode: "archive",
447
+ renditions: [],
448
+ singletonRoles: [],
449
+ additionalRenditions: null,
450
+ };
451
+ seen.delete(identifier);
452
+ const byPath = new Map(inherited.renditions.map((entry) => [entry.path, entry]));
453
+ for (const entry of declared.renditions || []) {
454
+ invariant(entry && typeof entry === "object" && typeof entry.path === "string", `${identifier} rendition is invalid`);
455
+ byPath.set(entry.path, { ...(byPath.get(entry.path) || {}), ...entry });
456
+ }
457
+ return {
458
+ mode: declared.mode || inherited.mode,
459
+ renditions: [...byPath.values()],
460
+ singletonRoles: [...new Set([...(inherited.singletonRoles || []), ...(declared.singletonRoles || [])])],
461
+ additionalRenditions: declared.additionalRenditions || inherited.additionalRenditions || null,
462
+ };
463
+ };
464
+ const resolved = resolve(profileId);
465
+ const profile = { id: profileId, ...resolved };
466
+ for (const [index, entry] of profile.renditions.entries()) {
467
+ if (profile.mode === "web-delivery") {
468
+ invariant(entry.budgetBasis, `${profileId}.renditions[${index}].budgetBasis is required`);
469
+ validateBudgetBasis(entry, `${profileId}.renditions[${index}]`);
470
+ }
471
+ }
472
+ return {
473
+ catalog,
474
+ catalogRoot: semanticRoot(catalog),
475
+ profile,
476
+ profileRoot: semanticRoot(profile),
477
+ };
478
+ }
479
+
480
+ function inspectIsoBmffFastStart(filePath) {
481
+ const bytes = readRegular(filePath, "MP4 rendition", MAX_BUNDLE_MEMBER_BYTES);
482
+ let offset = 0;
483
+ let moovOffset = -1;
484
+ let mdatOffset = -1;
485
+ let ftypSeen = false;
486
+ while (offset + 8 <= bytes.length) {
487
+ let size = bytes.readUInt32BE(offset);
488
+ const type = bytes.toString("ascii", offset + 4, offset + 8);
489
+ let headerSize = 8;
490
+ if (size === 1) {
491
+ invariant(offset + 16 <= bytes.length, "MP4 extended box header is truncated");
492
+ const extended = bytes.readBigUInt64BE(offset + 8);
493
+ invariant(extended <= BigInt(Number.MAX_SAFE_INTEGER), "MP4 box size is too large");
494
+ size = Number(extended);
495
+ headerSize = 16;
496
+ } else if (size === 0) {
497
+ size = bytes.length - offset;
498
+ }
499
+ invariant(size >= headerSize && offset + size <= bytes.length, "MP4 box layout is invalid");
500
+ if (type === "ftyp") ftypSeen = true;
501
+ if (type === "moov" && moovOffset === -1) moovOffset = offset;
502
+ if (type === "mdat" && mdatOffset === -1) mdatOffset = offset;
503
+ offset += size;
504
+ }
505
+ invariant(offset === bytes.length && ftypSeen, "MP4 top-level boxes are incomplete");
506
+ if (moovOffset === -1 || mdatOffset === -1) return "missing-moov-or-mdat";
507
+ return moovOffset < mdatOffset ? "moov-before-mdat" : "mdat-before-moov";
508
+ }
509
+
510
+ function rationalNumber(value) {
511
+ const match = /^([0-9]+)\/([0-9]+)$/.exec(String(value || ""));
512
+ if (!match || Number(match[2]) === 0) return 0;
513
+ return Number(match[1]) / Number(match[2]);
514
+ }
515
+
516
+ function inspectMediaFile(filePath) {
517
+ const result = spawnSync(
518
+ "ffprobe",
519
+ [
520
+ "-v", "error",
521
+ "-show_entries", "format=format_name,duration:stream=codec_type,codec_name,pix_fmt,width,height,avg_frame_rate",
522
+ "-of", "json",
523
+ filePath,
524
+ ],
525
+ { encoding: "utf8", maxBuffer: 1024 * 1024 },
526
+ );
527
+ invariant(!result.error && result.status === 0, `ffprobe failed for ${path.basename(filePath)}`);
528
+ let parsed;
529
+ try {
530
+ parsed = JSON.parse(result.stdout);
531
+ } catch {
532
+ throw new Error(`ffprobe returned invalid JSON for ${path.basename(filePath)}`);
533
+ }
534
+ const streams = Array.isArray(parsed.streams) ? parsed.streams : [];
535
+ const videos = streams.filter((entry) => entry.codec_type === "video");
536
+ const audioStreams = streams.filter((entry) => entry.codec_type === "audio").length;
537
+ invariant(videos.length === 1, `${path.basename(filePath)} must contain exactly one video or image stream`);
538
+ const video = videos[0];
539
+ const formatNames = String(parsed.format?.format_name || "").split(",");
540
+ let container = formatNames.includes("mp4") ? "mp4" : "";
541
+ if (formatNames.includes("webm")) container = "webm";
542
+ if (formatNames.includes("gif") || video.codec_name === "gif") container = "gif";
543
+ if (video.codec_name === "png") container = "png";
544
+ if (video.codec_name === "webp") container = "webp";
545
+ if (video.codec_name === "av1" && formatNames.includes("avif")) container = "avif";
546
+ const duration = Number(parsed.format?.duration || 0);
547
+ return {
548
+ container,
549
+ videoCodec: String(video.codec_name || ""),
550
+ pixelFormat: String(video.pix_fmt || ""),
551
+ width: Number(video.width || 0),
552
+ height: Number(video.height || 0),
553
+ durationMs: Number.isFinite(duration) ? Math.round(duration * 1000) : 0,
554
+ frameRate: rationalNumber(video.avg_frame_rate),
555
+ audioStreams,
556
+ progressiveDownload: container === "mp4" ? inspectIsoBmffFastStart(filePath) : "not-applicable",
557
+ };
558
+ }
559
+
560
+ function inspectRendererMedia(values) {
561
+ const renderOutput = path.resolve(required(values, "--render-output"));
562
+ const output = path.resolve(required(values, "--output"));
563
+ const rendererImage = required(values, "--renderer-image");
564
+ invariant(IMAGE_PATTERN.test(rendererImage), "renderer image must use an immutable sha256 coordinate");
565
+ invariant(!fs.existsSync(output), "media inspection output must not already exist");
566
+ const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
567
+ invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
568
+ invariant(manifest.renderer?.image === rendererImage, "renderer manifest image coordinate mismatch");
569
+ const members = Object.keys(manifest.outputs || {})
570
+ .filter((name) => name !== "media-probe.json")
571
+ .sort()
572
+ .map((name) => {
573
+ const target = resolveInside(renderOutput, name, "media inspection member");
574
+ const bytes = readRegular(target, name, MAX_BUNDLE_MEMBER_BYTES);
575
+ return {
576
+ path: name,
577
+ root: sha256(bytes),
578
+ bytes: bytes.length,
579
+ facts: inspectMediaFile(target),
580
+ };
581
+ });
582
+ const body = {
583
+ schema: "buildchain.auditable-demo-media-inspection/v1",
584
+ rendererImage,
585
+ members,
586
+ };
587
+ writeJson(output, { ...body, inspectionRoot: semanticRoot(body) });
588
+ }
589
+
590
+ function qualifyMediaFixture(values) {
591
+ const renderOutput = path.resolve(required(values, "--render-output"));
592
+ const output = path.resolve(required(values, "--output"));
593
+ const rendererImage = required(values, "--renderer-image");
594
+ const rendererSourceRepository = required(values, "--renderer-source-repository");
595
+ const rendererSourceRef = required(values, "--renderer-source-ref");
596
+ const rendererSourceSha = required(values, "--renderer-source-sha");
597
+ const mediaProfile = required(values, "--media-profile");
598
+ invariant(IMAGE_PATTERN.test(rendererImage), "renderer image must use an immutable sha256 coordinate");
599
+ invariant(/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/.test(rendererSourceRepository), "renderer source repository is invalid");
600
+ invariant(/^refs\/tags\/[a-zA-Z0-9._-]+$/.test(rendererSourceRef), "renderer source ref must be an exact tag ref");
601
+ invariant(/^[0-9a-f]{40}$/.test(rendererSourceSha), "renderer source SHA must be exact");
602
+ invariant(!fs.existsSync(output), "media fixture evidence output must not already exist");
603
+ const mediaInspection = loadMediaInspection(
604
+ path.resolve(required(values, "--media-inspection")),
605
+ renderOutput,
606
+ rendererImage,
607
+ );
608
+ const verified = verifyRendererOutput(renderOutput, rendererImage, {
609
+ scene: path.join(renderOutput, "scene.json"),
610
+ transcript: path.join(renderOutput, "complete-transcript.txt"),
611
+ projection: path.join(renderOutput, "public-projection.json"),
612
+ }, {
613
+ mediaProfile,
614
+ inspectMedia: mediaInspection.inspectMedia,
615
+ inspectionRoot: mediaInspection.inspectionRoot,
616
+ });
617
+ const body = {
618
+ schema: "buildchain.auditable-demo-media-profile-fixture/v1",
619
+ renderer: {
620
+ image: rendererImage,
621
+ sourceRepository: rendererSourceRepository,
622
+ sourceRef: rendererSourceRef,
623
+ sourceSha: rendererSourceSha,
624
+ },
625
+ inputs: Object.fromEntries(
626
+ ["complete-transcript.txt", "public-projection.json", "scene.json"].map((name) => [
627
+ name,
628
+ sha256(readRegular(path.join(renderOutput, name), `fixture ${name}`)),
629
+ ]),
630
+ ),
631
+ rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
632
+ qualification: verified.qualification,
633
+ };
634
+ writeJson(output, { ...body, evidenceRoot: semanticRoot(body) });
635
+ }
636
+
637
+ function loadMediaInspection(filePath, renderOutput, rendererImage) {
638
+ const value = readJson(filePath, "media inspection");
639
+ exactKeys(value, ["schema", "rendererImage", "members", "inspectionRoot"], [], "mediaInspection");
640
+ invariant(value.schema === "buildchain.auditable-demo-media-inspection/v1", "unsupported media inspection schema");
641
+ invariant(value.rendererImage === rendererImage, "media inspection renderer image mismatch");
642
+ invariant(Array.isArray(value.members), "mediaInspection.members must be an array");
643
+ const body = {
644
+ schema: value.schema,
645
+ rendererImage: value.rendererImage,
646
+ members: value.members,
647
+ };
648
+ invariant(value.inspectionRoot === semanticRoot(body), "media inspection root mismatch");
649
+ const byPath = new Map();
650
+ for (const [index, entry] of value.members.entries()) {
651
+ exactKeys(entry, ["path", "root", "bytes", "facts"], [], `mediaInspection.members[${index}]`);
652
+ text(entry.path, 1, 256, `mediaInspection.members[${index}].path`);
653
+ invariant(DIGEST_PATTERN.test(entry.root), `mediaInspection.members[${index}].root is invalid`);
654
+ integer(entry.bytes, 1, MAX_BUNDLE_MEMBER_BYTES, `mediaInspection.members[${index}].bytes`);
655
+ exactKeys(
656
+ entry.facts,
657
+ [
658
+ "container",
659
+ "videoCodec",
660
+ "pixelFormat",
661
+ "width",
662
+ "height",
663
+ "durationMs",
664
+ "frameRate",
665
+ "audioStreams",
666
+ "progressiveDownload",
667
+ ],
668
+ [],
669
+ `mediaInspection.members[${index}].facts`,
670
+ );
671
+ const facts = {
672
+ container: text(entry.facts.container, 1, 32, `mediaInspection.members[${index}].facts.container`),
673
+ videoCodec: text(entry.facts.videoCodec, 1, 32, `mediaInspection.members[${index}].facts.videoCodec`),
674
+ pixelFormat: text(entry.facts.pixelFormat, 0, 32, `mediaInspection.members[${index}].facts.pixelFormat`),
675
+ width: integer(entry.facts.width, 1, 16384, `mediaInspection.members[${index}].facts.width`),
676
+ height: integer(entry.facts.height, 1, 16384, `mediaInspection.members[${index}].facts.height`),
677
+ durationMs: integer(entry.facts.durationMs, 0, 3_600_000, `mediaInspection.members[${index}].facts.durationMs`),
678
+ frameRate: entry.facts.frameRate,
679
+ audioStreams: integer(entry.facts.audioStreams, 0, 64, `mediaInspection.members[${index}].facts.audioStreams`),
680
+ progressiveDownload: text(
681
+ entry.facts.progressiveDownload,
682
+ 1,
683
+ 32,
684
+ `mediaInspection.members[${index}].facts.progressiveDownload`,
685
+ ),
686
+ };
687
+ invariant(
688
+ Number.isFinite(facts.frameRate) && facts.frameRate >= 0 && facts.frameRate <= 240,
689
+ `mediaInspection.members[${index}].facts.frameRate is out of range`,
690
+ );
691
+ invariant(!byPath.has(entry.path), `duplicate media inspection member: ${entry.path}`);
692
+ const target = resolveInside(renderOutput, entry.path, "media inspection member");
693
+ const bytes = readRegular(target, entry.path, MAX_BUNDLE_MEMBER_BYTES);
694
+ invariant(entry.root === sha256(bytes), `media inspection member root mismatch: ${entry.path}`);
695
+ invariant(entry.bytes === bytes.length, `media inspection member byte count mismatch: ${entry.path}`);
696
+ byPath.set(entry.path, facts);
697
+ }
698
+ return {
699
+ inspectionRoot: value.inspectionRoot,
700
+ inspectMedia: (target) => {
701
+ const facts = byPath.get(path.relative(renderOutput, target).split(path.sep).join("/"));
702
+ invariant(facts, `media inspection facts are missing: ${path.basename(target)}`);
703
+ return facts;
704
+ },
705
+ };
706
+ }
707
+
708
+ function constraintsForAdditionalRendition(entry, policy) {
709
+ const common = {
710
+ path: entry.path,
711
+ role: entry.role,
712
+ mimeType: entry.mimeType,
713
+ maximumBytes: policy.maximumBytesByMimeType[entry.mimeType],
714
+ audioStreams: 0,
715
+ };
716
+ if (entry.mimeType === "video/mp4") {
717
+ return { ...common, container: "mp4", videoCodec: "h264", pixelFormat: "yuv420p", progressiveDownload: "moov-before-mdat" };
718
+ }
719
+ if (entry.mimeType === "video/webm") {
720
+ return { ...common, container: "webm", videoCodec: "vp9", pixelFormat: "yuv420p" };
721
+ }
722
+ if (entry.mimeType === "image/webp") return { ...common, container: "webp", videoCodec: "webp" };
723
+ if (entry.mimeType === "image/avif") return { ...common, container: "avif", videoCodec: "av1" };
724
+ throw new Error(`additional rendition MIME type is not allowed: ${entry.mimeType}`);
725
+ }
726
+
727
+ function qualifyRendererOutput(renderOutput, manifest, scene, profileId, inspectMedia, inspectionRoot = "") {
728
+ const loaded = loadMediaProfile(profileId);
729
+ const { profile, catalog } = loaded;
730
+ const outputs = manifest.outputs || {};
731
+ const rules = profile.renditions.map((entry) => ({ ...entry }));
732
+ const knownPaths = new Set(rules.map((entry) => entry.path));
733
+ const declaration = manifest.webDelivery;
734
+ if (declaration !== undefined) {
735
+ exactKeys(declaration, ["schema", "renditions"], [], "manifest.webDelivery");
736
+ invariant(
737
+ declaration.schema === "build-images.auditable-demo-web-delivery/v1",
738
+ "unsupported renderer web-delivery declaration",
739
+ );
740
+ invariant(Array.isArray(declaration.renditions), "manifest.webDelivery.renditions must be an array");
741
+ for (const [index, entry] of declaration.renditions.entries()) {
742
+ exactKeys(entry, ["path", "role", "mimeType"], [], `manifest.webDelivery.renditions[${index}]`);
743
+ invariant(!knownPaths.has(entry.path), `renderer web-delivery path is already profile-owned: ${entry.path}`);
744
+ const policy = profile.additionalRenditions;
745
+ invariant(policy, `media profile ${profileId} does not admit additional renditions`);
746
+ invariant(policy.allowedRoles.includes(entry.role), `additional rendition role is not allowed: ${entry.role}`);
747
+ invariant(policy.allowedMimeTypes.includes(entry.mimeType), `additional rendition MIME type is not allowed: ${entry.mimeType}`);
748
+ const maximumBytes = policy.maximumBytesByMimeType?.[entry.mimeType];
749
+ integer(maximumBytes, 1, MAX_BUNDLE_MEMBER_BYTES, `media profile ${profileId} additional ${entry.mimeType} budget`);
750
+ rules.push(constraintsForAdditionalRendition(entry, policy));
751
+ knownPaths.add(entry.path);
752
+ }
753
+ }
754
+ for (const name of Object.keys(outputs)) {
755
+ if (name !== "media-probe.json") invariant(knownPaths.has(name), `unbound renderer output: ${name}`);
756
+ }
757
+ const singletonRoles = new Set(profile.singletonRoles || []);
758
+ const observedRoles = new Set();
759
+ const renditions = [];
760
+ for (const rule of rules) {
761
+ const declared = outputs[rule.path];
762
+ invariant(declared && typeof declared === "object", `required renderer output is missing: ${rule.path}`);
763
+ const target = resolveInside(renderOutput, rule.path, "renderer output path");
764
+ const bytes = readRegular(target, rule.path, MAX_BUNDLE_MEMBER_BYTES);
765
+ invariant(declared.root === sha256(bytes), `renderer manifest root mismatch: ${rule.path}`);
766
+ invariant(declared.bytes === bytes.length, `renderer manifest byte count mismatch: ${rule.path}`);
767
+ if (rule.maximumBytes !== undefined) {
768
+ invariant(bytes.length <= rule.maximumBytes, `${rule.path} byte budget exceeded`);
769
+ }
770
+ if (singletonRoles.has(rule.role)) {
771
+ invariant(!observedRoles.has(rule.role), `duplicate singleton role: ${rule.role}`);
772
+ observedRoles.add(rule.role);
773
+ }
774
+ const facts = profile.mode === "web-delivery" ? inspectMedia(target) : {};
775
+ if (profile.mode === "web-delivery") {
776
+ invariant(facts && typeof facts === "object", `${rule.path} inspection is missing`);
777
+ invariant(facts.container === rule.container, `${rule.path} container mismatch`);
778
+ invariant(facts.videoCodec === rule.videoCodec, `${rule.path} video codec mismatch`);
779
+ if (rule.pixelFormat) invariant(facts.pixelFormat === rule.pixelFormat, `${rule.path} pixel format mismatch`);
780
+ invariant(facts.audioStreams === rule.audioStreams, `${rule.path} audio stream policy failed`);
781
+ invariant(facts.width === scene.width && facts.height === scene.height, `${rule.path} dimensions mismatch`);
782
+ if (facts.durationMs > 0) {
783
+ invariant(
784
+ Math.abs(facts.durationMs - scene.durationMs) <= catalog.qualification.durationToleranceMs,
785
+ `${rule.path} duration mismatch`,
786
+ );
787
+ }
788
+ if (rule.frameRatePolicy === "scene-exact") {
789
+ invariant(Math.abs(facts.frameRate - scene.fps) < 0.001, `${rule.path} frame rate mismatch`);
790
+ }
791
+ if (rule.progressiveDownload) {
792
+ invariant(
793
+ facts.progressiveDownload === rule.progressiveDownload,
794
+ `${rule.path} progressive download evidence mismatch`,
795
+ );
796
+ }
797
+ }
798
+ renditions.push({
799
+ path: rule.path,
800
+ role: rule.role,
801
+ mimeType: rule.mimeType,
802
+ root: declared.root,
803
+ bytes: declared.bytes,
804
+ maximumBytes: rule.maximumBytes || 0,
805
+ ...facts,
806
+ });
807
+ }
808
+ const body = {
809
+ schema: "buildchain.auditable-demo-media-qualification/v1",
810
+ profile: {
811
+ id: profileId,
812
+ mode: profile.mode,
813
+ catalogRoot: loaded.catalogRoot,
814
+ profileRoot: loaded.profileRoot,
815
+ },
816
+ inspectionRoot,
817
+ renditions,
818
+ nonClaims: catalog.qualification.nonClaims,
819
+ };
820
+ return { ...body, qualificationRoot: semanticRoot(body) };
821
+ }
822
+
823
+ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, options = {}) {
375
824
  invariant(IMAGE_PATTERN.test(expectedImage), "renderer image must use an immutable sha256 coordinate");
376
- const expectedMembers = [
825
+ const fixedMembers = [
377
826
  "checksums.sha256",
378
827
  "complete-transcript.txt",
379
- "demo.gif",
380
- "demo.mp4",
381
- "demo.webm",
382
828
  "manifest.json",
383
- "media-probe.json",
384
- "poster.png",
385
829
  "public-projection.json",
386
830
  "scene.json",
387
831
  ];
388
- invariant(
389
- JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers),
390
- "renderer output member set is not exact",
391
- );
392
832
  verifyChecksums(renderOutput);
393
833
  const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
394
834
  invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
395
835
  invariant(manifest.renderer?.image === expectedImage, "renderer manifest image coordinate mismatch");
396
- invariant(
397
- JSON.stringify(Object.keys(manifest.outputs || {}).sort()) ===
398
- JSON.stringify(["demo.gif", "demo.mp4", "demo.webm", "media-probe.json", "poster.png"]),
399
- "renderer manifest output set is not exact",
400
- );
836
+ const outputNames = Object.keys(manifest.outputs || {}).sort();
837
+ invariant(outputNames.includes("media-probe.json"), "renderer manifest must declare media-probe.json");
838
+ const expectedMembers = [...new Set([...fixedMembers, ...outputNames])].sort();
839
+ invariant(JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers), "renderer output member set is not exact");
840
+ for (const name of outputNames) {
841
+ invariant(!name.includes("\\") && !name.split("/").includes(".."), `renderer output path is invalid: ${name}`);
842
+ const target = resolveInside(renderOutput, name, "renderer manifest output");
843
+ const bytes = readRegular(target, name, MAX_BUNDLE_MEMBER_BYTES);
844
+ const declared = manifest.outputs[name];
845
+ invariant(declared?.root === sha256(bytes), `renderer manifest root mismatch: ${name}`);
846
+ invariant(declared?.bytes === bytes.length, `renderer manifest byte count mismatch: ${name}`);
847
+ }
401
848
  for (const [key, filePath] of Object.entries(expectedInputs)) {
402
849
  const observed = manifest.inputs?.[key]?.root;
403
850
  invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
404
851
  }
405
852
  const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
406
853
  invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
407
- return { manifest, probe };
854
+ const qualification = qualifyRendererOutput(
855
+ renderOutput,
856
+ manifest,
857
+ readJson(path.join(renderOutput, "scene.json"), "renderer scene"),
858
+ options.mediaProfile || "archive-v1",
859
+ options.inspectMedia || inspectMediaFile,
860
+ options.inspectionRoot || "",
861
+ );
862
+ return { manifest, probe, qualification };
408
863
  }
409
864
 
410
865
  function finalizeGate(values) {
@@ -427,7 +882,9 @@ function finalizeGate(values) {
427
882
  const sourceCoordinate = validateSourceCoordinate(readJson(sourceCoordinatePath, "source artifact coordinate"));
428
883
  invariant(sourceCoordinate.sourceSha === sourceSha, "source artifact coordinate SHA mismatch");
429
884
  ensureEmptyDirectory(output, "gate bundle");
430
- for (const name of REQUIRED_ADAPTER_FILES) copyFile(path.join(adapterOutput, name), path.join(output, name));
885
+ fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
886
+ writeJson(path.join(output, "scene.json"), normalized.scene);
887
+ writeJson(path.join(output, "public-projection.json"), normalized.projection);
431
888
  copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
432
889
  copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
433
890
  for (const name of listFiles(smokeOutput)) {
@@ -454,9 +911,9 @@ function finalizeGate(values) {
454
911
  smokeManifestRoot: sha256(readRegular(path.join(smokeOutput, "manifest.json"), "smoke manifest")),
455
912
  },
456
913
  qualifiedInputs: {
457
- transcript: sha256(readRegular(path.join(adapterOutput, "complete-transcript.txt"), "transcript")),
458
- projection: sha256(readRegular(path.join(adapterOutput, "public-projection.json"), "projection")),
459
- scene: sha256(readRegular(path.join(adapterOutput, "scene.json"), "scene")),
914
+ transcript: sha256(readRegular(path.join(output, "complete-transcript.txt"), "transcript")),
915
+ projection: sha256(readRegular(path.join(output, "public-projection.json"), "projection")),
916
+ scene: sha256(readRegular(path.join(output, "scene.json"), "scene")),
460
917
  evidenceClass: normalized.projection.evidenceClass,
461
918
  claimBoundary: normalized.projection.claimBoundary,
462
919
  },
@@ -498,16 +955,32 @@ function finalizeMedia(values) {
498
955
  const rendererImage = required(values, "--renderer-image");
499
956
  const gateRoot = required(values, "--gate-root");
500
957
  const sourceSha = required(values, "--source-sha");
958
+ const mediaProfile = values["--media-profile"] || "archive-v1";
959
+ const selectedProfile = loadMediaProfile(mediaProfile).profile;
960
+ const mediaInspectionPath = values["--media-inspection"]
961
+ ? path.resolve(values["--media-inspection"])
962
+ : "";
963
+ const mediaInspection = selectedProfile.mode === "web-delivery"
964
+ ? loadMediaInspection(
965
+ required({ "--media-inspection": mediaInspectionPath }, "--media-inspection"),
966
+ renderOutput,
967
+ rendererImage,
968
+ )
969
+ : null;
501
970
  verifyGate({
502
971
  "--bundle": gateBundle,
503
972
  "--expected-root": gateRoot,
504
973
  "--renderer-image": rendererImage,
505
974
  "--source-sha": sourceSha,
506
975
  });
507
- verifyRendererOutput(renderOutput, rendererImage, {
976
+ const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
508
977
  scene: path.join(gateBundle, "scene.json"),
509
978
  transcript: path.join(gateBundle, "complete-transcript.txt"),
510
979
  projection: path.join(gateBundle, "public-projection.json"),
980
+ }, {
981
+ mediaProfile,
982
+ inspectMedia: mediaInspection?.inspectMedia,
983
+ inspectionRoot: mediaInspection?.inspectionRoot || "",
511
984
  });
512
985
  ensureEmptyDirectory(output, "media bundle");
513
986
  for (const name of listFiles(renderOutput)) {
@@ -515,19 +988,30 @@ function finalizeMedia(values) {
515
988
  copyFile(path.join(renderOutput, name), path.join(output, destination));
516
989
  }
517
990
  copyFile(path.join(gateBundle, "gate-receipt.json"), path.join(output, "gate-receipt.json"));
518
- writeJson(path.join(output, "media-receipt.json"), {
519
- schema: "buildchain.auditable-demo-media/v1",
991
+ if (mediaInspectionPath) copyFile(mediaInspectionPath, path.join(output, "media-inspection.json"));
992
+ const commonReceipt = {
520
993
  status: "passed",
521
994
  sourceSha,
522
995
  qualifiedGateRoot: gateRoot,
523
996
  rendererImage,
524
997
  rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
525
- });
998
+ };
999
+ const mediaReceipt = selectedProfile.mode === "archive"
1000
+ ? { schema: "buildchain.auditable-demo-media/v1", ...commonReceipt }
1001
+ : {
1002
+ schema: "buildchain.auditable-demo-media/v2",
1003
+ ...commonReceipt,
1004
+ qualification: verifiedRenderer.qualification,
1005
+ qualificationRoot: verifiedRenderer.qualification.qualificationRoot,
1006
+ };
1007
+ writeJson(path.join(output, "media-receipt.json"), mediaReceipt);
526
1008
  const root = writeChecksums(output);
527
1009
  const artifactName = `auditable-demo-media-${sourceSha.slice(0, 12)}-${root.slice(7, 23)}`;
528
1010
  appendOutputs(values["--github-output"], {
529
1011
  "media-root": root,
530
1012
  "media-artifact-name": artifactName,
1013
+ "media-profile": mediaProfile,
1014
+ "media-qualification-root": verifiedRenderer.qualification.qualificationRoot,
531
1015
  });
532
1016
  process.stdout.write(stableJson({ status: "passed", root, artifactName }));
533
1017
  }
@@ -545,6 +1029,10 @@ function main(argv) {
545
1029
  return verifyGate(values);
546
1030
  case "finalize-media":
547
1031
  return finalizeMedia(values);
1032
+ case "inspect-media":
1033
+ return inspectRendererMedia(values);
1034
+ case "qualify-media-fixture":
1035
+ return qualifyMediaFixture(values);
548
1036
  default:
549
1037
  throw new Error(`unknown command: ${command || "<empty>"}`);
550
1038
  }
@@ -563,6 +1051,10 @@ if (invokedDirectly) {
563
1051
  export {
564
1052
  finalizeGate,
565
1053
  finalizeMedia,
1054
+ inspectIsoBmffFastStart,
1055
+ inspectMediaFile,
1056
+ inspectRendererMedia,
1057
+ qualifyMediaFixture,
566
1058
  prepareSmoke,
567
1059
  runAdapter,
568
1060
  sha256,