@kungfu-tech/buildchain 3.0.3 → 3.0.4-alpha.1

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.
@@ -31,8 +31,34 @@ import {
31
31
  sha256Text,
32
32
  stableJson,
33
33
  } from "./paper-repository.js";
34
+ import {
35
+ PAPER_AGENT_ENTRY_CONTRACT,
36
+ collectPaperAgentEntry,
37
+ paperAgentEntryFiles,
38
+ resolvePaperBuildchainSha,
39
+ } from "./paper-agent-entry.js";
40
+ import {
41
+ managedPaperPackageJson,
42
+ scaffoldMainTex,
43
+ scaffoldMakefile,
44
+ scaffoldMap,
45
+ scaffoldPackageJson,
46
+ scaffoldReadme,
47
+ } from "./paper-scaffold-content.js";
34
48
 
35
49
  export { PAPER_PATHS, resolvePaperRepository } from "./paper-repository.js";
50
+ export {
51
+ PAPER_AGENT_ENTRY_CONTRACT,
52
+ PAPER_AGENT_ENTRY_SCHEMA_VERSION,
53
+ PAPER_AGENT_ENTRY_SECTION_END,
54
+ PAPER_AGENT_ENTRY_SECTION_START,
55
+ collectPaperAgentEntry,
56
+ createPaperAgentEntry,
57
+ mergePaperAgentEntryInstructions,
58
+ paperAgentEntryFiles,
59
+ paperAgentEntryInstructions,
60
+ resolvePaperBuildchainSha,
61
+ } from "./paper-agent-entry.js";
36
62
  export {
37
63
  PAPER_WORK_START_PLAN_CONTRACT,
38
64
  PAPER_WORK_SUBMIT_PLAN_CONTRACT,
@@ -79,9 +105,12 @@ export const PAPER_STATE_ORDER = Object.freeze([
79
105
 
80
106
  const PAPER_SCAFFOLD_PATHS = Object.freeze([
81
107
  PAPER_PATHS.config,
108
+ PAPER_PATHS.agentEntry,
109
+ PAPER_PATHS.agentInstructions,
82
110
  PAPER_PATHS.contractLock,
83
111
  PAPER_PATHS.versionPin,
84
112
  PAPER_PATHS.buildWorkflow,
113
+ PAPER_PATHS.verifyWorkflow,
85
114
  PAPER_PATHS.releaseWorkflow,
86
115
  PAPER_PATHS.provisioningAuthority,
87
116
  "Makefile",
@@ -149,8 +178,11 @@ function buildchainPackageIdentity(buildchainRoot, explicitVersion = "") {
149
178
  };
150
179
  }
151
180
 
152
- function runtimeGitSha(buildchainRoot, buildchainVersion = "") {
153
- const value = gitValue(buildchainRoot, ["rev-parse", "HEAD"]);
181
+ export function resolvePaperRuntimeGitSha(
182
+ buildchainRoot,
183
+ buildchainVersion = "",
184
+ ) {
185
+ const value = resolvePaperBuildchainSha(buildchainRoot);
154
186
  if (GIT_SHA_PATTERN.test(value)) return value;
155
187
  const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
156
188
  if (!identity.version) return "";
@@ -227,18 +259,6 @@ function tomlString(value) {
227
259
  return JSON.stringify(String(value || ""));
228
260
  }
229
261
 
230
- function texEscape(value) {
231
- return String(value || "")
232
- .replaceAll("\\", "\\textbackslash{}")
233
- .replaceAll("&", "\\&")
234
- .replaceAll("%", "\\%")
235
- .replaceAll("$", "\\$")
236
- .replaceAll("#", "\\#")
237
- .replaceAll("_", "\\_")
238
- .replaceAll("{", "\\{")
239
- .replaceAll("}", "\\}");
240
- }
241
-
242
262
  function joinUrl(base, suffix = "") {
243
263
  const normalizedBase = String(base || "")
244
264
  .trim()
@@ -334,6 +354,31 @@ jobs:
334
354
  `;
335
355
  }
336
356
 
357
+ function scaffoldVerifyWorkflow(buildchainSha) {
358
+ return `name: Verify
359
+
360
+ on:
361
+ pull_request:
362
+ push:
363
+ branches:
364
+ - "dev/v*/v*"
365
+ - "alpha/v*/v*"
366
+ - "release/v*/v*"
367
+ workflow_dispatch:
368
+
369
+ permissions:
370
+ contents: read
371
+
372
+ jobs:
373
+ check:
374
+ uses: kungfu-systems/buildchain/.github/workflows/check.yml@${buildchainSha}
375
+ with:
376
+ buildchain-ref: ${buildchainSha}
377
+ require-version-state: true
378
+ upload-artifacts: true
379
+ `;
380
+ }
381
+
337
382
  function scaffoldReleaseWorkflow(
338
383
  buildchainSha,
339
384
  { artifactPaths = "_build/main.pdf", releasePassportProductName = "" } = {},
@@ -413,7 +458,10 @@ export function createPaperProvisioningAuthority({
413
458
  buildchainSha,
414
459
  contractLock,
415
460
  buildWorkflow,
461
+ verifyWorkflow,
416
462
  releaseWorkflow,
463
+ agentEntry,
464
+ agentInstructions,
417
465
  environment = "",
418
466
  }) {
419
467
  const policy = paperProvisioningPolicy();
@@ -439,6 +487,12 @@ export function createPaperProvisioningAuthority({
439
487
  reusablePath: ".github/workflows/publication-artifact.yml",
440
488
  reusableRef: buildchainSha,
441
489
  },
490
+ verify: {
491
+ path: PAPER_PATHS.verifyWorkflow,
492
+ sourceDigest: sha256Text(verifyWorkflow),
493
+ reusablePath: ".github/workflows/check.yml",
494
+ reusableRef: buildchainSha,
495
+ },
442
496
  release: {
443
497
  path: PAPER_PATHS.releaseWorkflow,
444
498
  sourceDigest: sha256Text(releaseWorkflow),
@@ -446,6 +500,13 @@ export function createPaperProvisioningAuthority({
446
500
  reusableRef: buildchainSha,
447
501
  },
448
502
  },
503
+ agentEntry: {
504
+ contract: PAPER_AGENT_ENTRY_CONTRACT,
505
+ policyPath: PAPER_PATHS.agentEntry,
506
+ policyDigest: sha256Text(agentEntry),
507
+ instructionsPath: PAPER_PATHS.agentInstructions,
508
+ instructionsDigest: sha256Text(agentInstructions),
509
+ },
449
510
  admission: {
450
511
  contractLockPath: PAPER_PATHS.contractLock,
451
512
  contractLockDigest: sha256Text(contractLock),
@@ -469,150 +530,6 @@ export function createPaperProvisioningAuthority({
469
530
  };
470
531
  }
471
532
 
472
- function scaffoldMakefile() {
473
- const image = `${DEFAULT_TOOLCHAIN_IMAGE}@${DEFAULT_TOOLCHAIN_DIGEST}`;
474
- return `.PHONY: check pdf clean
475
-
476
- BUILDER_IMAGE := ${image}
477
- SOURCE_DATE_EPOCH ?= 0
478
-
479
- check:
480
- \t@test -f paper/main.tex
481
- \t@test -f paper/references.bib
482
- \t@git diff --check
483
-
484
- pdf:
485
- \t@mkdir -p _build
486
- \tdocker run --rm --network=none \\
487
- \t\t-e SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \\
488
- \t\t-e TZ=UTC -e LANG=C.UTF-8 -e LC_ALL=C.UTF-8 -e HOME=/tmp \\
489
- \t\t-v "$(CURDIR):/workspace" -w /workspace \\
490
- \t\t"$(BUILDER_IMAGE)" bash -lc '${DEFAULT_TOOLCHAIN_COMMAND}'
491
-
492
- clean:
493
- \trm -rf _build
494
- `;
495
- }
496
-
497
- function paperPackageScripts(current = {}) {
498
- return {
499
- ...current,
500
- "buildchain:paper": "buildchain paper",
501
- "paper:preflight": "buildchain paper preflight --json",
502
- "paper:work:start": "buildchain paper work start",
503
- "paper:work:submit": "buildchain paper work submit",
504
- "paper:status": "buildchain paper status --json",
505
- };
506
- }
507
-
508
- function managedPaperPackageJson(current, buildchainVersion) {
509
- if (!/^3\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(buildchainVersion)) {
510
- throw new Error(
511
- "paper repositories require an exact Buildchain v3 version",
512
- );
513
- }
514
- const packageManager = current.packageManager || "pnpm@11.7.0";
515
- if (!packageManager.startsWith("pnpm@")) {
516
- throw new Error("paper repositories require a pnpm packageManager");
517
- }
518
- return {
519
- ...current,
520
- private: true,
521
- scripts: paperPackageScripts(current.scripts),
522
- devDependencies: {
523
- ...(current.devDependencies || {}),
524
- "@kungfu-tech/buildchain": buildchainVersion,
525
- },
526
- packageManager,
527
- };
528
- }
529
-
530
- function scaffoldPackageJson({
531
- name,
532
- packageName,
533
- repository,
534
- buildchainVersion,
535
- }) {
536
- return jsonText({
537
- ...managedPaperPackageJson(
538
- {
539
- name: packageName,
540
- description: `${name} publication source repository.`,
541
- repository: {
542
- type: "git",
543
- url: `git+https://github.com/${repository}.git`,
544
- },
545
- license: "Apache-2.0",
546
- },
547
- buildchainVersion,
548
- ),
549
- });
550
- }
551
-
552
- function scaffoldReadme({ title, packageName }) {
553
- return `# ${title}
554
-
555
- This repository is a Buildchain-governed publication artifact source.
556
-
557
- ## Local workflow
558
-
559
- \`\`\`sh
560
- pnpm paper:preflight
561
- pnpm paper:work:start -- <topic>
562
- pnpm paper:work:submit
563
- make pdf
564
- \`\`\`
565
-
566
- The public package identity is \`${packageName}\`. Buildchain owns reproducible
567
- artifact generation, sealed publication, npm Trusted Publishing, and recovery;
568
- this repository owns the paper source and review history.
569
-
570
- See [docs/MAP.md](docs/MAP.md) for the repository map.
571
- `;
572
- }
573
-
574
- function scaffoldMap() {
575
- return `# Repository Map
576
-
577
- - \`paper/main.tex\`: paper source entrypoint.
578
- - \`paper/references.bib\`: bibliography source.
579
- - \`.buildchain/buildchain.toml\`: publication identity, toolchain, package, and lifecycle contract.
580
- - \`.buildchain/contract-lock.json\`: accepted Buildchain runtime contract.
581
- - \`.github/workflows/build.yml\`: thin read-only build and reproducibility caller.
582
- - \`.github/workflows/paper-release.yml\`: thin protected sealed-release caller.
583
-
584
- The repository does not own npm transaction logic, publication authority,
585
- release-state recovery, or site deployment mechanics. Those remain Buildchain,
586
- npm/GitHub, and downstream site responsibilities respectively.
587
- `;
588
- }
589
-
590
- function scaffoldMainTex(title) {
591
- return `\\documentclass[11pt]{article}
592
- \\usepackage[T1]{fontenc}
593
- \\usepackage{lmodern}
594
- \\usepackage{hyperref}
595
-
596
- \\title{${texEscape(title)}}
597
- \\author{}
598
- \\date{}
599
-
600
- \\begin{document}
601
- \\maketitle
602
-
603
- \\begin{abstract}
604
- Replace this paragraph with the paper abstract.
605
- \\end{abstract}
606
-
607
- \\section{Introduction}
608
- Replace this section with the reviewed paper content.
609
-
610
- \\bibliographystyle{plain}
611
- \\bibliography{paper/references}
612
- \\end{document}
613
- `;
614
- }
615
-
616
533
  function scaffoldFiles({
617
534
  buildchainRoot,
618
535
  buildchainVersion,
@@ -647,6 +564,15 @@ function scaffoldFiles({
647
564
  artifactPaths: "_build/main.pdf",
648
565
  releasePassportProductName: title,
649
566
  });
567
+ const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha);
568
+ const agentEntry = paperAgentEntryFiles({
569
+ cwd,
570
+ buildchainVersion,
571
+ buildchainSha,
572
+ developmentRef: `dev/v${parsePaperVersion(version).major}/v${
573
+ parsePaperVersion(version).major
574
+ }.${parsePaperVersion(version).minor}`,
575
+ });
650
576
  const provisioningAuthority = createPaperProvisioningAuthority({
651
577
  repository,
652
578
  packageName,
@@ -654,7 +580,10 @@ function scaffoldFiles({
654
580
  buildchainSha,
655
581
  contractLock: contractLockText,
656
582
  buildWorkflow,
583
+ verifyWorkflow,
657
584
  releaseWorkflow,
585
+ agentEntry: agentEntry.get(PAPER_PATHS.agentEntry),
586
+ agentInstructions: agentEntry.get(PAPER_PATHS.agentInstructions),
658
587
  });
659
588
  const licenseText = runtimeLicenseText(buildchainRoot);
660
589
  return new Map([
@@ -671,9 +600,18 @@ function scaffoldFiles({
671
600
  [PAPER_PATHS.contractLock, contractLockText],
672
601
  [PAPER_PATHS.versionPin, `${buildchainVersion}\n`],
673
602
  [PAPER_PATHS.buildWorkflow, buildWorkflow],
603
+ [PAPER_PATHS.verifyWorkflow, verifyWorkflow],
674
604
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
675
605
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
676
- ["Makefile", scaffoldMakefile()],
606
+ ...agentEntry,
607
+ [
608
+ "Makefile",
609
+ scaffoldMakefile({
610
+ image: DEFAULT_TOOLCHAIN_IMAGE,
611
+ digest: DEFAULT_TOOLCHAIN_DIGEST,
612
+ command: DEFAULT_TOOLCHAIN_COMMAND,
613
+ }),
614
+ ],
677
615
  [
678
616
  "package.json",
679
617
  scaffoldPackageJson({
@@ -727,7 +665,8 @@ export function planPaperScaffold({
727
665
  buildchainVersion,
728
666
  );
729
667
  const runtimeSha =
730
- buildchainSha || runtimeGitSha(buildchainRoot, runtimeIdentity.version);
668
+ buildchainSha ||
669
+ resolvePaperRuntimeGitSha(buildchainRoot, runtimeIdentity.version);
731
670
  if (!GIT_SHA_PATTERN.test(runtimeSha)) {
732
671
  throw new Error(
733
672
  "paper scaffold cannot resolve the exact Buildchain runtime SHA; pass buildchainSha or use a published Buildchain package with npm gitHead provenance",
@@ -920,7 +859,8 @@ function migrationFiles({
920
859
  "paper publish package",
921
860
  );
922
861
  const runtimeSha =
923
- buildchainSha || runtimeGitSha(buildchainRoot, buildchainVersion);
862
+ buildchainSha ||
863
+ resolvePaperRuntimeGitSha(buildchainRoot, buildchainVersion);
924
864
  if (!GIT_SHA_PATTERN.test(runtimeSha)) {
925
865
  throw new Error("paper migration requires an exact Buildchain source SHA");
926
866
  }
@@ -957,6 +897,12 @@ function migrationFiles({
957
897
  artifactPaths: config.publication.artifactPaths.join(","),
958
898
  releasePassportProductName: config.publication.title,
959
899
  });
900
+ const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha);
901
+ const agentEntry = paperAgentEntryFiles({
902
+ cwd,
903
+ buildchainVersion: runtimeIdentity.version,
904
+ buildchainSha: runtimeSha,
905
+ });
960
906
  const provisioningAuthority = createPaperProvisioningAuthority({
961
907
  repository,
962
908
  packageName,
@@ -964,7 +910,10 @@ function migrationFiles({
964
910
  buildchainSha: runtimeSha,
965
911
  contractLock: contractLockText,
966
912
  buildWorkflow,
913
+ verifyWorkflow,
967
914
  releaseWorkflow,
915
+ agentEntry: agentEntry.get(PAPER_PATHS.agentEntry),
916
+ agentInstructions: agentEntry.get(PAPER_PATHS.agentInstructions),
968
917
  });
969
918
  const currentPackage = readJson(path.resolve(cwd, "package.json"));
970
919
  if (!currentPackage.exists || currentPackage.error || !currentPackage.value) {
@@ -978,21 +927,12 @@ function migrationFiles({
978
927
  [PAPER_PATHS.contractLock, contractLockText],
979
928
  [PAPER_PATHS.versionPin, `${runtimeIdentity.version}\n`],
980
929
  [PAPER_PATHS.buildWorkflow, buildWorkflow],
930
+ [PAPER_PATHS.verifyWorkflow, verifyWorkflow],
981
931
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
982
932
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
933
+ ...agentEntry,
983
934
  ["package.json", jsonText(packageJson)],
984
935
  ]);
985
- const verifyWorkflowPath = path.resolve(cwd, PAPER_PATHS.verifyWorkflow);
986
- if (fs.existsSync(verifyWorkflowPath)) {
987
- const currentVerifyWorkflow = fs.readFileSync(verifyWorkflowPath, "utf8");
988
- const migratedVerifyWorkflow = currentVerifyWorkflow.replace(
989
- /(uses:\s*kungfu-systems\/buildchain\/\.github\/workflows\/check\.yml@)[^\s]+/g,
990
- `$1${runtimeSha}`,
991
- );
992
- if (migratedVerifyWorkflow !== currentVerifyWorkflow) {
993
- files.set(PAPER_PATHS.verifyWorkflow, migratedVerifyWorkflow);
994
- }
995
- }
996
936
  return files;
997
937
  }
998
938
 
@@ -1687,7 +1627,27 @@ function validatePaperProvisioningAuthority(cwd) {
1687
1627
  "paper release policy permits avoidable bookkeeping pull requests",
1688
1628
  );
1689
1629
  }
1690
- for (const workflow of [value.workflows?.build, value.workflows?.release]) {
1630
+ if (value.agentEntry?.contract !== PAPER_AGENT_ENTRY_CONTRACT) {
1631
+ errors.push("paper agent-entry authority contract mismatch");
1632
+ }
1633
+ for (const [entryPath, expectedDigest] of [
1634
+ [value.agentEntry?.policyPath, value.agentEntry?.policyDigest],
1635
+ [value.agentEntry?.instructionsPath, value.agentEntry?.instructionsDigest],
1636
+ ]) {
1637
+ if (!entryPath || !SHA256_PATTERN.test(String(expectedDigest || ""))) {
1638
+ errors.push("paper agent-entry authority is incomplete");
1639
+ continue;
1640
+ }
1641
+ const absolute = path.resolve(cwd, entryPath);
1642
+ if (!fs.existsSync(absolute) || sha256File(absolute) !== expectedDigest) {
1643
+ errors.push(`paper agent-entry source digest mismatch: ${entryPath}`);
1644
+ }
1645
+ }
1646
+ for (const workflow of [
1647
+ value.workflows?.build,
1648
+ value.workflows?.verify,
1649
+ value.workflows?.release,
1650
+ ]) {
1691
1651
  if (!workflow?.path || !workflow?.sourceDigest) {
1692
1652
  errors.push("paper workflow authority is incomplete");
1693
1653
  continue;
@@ -1707,7 +1667,10 @@ function validatePaperProvisioningAuthority(cwd) {
1707
1667
  `paper workflow reusable source is not exact: ${workflow.path}`,
1708
1668
  );
1709
1669
  }
1710
- if (!text.includes(`buildchain-ref: ${value.runtime.resolvedSha}`)) {
1670
+ if (
1671
+ workflow.reusablePath !== ".github/workflows/check.yml" &&
1672
+ !text.includes(`buildchain-ref: ${value.runtime.resolvedSha}`)
1673
+ ) {
1711
1674
  errors.push(
1712
1675
  `paper workflow runtime input is not exact: ${workflow.path}`,
1713
1676
  );
@@ -1777,7 +1740,8 @@ function runtimeFacts({
1777
1740
  version: identity.version,
1778
1741
  ref: buildchainRef,
1779
1742
  resolvedSha:
1780
- buildchainSha || runtimeGitSha(buildchainRoot, identity.version),
1743
+ buildchainSha ||
1744
+ resolvePaperRuntimeGitSha(buildchainRoot, identity.version),
1781
1745
  contract: contractWorld.contract,
1782
1746
  contractDigest: contractWorld.contractDigest,
1783
1747
  compatibilityDigest: contractWorld.compatibilityDigest,
@@ -2094,6 +2058,7 @@ export function collectPaperPreflight({
2094
2058
  buildchainSha = "",
2095
2059
  registry = NPM_REGISTRY,
2096
2060
  offline = false,
2061
+ agentEntryMode = "contract",
2097
2062
  } = {}) {
2098
2063
  const resolvedCwd = path.resolve(cwd);
2099
2064
  const status = collectPaperStatus({ cwd: resolvedCwd });
@@ -2114,6 +2079,11 @@ export function collectPaperPreflight({
2114
2079
  buildchainRef: provisioning.value?.runtime?.ref || buildchainRef,
2115
2080
  buildchainSha,
2116
2081
  });
2082
+ const agentEntry = collectPaperAgentEntry({
2083
+ cwd: resolvedCwd,
2084
+ buildchainSha: runtime.resolvedSha,
2085
+ mode: agentEntryMode,
2086
+ });
2117
2087
  const lockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
2118
2088
  let lockEvaluation = {
2119
2089
  status: "missing-lock",
@@ -2220,6 +2190,11 @@ export function collectPaperPreflight({
2220
2190
  configResult.loaded?.config?.publication,
2221
2191
  );
2222
2192
  const checks = [
2193
+ ...agentEntry.checks.map((entry) => ({
2194
+ ...entry,
2195
+ blocking: true,
2196
+ scope: "local",
2197
+ })),
2223
2198
  {
2224
2199
  id: "provisioning.authority",
2225
2200
  status: provisioning.valid ? "pass" : "fail",
@@ -2486,6 +2461,7 @@ export function collectPaperPreflight({
2486
2461
  policyDigest: provisioning.value?.policy?.policyDigest || "",
2487
2462
  errors: provisioning.errors,
2488
2463
  },
2464
+ agentEntry,
2489
2465
  repositoryPermissions,
2490
2466
  repositoryActions,
2491
2467
  generatedWriteAuthority,
@@ -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}`,