@xemahq/edition-tooling 0.0.0-stage → 0.1.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.
Files changed (219) hide show
  1. package/LICENSE +175 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +47 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/edition.d.ts +4 -0
  7. package/dist/commands/edition.d.ts.map +1 -0
  8. package/dist/commands/edition.js +58 -0
  9. package/dist/commands/edition.js.map +1 -0
  10. package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
  11. package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
  12. package/dist/lib/exit-codes.d.ts +6 -0
  13. package/dist/lib/exit-codes.d.ts.map +1 -0
  14. package/dist/lib/exit-codes.js +21 -0
  15. package/dist/lib/exit-codes.js.map +1 -0
  16. package/dist/runtime/edition/run-entry.d.ts +10 -0
  17. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  18. package/dist/runtime/edition/run-entry.js +49 -0
  19. package/dist/runtime/edition/run-entry.js.map +1 -0
  20. package/dist/runtime/edition/verb-map.d.ts +70 -0
  21. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  22. package/dist/runtime/edition/verb-map.js +166 -0
  23. package/dist/runtime/edition/verb-map.js.map +1 -0
  24. package/edition/README.md +291 -0
  25. package/edition/adapter-bundled-versions.mjs +128 -0
  26. package/edition/adapter-host-versions.mjs +76 -0
  27. package/edition/assemble-producer-input.mjs +1423 -0
  28. package/edition/assemble-third-party-input.mjs +348 -0
  29. package/edition/authored-source-check.mjs +141 -0
  30. package/edition/authority-binding.mjs +23 -0
  31. package/edition/authorization-base.mjs +357 -0
  32. package/edition/biome-content.mjs +327 -0
  33. package/edition/build-biome-index.mjs +180 -0
  34. package/edition/build-ledger-completeness.mjs +67 -0
  35. package/edition/build-ledger.mjs +107 -0
  36. package/edition/build-platform-service-index.mjs +292 -0
  37. package/edition/build-runtime-image-source-index.mjs +1197 -0
  38. package/edition/built-verifier.mjs +55 -0
  39. package/edition/candidate-edition.mjs +735 -0
  40. package/edition/canonical-evidence-json.mjs +82 -0
  41. package/edition/canonical-repository-url.mjs +61 -0
  42. package/edition/catalogue-edition.mjs +49 -0
  43. package/edition/catalogue-evidence.mjs +228 -0
  44. package/edition/catalogue-fetch.mjs +124 -0
  45. package/edition/catalogue-lock-field.mjs +41 -0
  46. package/edition/catalogue-pin.mjs +71 -0
  47. package/edition/catalogue.mjs +237 -0
  48. package/edition/chart-set-source-commit.mjs +97 -0
  49. package/edition/check-committed-image-evidence.mjs +412 -0
  50. package/edition/checkout-indexed-source-repositories.mjs +283 -0
  51. package/edition/collected-refusals.mjs +59 -0
  52. package/edition/comment-blanker.mjs +75 -0
  53. package/edition/connector-source-authority.mjs +60 -0
  54. package/edition/contract-authority.mjs +122 -0
  55. package/edition/cosign-certificate-constraints.mjs +50 -0
  56. package/edition/cross-repo-build-inputs.mjs +158 -0
  57. package/edition/customer-definition.mjs +72 -0
  58. package/edition/customer-edition-verbs.mjs +610 -0
  59. package/edition/customer-flags.mjs +15 -0
  60. package/edition/definition-checkout.mjs +96 -0
  61. package/edition/develop-build-record.mjs +122 -0
  62. package/edition/develop-catalogue-free-resolve.mjs +282 -0
  63. package/edition/develop-catalogue-free-vendor.mjs +118 -0
  64. package/edition/develop-channel-evidence.mjs +23 -0
  65. package/edition/develop-channel-record.mjs +49 -0
  66. package/edition/develop-channel-satellites.mjs +65 -0
  67. package/edition/develop-chart-roster.mjs +96 -0
  68. package/edition/develop-chart-set.mjs +147 -0
  69. package/edition/develop-current-resolution.mjs +29 -0
  70. package/edition/develop-edition-invariants.mjs +142 -0
  71. package/edition/develop-edition-resolve.mjs +250 -0
  72. package/edition/develop-edition.mjs +332 -0
  73. package/edition/develop-engine-image.mjs +97 -0
  74. package/edition/develop-freeze.mjs +304 -0
  75. package/edition/develop-git-bundles.mjs +296 -0
  76. package/edition/develop-graph.mjs +34 -0
  77. package/edition/develop-image-coordinate.mjs +85 -0
  78. package/edition/develop-image-probe.mjs +122 -0
  79. package/edition/develop-image-record.mjs +260 -0
  80. package/edition/develop-resolve-report.mjs +159 -0
  81. package/edition/develop-select-records.mjs +90 -0
  82. package/edition/develop-selection-line.mjs +29 -0
  83. package/edition/develop-selection-pins.mjs +36 -0
  84. package/edition/develop-snapshot-inputs.mjs +112 -0
  85. package/edition/develop-store-root.mjs +20 -0
  86. package/edition/dispatched-runtime-components.mjs +172 -0
  87. package/edition/dist-check.mjs +1174 -0
  88. package/edition/distribution-evidence-set.mjs +1064 -0
  89. package/edition/distribution-paths.mjs +191 -0
  90. package/edition/edition-artifact.mjs +529 -0
  91. package/edition/edition-biome-manifests.mjs +85 -0
  92. package/edition/edition-ceiling.mjs +182 -0
  93. package/edition/edition-channel.mjs +27 -0
  94. package/edition/edition-chart-set.mjs +104 -0
  95. package/edition/edition-config.mjs +374 -0
  96. package/edition/edition-constants.mjs +110 -0
  97. package/edition/edition-contract-parse.mjs +179 -0
  98. package/edition/edition-contract.json +100 -0
  99. package/edition/edition-image-seal.mjs +115 -0
  100. package/edition/edition-kind.mjs +10 -0
  101. package/edition/edition-list.mjs +35 -0
  102. package/edition/edition-locks.mjs +78 -0
  103. package/edition/edition-pin-verbs.mjs +183 -0
  104. package/edition/edition-pin.mjs +17 -0
  105. package/edition/edition-pinned-inputs.mjs +116 -0
  106. package/edition/edition-protocol.mjs +285 -0
  107. package/edition/edition-publisher.mjs +21 -0
  108. package/edition/edition-seal.mjs +257 -0
  109. package/edition/edition-source-pins.mjs +211 -0
  110. package/edition/edition-unpin.mjs +16 -0
  111. package/edition/edition-workspace.mjs +205 -0
  112. package/edition/evidence-checkout.mjs +439 -0
  113. package/edition/evidence-grammars.mjs +18 -0
  114. package/edition/evidence-input-mode.mjs +136 -0
  115. package/edition/evidence-trust-pin.mjs +392 -0
  116. package/edition/evidence-trust-policy.mjs +374 -0
  117. package/edition/external-source-walk.mjs +486 -0
  118. package/edition/first-party-source-ids.mjs +62 -0
  119. package/edition/fleet-coordinate-finality.mjs +363 -0
  120. package/edition/fulcio-certificate-extensions.mjs +278 -0
  121. package/edition/git-json.mjs +54 -0
  122. package/edition/git-source-auth.mjs +131 -0
  123. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  124. package/edition/image-evidence-replay-state.mjs +242 -0
  125. package/edition/image-evidence.mjs +1328 -0
  126. package/edition/image-lock-service-rows.mjs +26 -0
  127. package/edition/image-subject-identity.mjs +19 -0
  128. package/edition/images-from-record.mjs +216 -0
  129. package/edition/indexes-at-pins.mjs +13 -0
  130. package/edition/infra-images-lock.mjs +105 -0
  131. package/edition/kernel-capability-domain.mjs +30 -0
  132. package/edition/kernel-contract-authority.mjs +526 -0
  133. package/edition/kernel-contract-source-path-set.mjs +96 -0
  134. package/edition/kernel-package.mjs +237 -0
  135. package/edition/kernel-pin-source.mjs +63 -0
  136. package/edition/kernel-protocol-choice.mjs +239 -0
  137. package/edition/kernel-signer-kind.mjs +23 -0
  138. package/edition/lib.mjs +3322 -0
  139. package/edition/locked-bundle-signer.mjs +297 -0
  140. package/edition/map-with-concurrency.mjs +19 -0
  141. package/edition/no-follow-file.mjs +94 -0
  142. package/edition/oci-evidence-verbs.mjs +421 -0
  143. package/edition/oci-image-source-evidence.mjs +377 -0
  144. package/edition/operation-grant-admission.mjs +285 -0
  145. package/edition/operation-requirements.mjs +282 -0
  146. package/edition/oras-process.mjs +35 -0
  147. package/edition/oras.mjs +664 -0
  148. package/edition/pin-source.mjs +242 -0
  149. package/edition/pinned-cosign.mjs +86 -0
  150. package/edition/plan-image-evidence-refresh.mjs +428 -0
  151. package/edition/platform-lock-readability.mjs +903 -0
  152. package/edition/platform-service-class.mjs +85 -0
  153. package/edition/platform-service-ownership.mjs +91 -0
  154. package/edition/platform-service-source-authority.mjs +105 -0
  155. package/edition/positive-closure.mjs +162 -0
  156. package/edition/previous-image-lock.mjs +25 -0
  157. package/edition/produce-distribution-completeness-evidence.mjs +317 -0
  158. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  159. package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
  160. package/edition/produce-image-source-evidence.mjs +788 -0
  161. package/edition/producer-evidence-bundle.mjs +201 -0
  162. package/edition/producer-evidence-index-file.mjs +4 -0
  163. package/edition/record-refused.mjs +11 -0
  164. package/edition/registry-client-factory.mjs +21 -0
  165. package/edition/registry-credentials.mjs +225 -0
  166. package/edition/release-catalogue.mjs +48 -0
  167. package/edition/release-channel-guard.mjs +108 -0
  168. package/edition/release-edition-artifact.mjs +152 -0
  169. package/edition/release-edition.mjs +54 -0
  170. package/edition/remedy-command.mjs +89 -0
  171. package/edition/repin-evidence-trust.mjs +109 -0
  172. package/edition/required-resource-provider.mjs +58 -0
  173. package/edition/resolve-chart-set.mjs +376 -0
  174. package/edition/resolve-customer-edition.mjs +374 -0
  175. package/edition/resolve-distribution.mjs +2642 -0
  176. package/edition/resolve-edition.mjs +192 -0
  177. package/edition/resolve-images.mjs +2344 -0
  178. package/edition/resolve-infra-images.mjs +88 -0
  179. package/edition/seal-image-digests.mjs +954 -0
  180. package/edition/sealed-distribution-lock.mjs +22 -0
  181. package/edition/sealed-evidence-accept.mjs +82 -0
  182. package/edition/shallow-checkout.mjs +18 -0
  183. package/edition/sigstore-bundle-media-type.mjs +4 -0
  184. package/edition/source-authority.mjs +713 -0
  185. package/edition/source-closure.mjs +115 -0
  186. package/edition/store-publisher-trust.mjs +40 -0
  187. package/edition/strict-json.mjs +121 -0
  188. package/edition/string-order.mjs +5 -0
  189. package/edition/third-party-admission.mjs +118 -0
  190. package/edition/third-party-bundle-registry.mjs +349 -0
  191. package/edition/third-party-certificate-extensions.mjs +58 -0
  192. package/edition/third-party-image-pins.mjs +41 -0
  193. package/edition/third-party-pins-trusted.mjs +24 -0
  194. package/edition/third-party-producer-evidence.mjs +76 -0
  195. package/edition/third-party-source-check.mjs +28 -0
  196. package/edition/third-party-source-context.mjs +41 -0
  197. package/edition/third-party-source-evidence.mjs +326 -0
  198. package/edition/third-party-source-flags.mjs +148 -0
  199. package/edition/third-party-source-pins.mjs +270 -0
  200. package/edition/third-party-source-uniqueness.mjs +54 -0
  201. package/edition/third-party-source-walk.mjs +118 -0
  202. package/edition/third-party-source-writer.mjs +228 -0
  203. package/edition/third-party-subjects.mjs +128 -0
  204. package/edition/trust-policy-arming-verb.mjs +60 -0
  205. package/edition/trust-policy-arming.mjs +144 -0
  206. package/edition/trust-policy-image-ownership.mjs +85 -0
  207. package/edition/trusted-executable.mjs +166 -0
  208. package/edition/verifier-binding.mjs +45 -0
  209. package/edition/verifier-digest-pin.mjs +17 -0
  210. package/edition/verifier-files.mjs +8 -0
  211. package/edition/verifier-kernel-schemas.mjs +22 -0
  212. package/edition/verifier-provenance.mjs +64 -0
  213. package/edition/verify-catalogue-evidence.mjs +247 -0
  214. package/edition/verify-image-evidence.mjs +493 -0
  215. package/edition/verify-image-platforms.mjs +523 -0
  216. package/edition/why.mjs +56 -0
  217. package/edition/workspace-sections.mjs +287 -0
  218. package/package.json +83 -4
  219. package/README.md +0 -3
@@ -0,0 +1,107 @@
1
+ // The committed build ledger (`build-ledger.json`): which commits of each source
2
+ // repository published images, and (schema v2) which `imageRef:contentHash`
3
+ // coordinates are published with which platforms.
4
+ //
5
+ // `node:` builtins plus the workspace locator only, so `source-authority.mjs`
6
+ // (which resolves from a bare checkout) can read it.
7
+ import { readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ import { dataRoot } from './edition-workspace.mjs';
11
+
12
+ export const BUILD_LEDGER_FILE = 'build-ledger.json';
13
+
14
+ export class BuildLedgerError extends Error {
15
+ constructor(message) {
16
+ super(`${BUILD_LEDGER_FILE} is malformed: ${message}`);
17
+ this.name = 'BuildLedgerError';
18
+ }
19
+ }
20
+
21
+ const SHA1_PATTERN = /^[0-9a-f]{40}$/u;
22
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
23
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
24
+ const PLATFORM_PATTERN = /^[a-z0-9]+\/[a-z0-9]+(\/[a-z0-9]+)?$/u;
25
+
26
+ const isRecord = (value) =>
27
+ value !== null && typeof value === 'object' && !Array.isArray(value);
28
+
29
+ function parseImages(repo, images) {
30
+ if (!isRecord(images)) {
31
+ throw new BuildLedgerError(`repositories["${repo}"].images must be an object`);
32
+ }
33
+ for (const [imageRef, image] of Object.entries(images)) {
34
+ const where = `repositories["${repo}"].images["${imageRef}"]`;
35
+ if (!isRecord(image) || !isRecord(image.contentHashes)) {
36
+ throw new BuildLedgerError(`${where}.contentHashes must be an object`);
37
+ }
38
+ for (const [hash, entry] of Object.entries(image.contentHashes)) {
39
+ const at = `${where}.contentHashes["${hash}"]`;
40
+ if (!SHA256_PATTERN.test(hash)) {
41
+ throw new BuildLedgerError(`${at}: key is not 64 lowercase hex characters`);
42
+ }
43
+ if (
44
+ !isRecord(entry) ||
45
+ !Array.isArray(entry.platforms) ||
46
+ entry.platforms.length === 0 ||
47
+ entry.platforms.some(
48
+ (platform) => typeof platform !== 'string' || !PLATFORM_PATTERN.test(platform),
49
+ ) ||
50
+ new Set(entry.platforms).size !== entry.platforms.length
51
+ ) {
52
+ throw new BuildLedgerError(
53
+ `${at}.platforms must be a non-empty list of distinct "os/arch" strings`,
54
+ );
55
+ }
56
+ if (typeof entry.digest !== 'string' || !DIGEST_PATTERN.test(entry.digest)) {
57
+ throw new BuildLedgerError(`${at}.digest must be "sha256:<64 hex>"`);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Parse a ledger document strictly. Returns `{ schemaVersion, repositories }`
65
+ * where each repository is `{ commits, images }`; `images` is null for v1.
66
+ */
67
+ export function parseBuildLedger(raw) {
68
+ if (
69
+ !isRecord(raw) ||
70
+ (raw.schemaVersion !== 1 && raw.schemaVersion !== 2) ||
71
+ !isRecord(raw.repositories)
72
+ ) {
73
+ throw new BuildLedgerError('expected schemaVersion 1 or 2 with a repositories object');
74
+ }
75
+ const repositories = {};
76
+ for (const [repo, value] of Object.entries(raw.repositories)) {
77
+ // v1 is removed once the data root is re-projected at v2.
78
+ if (raw.schemaVersion === 1) {
79
+ if (!Array.isArray(value)) {
80
+ throw new BuildLedgerError(`repositories["${repo}"] must be a commit list`);
81
+ }
82
+ repositories[repo] = { commits: value, images: null };
83
+ continue;
84
+ }
85
+ if (!isRecord(value) || !Array.isArray(value.commits)) {
86
+ throw new BuildLedgerError(`repositories["${repo}"].commits must be a list`);
87
+ }
88
+ if (value.commits.some((commit) => typeof commit !== 'string' || !SHA1_PATTERN.test(commit))) {
89
+ throw new BuildLedgerError(`repositories["${repo}"].commits holds a non-40-hex entry`);
90
+ }
91
+ parseImages(repo, value.images);
92
+ repositories[repo] = { commits: value.commits, images: value.images };
93
+ }
94
+ return { schemaVersion: raw.schemaVersion, repositories };
95
+ }
96
+
97
+ /** The parsed ledger of a data root, or null when none is projected. Malformed THROWS. */
98
+ export function readBuildLedger(distributionsDir = dataRoot()) {
99
+ let raw;
100
+ try {
101
+ raw = JSON.parse(readFileSync(join(distributionsDir, BUILD_LEDGER_FILE), 'utf8'));
102
+ } catch (error) {
103
+ if (error?.code === 'ENOENT') {return null;}
104
+ throw error;
105
+ }
106
+ return parseBuildLedger(raw);
107
+ }
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
+
6
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
7
+ import { platformServiceIndexPath , WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
8
+ import {
9
+ loadKernelDistributionSchemas,
10
+ readGitJsonAtCommit,
11
+ serializeJson,
12
+ sha256Hex,
13
+ } from './lib.mjs';
14
+ import {
15
+ comparablePlatformService,
16
+ declaredInstallationClass,
17
+ sourceDeclaredRequirements,
18
+ sourceOwnedCapabilityDomains,
19
+ } from './platform-service-class.mjs';
20
+ import {
21
+ platformServiceSourceDeclaration,
22
+ platformServiceSourceDeclarations,
23
+ platformServiceSourceRepositories,
24
+ } from './platform-service-source-authority.mjs';
25
+ import {
26
+ DistributionSourceMode,
27
+ committedRepositoryPins,
28
+ createPinnedSourceAuthority,
29
+ createSourceAuthority,
30
+ parseSourceModeArgs,
31
+ } from './source-authority.mjs';
32
+ import { readStrictJsonFile } from './strict-json.mjs';
33
+
34
+ if (isEntryModule(import.meta.url)) {
35
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
36
+ }
37
+
38
+ const TRUSTED_WRITE_FLAG = '--trusted-write';
39
+ const CHECK_FLAG = '--check';
40
+ const CHECK_SOURCES_FLAG = '--check-sources';
41
+ const UNPINNED_FLAG = '--unpinned';
42
+
43
+ /**
44
+ * THE LAST INDEX IN THIS PLANE THAT COULD NOT BE SCOPED, and the one that
45
+ * measurably fell behind because of it.
46
+ *
47
+ * `build-biome-index.mjs` and `build-runtime-image-source-index.mjs` could both
48
+ * be narrowed to one repository. This one could not: every run resolved all
49
+ * eight platform-service source repositories live. So advancing the two services
50
+ * sourced from `repos/xema-base` REQUIRED moving `event-hub-api`,
51
+ * `identity-api`, `llm-gateway-api` and `xema-host-web` with them — measured
52
+ * 2026-09-23, four collateral repositories that then disagreed with the runtime
53
+ * pins a scoped refresh had just held.
54
+ *
55
+ * It did not stay theoretical. Measured on `origin/main` the same day, the two
56
+ * committed indexes pinned `repos/xema-base` at commits 164 APART —
57
+ * `platform-service-index.json` at `e1987a11` (2026-09-17) against
58
+ * `runtime-image-source-index.json` at `fcde2a83` (2026-09-20) — because the
59
+ * pinnable index had been scope-refreshed and this one could only move by
60
+ * moving everything. Two committed artifacts, one repository, two commits.
61
+ *
62
+ * The fix is not a `--repo` flag here. A flag would have made this index a
63
+ * SECOND place that decides what commit a repository is at, which is the defect
64
+ * wearing a narrower costume. It reads the pins instead, like every other
65
+ * consumer, so a scoped refresh reaches it for free and there is nothing to
66
+ * keep in step.
67
+ */
68
+
69
+ function readJson(path) {
70
+ return readStrictJsonFile(path);
71
+ }
72
+
73
+ function git(repo, args) {
74
+ return execFileSync('git', ['-C', repo, ...args], {
75
+ encoding: 'utf8',
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ }).trim();
78
+ }
79
+
80
+ /**
81
+ * Every platform service the data root declares (`platformServiceSources`) — the
82
+ * platform floor. NOT filtered by tier: the kernel distribution-lock schema
83
+ * requires an exact source identity on EVERY `platformServices[]` entry, so an
84
+ * index that describes only one tier makes every edition unresolvable. It did:
85
+ * filtering to `infra` left `xema-kernel-server`, `docs-api`,
86
+ * `opencode-image-prepuller` and `xema-host-web` with no identity at all.
87
+ *
88
+ * The index is derived from the declarations alone: no edition is read, so a
89
+ * data root with any set of editions (or none) builds and checks it the same.
90
+ */
91
+ function loadPlatformServiceDeclarations() {
92
+ return platformServiceSourceDeclarations()
93
+ .slice()
94
+ .sort((a, b) => a.name.localeCompare(b.name));
95
+ }
96
+
97
+ function trustedSourceAuthority(sourceMode, unpinned) {
98
+ return unpinned
99
+ ? createSourceAuthority({
100
+ mode: sourceMode,
101
+ knownRepositories: platformServiceSourceRepositories(),
102
+ })
103
+ : // No delegate: this artifact decides no commits, so it must not be able
104
+ // to ask for one. A platform-service repository with no committed pin
105
+ // refuses by name rather than resolving to its tip.
106
+ createPinnedSourceAuthority({ pins: committedRepositoryPins() });
107
+ }
108
+
109
+ function buildTrustedIndex(sourceAuthority) {
110
+ const declarations = loadPlatformServiceDeclarations();
111
+ const services = [];
112
+ for (const declaration of declarations) {
113
+ const binding = platformServiceSourceDeclaration(declaration.name);
114
+ const repoDirectory = sourcePath(binding.repo);
115
+ // Read through the one explicit source authority. Published mode names a
116
+ // fetchable origin/main commit; workspace mode names a deterministic local
117
+ // snapshot commit containing tracked and untracked non-ignored files.
118
+ const before = sourceAuthority.commitForRepository(binding.repo);
119
+ const sourceManifest = readGitJsonAtCommit(
120
+ repoDirectory,
121
+ binding.manifestPath,
122
+ before,
123
+ );
124
+ if (sourceManifest.name !== binding.manifestName) {
125
+ throw new Error(
126
+ `${binding.path} at ${before} declares name "${String(sourceManifest.name)}", ` +
127
+ `but platform service "${declaration.name}" is declared to be sourced from ` +
128
+ `"${binding.manifestName}"`,
129
+ );
130
+ }
131
+ if (
132
+ typeof sourceManifest.version !== 'string' ||
133
+ sourceManifest.version.length === 0
134
+ ) {
135
+ throw new Error(
136
+ `${binding.path} has no exact version at ${before}`,
137
+ );
138
+ }
139
+ const repositoryUrl = canonicalPublicHttpsRepositoryUrl(
140
+ git(repoDirectory, ['remote', 'get-url', 'origin']),
141
+ `${binding.repo} origin`,
142
+ );
143
+ services.push({
144
+ name: declaration.name,
145
+ tier: declaration.tier,
146
+ ...declaredInstallationClass(declaration),
147
+ ...sourceOwnedCapabilityDomains(sourceManifest, `${binding.path} at ${before}`),
148
+ ...sourceDeclaredRequirements(sourceManifest, `${binding.path} at ${before}`),
149
+ repositoryUrl,
150
+ repo: binding.repo,
151
+ path: binding.path,
152
+ sourceCommit: before,
153
+ version: sourceManifest.version,
154
+ sourceManifestHash: sha256Hex(serializeJson(sourceManifest)),
155
+ imageKey: binding.imageKey,
156
+ });
157
+ }
158
+ return { schemaVersion: 1, services };
159
+ }
160
+
161
+ function validateOfflineIndex(index) {
162
+ const declarations = loadPlatformServiceDeclarations();
163
+ const expected = declarations.map(comparablePlatformService);
164
+ const actual = index.services.map(comparablePlatformService);
165
+ if (serializeJson(actual) !== serializeJson(expected)) {
166
+ throw new Error(
167
+ 'platform-service-index.json does not exactly match the platformServiceSources declared in edition-workspace.json',
168
+ );
169
+ }
170
+ for (const service of index.services) {
171
+ const canonicalRepositoryUrl = canonicalPublicHttpsRepositoryUrl(
172
+ service.repositoryUrl,
173
+ `platform service "${service.name}" repositoryUrl`,
174
+ );
175
+ if (canonicalRepositoryUrl !== service.repositoryUrl) {
176
+ throw new Error(
177
+ `platform service "${service.name}" repositoryUrl is not canonical`,
178
+ );
179
+ }
180
+ const expectedRepoName = service.repo.split('/').at(-1);
181
+ // Offline, and still exact: the declared source location is the authority,
182
+ // so a committed index that names a different repo, manifest or image key
183
+ // than the declaration fails here without touching Git.
184
+ const binding = platformServiceSourceDeclaration(service.name);
185
+ if (service.repo !== binding.repo || service.path !== binding.path) {
186
+ throw new Error(
187
+ `platform service "${service.name}" source ${service.repo}:${service.path} ` +
188
+ `does not match its declared source ${binding.repo}:${binding.path}`,
189
+ );
190
+ }
191
+ if (service.imageKey !== binding.imageKey) {
192
+ throw new Error(
193
+ `platform service "${service.name}" imageKey "${service.imageKey}" ` +
194
+ `does not match its declared image key "${binding.imageKey}"`,
195
+ );
196
+ }
197
+ if (!service.path.startsWith(`${service.repo}/`)) {
198
+ throw new Error(
199
+ `platform service "${service.name}" source path is outside its repo`,
200
+ );
201
+ }
202
+ if (
203
+ !service.repositoryUrl.endsWith(`/${expectedRepoName}.git`) &&
204
+ !service.repositoryUrl.endsWith(`/${expectedRepoName}`)
205
+ ) {
206
+ throw new Error(
207
+ `platform service "${service.name}" repositoryUrl does not identify ${service.repo}`,
208
+ );
209
+ }
210
+ }
211
+ return index;
212
+ }
213
+
214
+ /**
215
+ * The index as `--trusted-write` produces and validates it, over an explicit
216
+ * source authority. The develop edition builds its snapshot index here, with a
217
+ * pinned authority whose pins are the snapshot's, so the release and the
218
+ * develop lane share one builder and one validation.
219
+ */
220
+ export function buildValidatedPlatformServiceIndex(schemas, sourceAuthority) {
221
+ const parsed = schemas.parsePlatformServiceSourceIndex(
222
+ buildTrustedIndex(sourceAuthority),
223
+ );
224
+ return validateOfflineIndex(parsed);
225
+ }
226
+
227
+ async function main() {
228
+ const { mode, remaining } = parseSourceModeArgs(process.argv.slice(2));
229
+ const flags = new Set(remaining);
230
+ const unpinnedFlag = flags.delete(UNPINNED_FLAG);
231
+ const unknown = [...flags].filter(
232
+ (flag) =>
233
+ flag !== TRUSTED_WRITE_FLAG &&
234
+ flag !== CHECK_FLAG &&
235
+ flag !== CHECK_SOURCES_FLAG,
236
+ );
237
+ if (unknown.length > 0 || flags.size !== 1) {
238
+ throw new Error(
239
+ `usage: node build-platform-service-index.mjs (${CHECK_FLAG} | ${CHECK_SOURCES_FLAG} | ${TRUSTED_WRITE_FLAG}) [--source published|workspace] [${UNPINNED_FLAG}]`,
240
+ );
241
+ }
242
+ // A workspace snapshot has no committed pin by construction, so it IMPLIES
243
+ // the live authority rather than requiring the operator to state both.
244
+ const unpinned = unpinnedFlag || mode === DistributionSourceMode.Workspace;
245
+ const schemas = await loadKernelDistributionSchemas();
246
+ if (flags.has(CHECK_FLAG)) {
247
+ if (!existsSync(platformServiceIndexPath())) {
248
+ throw new Error(`${platformServiceIndexPath()} does not exist`);
249
+ }
250
+ const parsed = schemas.parsePlatformServiceSourceIndex(
251
+ readJson(platformServiceIndexPath()),
252
+ );
253
+ validateOfflineIndex(parsed);
254
+ console.log(
255
+ `[platform-service-index] offline check passed for ${parsed.services.length} services`,
256
+ );
257
+ return;
258
+ }
259
+ const parsed = buildValidatedPlatformServiceIndex(schemas, trustedSourceAuthority(mode, unpinned));
260
+ if (flags.has(CHECK_SOURCES_FLAG)) {
261
+ if (!existsSync(platformServiceIndexPath())) {
262
+ throw new Error(`${platformServiceIndexPath()} does not exist`);
263
+ }
264
+ const committed = readFileSync(platformServiceIndexPath(), 'utf8');
265
+ if (committed !== serializeJson(parsed)) {
266
+ throw new Error(
267
+ `platform-service-index.json does not match the exact trusted clean sources ` +
268
+ `(authority: ${
269
+ unpinned
270
+ ? 'UNPINNED — every repository resolved LIVE, so this names the ' +
271
+ 'fleet as it is now and regenerating will move pins'
272
+ : 'PINNED — the committed repositoryPins, no live resolution'
273
+ }).`,
274
+ );
275
+ }
276
+ console.log(
277
+ `[platform-service-index] trusted source check passed for ${parsed.services.length} services`,
278
+ );
279
+ return;
280
+ }
281
+ writeFileSync(platformServiceIndexPath(), serializeJson(parsed));
282
+ console.log(
283
+ `[platform-service-index] wrote ${platformServiceIndexPath()} from ${parsed.services.length} clean trusted sources`,
284
+ );
285
+ }
286
+
287
+ if (isEntryModule(import.meta.url)) {
288
+ main().catch((error) => {
289
+ console.error(`[platform-service-index] ${error.message}`);
290
+ process.exitCode = 1;
291
+ });
292
+ }