@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,1197 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
5
+ import { isAbsolute, join } from 'node:path';
6
+
7
+ import { buildValidatedPlatformServiceIndex } from './build-platform-service-index.mjs';
8
+ import { canonicalPublicHttpsRepositoryUrl, sameRepository } from './canonical-repository-url.mjs';
9
+ import { connectorCrossRepoBuildInputs } from './connector-source-authority.mjs';
10
+ import { EditionChannel } from './edition-channel.mjs';
11
+ import { editionWorkspaceConfig, workspaceSection } from './edition-config.mjs';
12
+ import { biomeIndexPath, dataRoot, platformServiceIndexPath, runtimeImageSourceIndexPath, WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
13
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
14
+ import { firstPartySources } from './first-party-source-ids.mjs';
15
+ import {
16
+ buildTrustedKernelContractAuthority,
17
+ KERNEL_COMMIT_FLAG,
18
+ describeKernelContractAuthorityDrift,
19
+ validateKernelContractAuthorityFromGit,
20
+ } from './kernel-contract-authority.mjs';
21
+ import {
22
+ buildBiomeIndex,
23
+ compareStrings,
24
+ distributionBiomeSourceRepositories,
25
+ loadKernelDistributionSchemas,
26
+ readGitJsonAtCommit,
27
+ serializeJson,
28
+ sha256Hex,
29
+ } from './lib.mjs';
30
+ import { createRegistryClientResolver } from './oci-image-source-evidence.mjs';
31
+ import { disposeRegistrySessions } from './oras.mjs';
32
+ import {
33
+ platformServiceRuntimeBindings,
34
+ } from './platform-service-source-authority.mjs';
35
+ import {
36
+ createExactCommitSourceAuthority,
37
+ createPinnedSourceAuthority,
38
+ createSourceAuthority,
39
+ parseSourceModeArgs,
40
+ } from './source-authority.mjs';
41
+ import { readStrictJsonFile } from './strict-json.mjs';
42
+ import { checkThirdPartySourcePins } from './third-party-source-check.mjs';
43
+ import { loadThirdPartySourceContext } from './third-party-source-context.mjs';
44
+ import {
45
+ CHANNEL_FLAG,
46
+ CHECK_THIRD_PARTY_FLAG,
47
+ THIRD_PARTY_FLAG,
48
+ THIRD_PARTY_WRITER_USAGE,
49
+ takeThirdPartyFlags,
50
+ } from './third-party-source-flags.mjs';
51
+ import { commitThirdPartySourceWrite, planThirdPartySourceWrite } from './third-party-source-writer.mjs';
52
+
53
+ if (isEntryModule(import.meta.url)) {
54
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
55
+ }
56
+
57
+ const runtimeImageSources = () =>
58
+ workspaceSection('runtimeImageSources', 'building or validating the runtime image source index');
59
+
60
+ const sourceDeclarations = () => runtimeImageSources().supplementalSources;
61
+
62
+ /**
63
+ * Content a producing repository copies OUT OF ANOTHER repository into its build
64
+ * context before the image build.
65
+ *
66
+ * This is the ONLY image build input that is not derivable from the consuming
67
+ * repository at its own commit — nothing inside `xema-base@<sha>` names the
68
+ * bytes of `xema-cultivars/biomes/platform/mailops`. Every other input (the
69
+ * resolved Dockerfile, its `COPY` sources, the `workspace:` graph) is read
70
+ * straight out of the consuming commit, so this list is the whole hand-declared
71
+ * surface and stays small by construction.
72
+ *
73
+ * The list is the data root's (`runtimeImageSources.crossRepoBuildInputs`), plus
74
+ * one input per production connector derived from `connectorSources`; the
75
+ * history of why each entry is there lives with the data. Each entry pins the vendored bytes to the SOURCE repository's own
76
+ * `repositoryPins` commit — the revision this distribution has resolved, and
77
+ * therefore the revision the producing build must clone.
78
+ *
79
+ * CLOSED (was a KNOWN GAP): the producing workflows used to clone with
80
+ * `git clone --depth 1` and no ref — the source repository's default-branch HEAD
81
+ * at build time — so a built image was not a function of any commit pair and its
82
+ * published tag could describe bytes it did not contain. They now vendor through
83
+ * `tooling/release/vendor-cross-repo-inputs.mjs`, driven by a per-repository
84
+ * record the aggregator generates FROM THIS TABLE joined to `repositoryPins`
85
+ * (`tooling/release/generate-cross-repo-build-inputs.mjs`). The same record
86
+ * supplies `compute-image-tag.mjs`'s `--vendored` pins, so the bytes in the image
87
+ * and the bytes in the hash come from one place. The gate
88
+ * `cross-repo-build-inputs` fails on a stale record, and on a workflow that
89
+ * hand-rolls a clone or a copy instead of reading it.
90
+ *
91
+ * Consequence worth knowing: a change to a source repository's vendored content
92
+ * does NOT reach a consumer's image until this index is regenerated and the
93
+ * emitted record re-committed. That is the point — the vendored revision is
94
+ * declared, not whatever HEAD happened to be.
95
+ */
96
+ export const crossRepoBuildInputs = () => [
97
+ ...runtimeImageSources().crossRepoBuildInputs,
98
+ ...connectorCrossRepoBuildInputs(),
99
+ ];
100
+
101
+ /**
102
+ * Context directories a build step creates, which exist in no commit. Declared
103
+ * (`runtimeImageSources.buildStepContextDirectories`) so a Dockerfile COPY of
104
+ * one is a REVIEWED absence rather than a silent miss.
105
+ */
106
+ const buildStepContextDirectoriesDeclared = () => runtimeImageSources().buildStepContextDirectories;
107
+
108
+ /**
109
+ * The logical-platform-service → actual-runtime-subject bindings that cannot be
110
+ * inferred. DERIVED from `platform-service-source-authority.mjs`, which is the
111
+ * one place the partition — own repository-root image vs. carried by another
112
+ * subject — is declared. It used to be a second copy here, and the two could
113
+ * only agree by hand.
114
+ */
115
+
116
+ function readJson(path) {
117
+ return readStrictJsonFile(path);
118
+ }
119
+
120
+ function git(repo, args) {
121
+ return execFileSync('git', ['-C', repo, ...args], {
122
+ encoding: 'utf8',
123
+ stdio: ['ignore', 'pipe', 'pipe'],
124
+ }).trim();
125
+ }
126
+
127
+ function declaredSourceRepositories() {
128
+ return [
129
+ ...new Set([
130
+ ...readJson(biomeIndexPath()).map(({ repo }) => repo),
131
+ ...readJson(
132
+ join(dataRoot(), 'platform-service-index.json'),
133
+ ).services.map(({ repo }) => repo),
134
+ ...sourceDeclarations().map(({ repo }) => repo),
135
+ ...crossRepoBuildInputs().flatMap(({ consumerRepo, sourceRepo }) => [
136
+ consumerRepo,
137
+ sourceRepo,
138
+ ]),
139
+ ]),
140
+ ];
141
+ }
142
+
143
+ /**
144
+ * One repository, BOTH provenance axes, in ONE record.
145
+ *
146
+ * `sourceCommit` is the IMAGE axis and its meaning is unchanged: every image
147
+ * subject from this repository must carry exactly this commit, and
148
+ * `expectedProducerSource` throws when one does not. It is therefore
149
+ * constrained to commits with published image evidence.
150
+ *
151
+ * `vendoredSourceCommit` is the SOURCE axis — where vendored bytes come from.
152
+ * It is the published tip, because a vendored path needs its content to exist
153
+ * at a published immutable commit and does NOT need an image to have been
154
+ * built there. See `publishedSourceTip` in `source-authority.mjs` for the
155
+ * measured production failure that fusing the two produced.
156
+ *
157
+ * They are emitted together, from one resolution, deliberately: the release has
158
+ * ONE authority that answers both questions about a component, never two
159
+ * competing ones that can disagree about which commit a component is at.
160
+ */
161
+ export function repositoryIdentity(repo, sourceAuthority) {
162
+ const repoDirectory = sourcePath(repo);
163
+ const sourceCommit = sourceAuthority.commitForRepository(repo);
164
+ const vendoredSourceCommit =
165
+ sourceAuthority.vendoredSourceCommitForRepository(repo);
166
+ const repositoryUrl = canonicalPublicHttpsRepositoryUrl(
167
+ git(repoDirectory, ['remote', 'get-url', 'origin']),
168
+ `${repo} origin`,
169
+ );
170
+ return { repo, repositoryUrl, sourceCommit, vendoredSourceCommit };
171
+ }
172
+
173
+ /**
174
+ * Refuse to WRITE an index whose declared vendored source paths do not exist at
175
+ * the commit whose bytes the build will actually fetch.
176
+ *
177
+ * This assertion landed on `main` in `0dfbba7` and was removed by `a416f1e`
178
+ * ("Revert the develop merge on main — the CONSUMER landed before its
179
+ * PRODUCER") as COLLATERAL of reverting an unrelated cross-repo ordering
180
+ * failure — 93 lines of this file among 62 reverted files. It is re-landed here
181
+ * on its own merits, and on the SOURCE axis.
182
+ *
183
+ * Nothing reads the declared cross-repo build inputs at declaration time, so a path that
184
+ * has moved fails SILENTLY where it is written and LOUDLY somewhere else
185
+ * entirely: the consuming image build dies at "Fetch pinned cross-repo build
186
+ * inputs", in another repository, for a declaration nobody was editing. The
187
+ * defect belongs where it is authored, and an index that cannot be written is
188
+ * strictly better than one that can be written and cannot be built from.
189
+ */
190
+ function assertVendoredSourcesResolve(crossRepoBuildInputs, resolveIdentity) {
191
+ const unresolved = [];
192
+ for (const input of crossRepoBuildInputs) {
193
+ // The SOURCE axis, not the image pin. A vendored path is a fact about the
194
+ // commit whose BYTES are vendored, and `vendor-cross-repo-inputs.mjs`
195
+ // fetches exactly that commit — so asking the image-evidence pin whether a
196
+ // source path exists answers about a commit the build never reads.
197
+ const { vendoredSourceCommit } = resolveIdentity(input.sourceRepo);
198
+ const found = git(sourcePath(input.sourceRepo), [
199
+ 'ls-tree',
200
+ '--name-only',
201
+ vendoredSourceCommit,
202
+ '--',
203
+ input.sourcePath,
204
+ ]);
205
+ if (found.length === 0) {
206
+ unresolved.push(
207
+ `${input.sourceRepo}@${vendoredSourceCommit}:${input.sourcePath} does not ` +
208
+ `exist (vendored into ${input.consumerRepo}` +
209
+ `${input.consumerService ? ` service "${input.consumerService}"` : ''} ` +
210
+ `at ${input.destinationPath})`,
211
+ );
212
+ }
213
+ }
214
+ if (unresolved.length > 0) {
215
+ throw new Error(
216
+ `the declared crossRepoBuildInputs name ${unresolved.length} source path(s) that do ` +
217
+ `not exist at the vendored source commit this index pins. A vendored path ` +
218
+ `is a fact about that PINNED commit, not about anybody's working tree or ` +
219
+ `develop branch — fix the declaration, or pin a commit that carries it:` +
220
+ `\n ${unresolved.join('\n ')}`,
221
+ );
222
+ }
223
+ }
224
+
225
+ const CURRENT_INDEX_SCHEMA_VERSION = 2;
226
+
227
+ /**
228
+ * The kernel-contract authority for a pin regeneration.
229
+ *
230
+ * ── WHY THIS IS NOT `buildTrustedKernelContractAuthority` ──
231
+ *
232
+ * This function regenerates `repositoryPins` — a PROJECTION of immutable
233
+ * remote facts. Every one of those pins is read from
234
+ * `refs/remotes/origin/main` of its repository, which is exactly right: a
235
+ * production composition must resolve from durable, published state.
236
+ *
237
+ * The kernel-contract authority was the ONE exception, and it was the wrong
238
+ * kind of exception. `buildTrustedKernelContractAuthority` takes its commit
239
+ * from the LOCAL HEAD of `repos/xema-kernel-contracts` and requires that
240
+ * checkout to be EXACTLY CLEAN — so regenerating thirteen pins that have
241
+ * nothing to do with the kernel contract failed outright whenever somebody
242
+ * had an uncommitted file in that one repository. Measured 2026-09-14: ten of
243
+ * twenty-eight repositories in a shared checkout held another agent's
244
+ * in-flight work, and the pins could not be regenerated at all.
245
+ *
246
+ * That is a development-state fact deciding a release-composition question,
247
+ * and it is the boundary this change moves. Whether a colleague's working
248
+ * tree is dirty must never determine whether we can resolve what production
249
+ * ships.
250
+ *
251
+ * ── WHAT IS PRESERVED, AND WHY IT IS NOT WEAKER ──
252
+ *
253
+ * The authority is read from the COMMITTED index and re-verified from git AT
254
+ * ITS OWN PINNED COMMIT: `validateKernelContractAuthorityFromGit` recomputes
255
+ * the content digest from `ls-tree` + `cat-file` blobs at `sourceCommit` and
256
+ * refuses anything that does not match byte for byte. No worktree file is
257
+ * read on that path — it never was — so the provenance guarantee is identical
258
+ * and the clean-checkout assertion was buying nothing for it.
259
+ *
260
+ * `requireHeadMatch: false` because MOVING the authority is a different
261
+ * operation with its own tool: `xema-edition index runtime-image-source
262
+ * --repin-contract-authority`, which
263
+ * legitimately demands a deliberate clean checkout because it is CHOOSING a
264
+ * new commit. Re-deriving it here made pin regeneration silently re-pin the
265
+ * contract as a side effect — two operations behind one command, and the one
266
+ * nobody asked for is the one that took the lock.
267
+ *
268
+ * BOOTSTRAP is the single case with no committed authority to preserve: it
269
+ * CHOOSES a commit exactly as the re-pin does, so it obeys the re-pin's one
270
+ * rule — the caller states the commit (`--kernel-commit`) and the checkout's
271
+ * HEAD must equal it. Stating a commit when a committed authority exists is
272
+ * refused: it would be read by nobody.
273
+ *
274
+ * A committed index of any schemaVersion but the current one is refused by name,
275
+ * never migrated: a schemaVersion 1 index is re-indexed by deleting it and
276
+ * running bootstrap. Any other
277
+ * schemaVersion is refused by name, never read as one of these.
278
+ */
279
+ async function resolveContractAuthority(schemas, kernelRoot, kernelCommit) {
280
+ const repositoryDirectory = kernelRoot ?? undefined;
281
+ const committed = existsSync(runtimeImageSourceIndexPath())
282
+ ? readJson(runtimeImageSourceIndexPath())
283
+ : undefined;
284
+ if (committed !== undefined && committed.schemaVersion !== CURRENT_INDEX_SCHEMA_VERSION) {
285
+ throw new Error(
286
+ `${runtimeImageSourceIndexPath()} has schemaVersion ${JSON.stringify(committed.schemaVersion)}; ` +
287
+ `this command reads only schemaVersion ${CURRENT_INDEX_SCHEMA_VERSION} and does not migrate older indexes: ` +
288
+ `delete the index and re-run with ${KERNEL_COMMIT_FLAG} <40-hex> to bootstrap a new one`,
289
+ );
290
+ }
291
+ if (committed === undefined || !committed.contractAuthority) {
292
+ return buildTrustedKernelContractAuthority({
293
+ ...(repositoryDirectory === undefined ? {} : { repositoryDirectory }),
294
+ schemas,
295
+ kernelCommit,
296
+ });
297
+ }
298
+ if (kernelCommit !== undefined) {
299
+ throw new Error(
300
+ `${KERNEL_COMMIT_FLAG} states the commit of a NEW contract authority, and the committed index already pins ` +
301
+ `${committed.contractAuthority.sourceCommit}: this command preserves it. Move it with ${REPIN_FLAG} ${KERNEL_COMMIT_FLAG} <40-hex>.`,
302
+ );
303
+ }
304
+ return validateKernelContractAuthorityFromGit({
305
+ authority: committed.contractAuthority,
306
+ ...(repositoryDirectory === undefined ? {} : { repositoryDirectory }),
307
+ schemas,
308
+ requireHeadMatch: false,
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Re-resolve ONE repository pin and PRESERVE every other one byte for byte.
314
+ *
315
+ * ── WHY A WHOLE-INDEX REBUILD IS THE WRONG TOOL FOR A ONE-REPOSITORY FIX ──
316
+ *
317
+ * `--trusted-write` re-resolves all thirteen pins against each repository's
318
+ * `refs/remotes/origin/main`. That is exactly right when the question is "what
319
+ * does the fleet look like now", and exactly wrong when the question is "ship
320
+ * the fix that is already built in ONE repository": every other pin moves to
321
+ * whatever that repository has published since, and every subject whose
322
+ * identity depends on a moved pin is recomputed. Measured 2026-09-18 on this
323
+ * index, a full rebuild moves 5 of 13 pins and recomputes 54 of 62 subjects,
324
+ * 53 of them collateral — so a one-line fix arrives as a fleet-wide re-pin
325
+ * nobody asked for, and every one of those 53 subjects is a coordinate the
326
+ * registry must already be serving or delivery fails.
327
+ *
328
+ * The scope is a CHOICE somebody states, and it narrows: `--repo <repository>`
329
+ * says "this repository has moved; nothing else has". Every pin outside it is
330
+ * taken verbatim from the committed index, so it cannot drift by accident, and
331
+ * every subject derived from an unmoved pin is recomputed at the SAME commit
332
+ * and therefore lands byte-identical. Only the subjects whose identity actually
333
+ * depends on the named repository change.
334
+ *
335
+ * It is deliberately NOT a separate code path: the scope seeds the memo map
336
+ * `getRepositoryIdentity` already uses, so the index is produced by the one
337
+ * builder, passes the one validator, and is byte-comparable with a full
338
+ * rebuild's output for the parts that did not move. A second writer for
339
+ * "narrow" indexes would be a second answer to what an index IS.
340
+ *
341
+ * A repository this index does not source is a REFUSAL, never a silent no-op —
342
+ * a typo that quietly rewrote nothing would read in review as "the refresh ran".
343
+ *
344
+ * A declared source repository with NO committed pin JOINS the index through the
345
+ * same scope: every committed pin is preserved verbatim and only the named
346
+ * repository is resolved (at `--at`, which must be published). It needs no
347
+ * full `--trusted-write`, which would re-derive and drift every other pin.
348
+ */
349
+ export function preserveOtherRepositoryPins(
350
+ repoScope,
351
+ committedIndex,
352
+ declaredRepositories,
353
+ ) {
354
+ const declared = new Set(declaredRepositories);
355
+ if (!declared.has(repoScope)) {
356
+ throw new Error(
357
+ `--repo "${repoScope}" is not a source repository of this index. ` +
358
+ `Known repositories: ${[...declared].sort(compareStrings).join(', ')}`,
359
+ );
360
+ }
361
+ const pins = Array.isArray(committedIndex?.repositoryPins)
362
+ ? committedIndex.repositoryPins
363
+ : [];
364
+ if (pins.length === 0) {
365
+ throw new Error(
366
+ 'the committed runtime image source index declares no repositoryPins to preserve',
367
+ );
368
+ }
369
+ return pins
370
+ .filter((pin) => pin.repo !== repoScope)
371
+ .map((pin) => ({
372
+ repo: pin.repo,
373
+ repositoryUrl: pin.repositoryUrl,
374
+ sourceCommit: pin.sourceCommit,
375
+ vendoredSourceCommit: pin.vendoredSourceCommit,
376
+ }));
377
+ }
378
+
379
+ function committedRepositoryPinsToPreserve(repoScope) {
380
+ if (!existsSync(runtimeImageSourceIndexPath())) {
381
+ throw new Error(
382
+ `--repo requires ${runtimeImageSourceIndexPath()} to exist: there is no ` +
383
+ `committed index whose other pins could be preserved. Bootstrap with ` +
384
+ `--trusted-write and no --repo.`,
385
+ );
386
+ }
387
+ return preserveOtherRepositoryPins(
388
+ repoScope,
389
+ readJson(runtimeImageSourceIndexPath()),
390
+ declaredSourceRepositories(),
391
+ );
392
+ }
393
+
394
+ /**
395
+ * `producerWorkflowPins` are operator-stated TRUST DATA: which commit of a
396
+ * producer repository's workflow the verifier believes. Nothing is derived (not
397
+ * from `repositoryPins`, not from a policy row); the committed pins are kept
398
+ * exactly, and `--producer-workflow-pin <owner/repo>=<40-hex>` sets or updates
399
+ * one. A bootstrap index with none committed writes `[]`, which the kernel allows.
400
+ */
401
+ export function mergeProducerWorkflowPins(committedPins, statedPins) {
402
+ const merged = new Map(
403
+ (Array.isArray(committedPins) ? committedPins : []).map((pin) => [
404
+ pin.repository,
405
+ { repository: pin.repository, workflowCommit: pin.workflowCommit },
406
+ ]),
407
+ );
408
+ for (const pin of statedPins) {
409
+ merged.set(pin.repository, { repository: pin.repository, workflowCommit: pin.workflowCommit });
410
+ }
411
+ return [...merged.values()].sort((left, right) => compareStrings(left.repository, right.repository));
412
+ }
413
+
414
+ /**
415
+ * The committed index with one producer workflow pin merged in by `mergeProducerWorkflowPins`: what the third-party
416
+ * writer adds (`{repository, workflowCommit: sourceCommit}`). Every repository pin and subject is kept exactly: a
417
+ * third-party repository is not an index repository, so nothing about it can be re-resolved.
418
+ */
419
+ export function indexWithProducerWorkflowPin({ committedIndex, pin }) {
420
+ return {
421
+ ...committedIndex,
422
+ producerWorkflowPins: mergeProducerWorkflowPins(committedIndex.producerWorkflowPins, [pin]),
423
+ };
424
+ }
425
+
426
+ const PRODUCER_WORKFLOW_PIN_FLAG = '--producer-workflow-pin';
427
+
428
+ /**
429
+ * Every `--producer-workflow-pin <owner/repo>=<40-hex>` of the argv. A malformed
430
+ * value, or a repository stated twice, is refused here; the repository and
431
+ * commit grammar is the kernel's, applied by the index parse.
432
+ */
433
+ export function parseProducerWorkflowPinFlags(argv) {
434
+ const pins = [];
435
+ const remaining = [];
436
+ for (let index = 0; index < argv.length; index += 1) {
437
+ if (argv[index] !== PRODUCER_WORKFLOW_PIN_FLAG) {
438
+ remaining.push(argv[index]);
439
+ continue;
440
+ }
441
+ const value = argv[index + 1];
442
+ const separator = value?.indexOf('=') ?? -1;
443
+ if (separator < 1 || value.startsWith('--')) {
444
+ throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} requires <owner/repo>=<40-hex-commit>`);
445
+ }
446
+ const repository = value.slice(0, separator);
447
+ const workflowCommit = value.slice(separator + 1);
448
+ if (pins.some((pin) => pin.repository === repository)) {
449
+ throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} names ${repository} more than once`);
450
+ }
451
+ if (!COMMIT_PATTERN.test(workflowCommit)) {
452
+ throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} ${repository}: the commit must be a full 40-hex sha`);
453
+ }
454
+ pins.push({ repository, workflowCommit });
455
+ index += 1;
456
+ }
457
+ return { producerWorkflowPins: pins, remaining };
458
+ }
459
+
460
+ /**
461
+ * A `--producer-workflow-pin` is trust data for the index `--trusted-write`
462
+ * writes, and only there. Every other mode writes nothing, so a pin stated to
463
+ * one would be silently ignored; `--check-sources` included: it grades the
464
+ * committed index against the sources, and a pin is not a source.
465
+ */
466
+ export function assertProducerWorkflowPinsWritten(producerWorkflowPins, flag, usage) {
467
+ if (producerWorkflowPins.length > 0 && flag !== '--trusted-write') {
468
+ throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} states trust data for a written index: ${flag} writes none\n${usage}`);
469
+ }
470
+ }
471
+
472
+ /**
473
+ * `biomeIndex` and `platformServiceIndex` are the indexes this one is built
474
+ * over, INJECTED: the release CLI passes the committed ones read from disk
475
+ * (byte-identical to reading them here), a develop snapshot passes the ones it
476
+ * built at its own pins.
477
+ */
478
+ export async function buildTrustedIndex(
479
+ schemas,
480
+ sourceAuthority,
481
+ kernelRoot,
482
+ repoScope,
483
+ { biomeIndex, platformServiceIndex, kernelCommit, producerWorkflowPins = [], contractAuthority: statedAuthority },
484
+ ) {
485
+ const contractAuthority = statedAuthority ?? (await resolveContractAuthority(schemas, kernelRoot, kernelCommit));
486
+ const repositoryIdentities = new Map();
487
+ if (repoScope !== undefined) {
488
+ for (const pin of committedRepositoryPinsToPreserve(repoScope)) {
489
+ repositoryIdentities.set(pin.repo, pin);
490
+ }
491
+ }
492
+ const getRepositoryIdentity = (repo) => {
493
+ let identity = repositoryIdentities.get(repo);
494
+ if (!identity) {
495
+ if (repoScope !== undefined && repo !== repoScope) {
496
+ throw new Error(
497
+ `source repository "${repo}" has no committed pin and is not the --repo scope ` +
498
+ `"${repoScope}". A scoped write adds one repository; resolving "${repo}" live ` +
499
+ `would re-pin a repository nobody asked about. Add it with its own --repo ${repo} --at <sha>.`,
500
+ );
501
+ }
502
+ identity = repositoryIdentity(repo, sourceAuthority);
503
+ repositoryIdentities.set(repo, identity);
504
+ }
505
+ return identity;
506
+ };
507
+ const supplementalSubjects = sourceDeclarations().map((declaration) => {
508
+ const repoDirectory = sourcePath(declaration.repo);
509
+ const { sourceCommit, repositoryUrl } = getRepositoryIdentity(
510
+ declaration.repo,
511
+ );
512
+ const sourceManifestJson = readGitJsonAtCommit(
513
+ repoDirectory,
514
+ declaration.sourceManifestPath,
515
+ sourceCommit,
516
+ );
517
+ return {
518
+ kind: declaration.kind,
519
+ id: declaration.id,
520
+ imageRef: `${editionWorkspaceConfig().imageRegistry}/${declaration.id}`,
521
+ repositoryUrl,
522
+ repo: declaration.repo,
523
+ path:
524
+ declaration.path === '.'
525
+ ? declaration.repo
526
+ : `${declaration.repo}/${declaration.path}`,
527
+ sourceManifestPath: `${declaration.repo}/${declaration.sourceManifestPath}`,
528
+ sourceCommit,
529
+ sourceManifestHash: sha256Hex(serializeJson(sourceManifestJson)),
530
+ };
531
+ }).sort(compareSubject);
532
+ const sourceRepos = new Set([
533
+ ...biomeIndex.map((biome) => biome.repo),
534
+ ...platformServiceIndex.services.map((service) => service.repo),
535
+ ...supplementalSubjects.map((subject) => subject.repo),
536
+ ]);
537
+ const repositoryPins = [...sourceRepos]
538
+ .sort(compareStrings)
539
+ .map(getRepositoryIdentity);
540
+ const sortedCrossRepoBuildInputs = [...crossRepoBuildInputs()].sort(
541
+ (left, right) =>
542
+ compareStrings(
543
+ [left.consumerRepo, left.consumerService, left.destinationPath].join(
544
+ '\u0000',
545
+ ),
546
+ [right.consumerRepo, right.consumerService, right.destinationPath].join(
547
+ '\u0000',
548
+ ),
549
+ ),
550
+ );
551
+ assertVendoredSourcesResolve(sortedCrossRepoBuildInputs, getRepositoryIdentity);
552
+ const buildStepContextDirectories = [...buildStepContextDirectoriesDeclared()].sort(
553
+ (left, right) =>
554
+ compareStrings(
555
+ `${left.repo}\u0000${left.path}`,
556
+ `${right.repo}\u0000${right.path}`,
557
+ ),
558
+ );
559
+ for (const directory of buildStepContextDirectories) {
560
+ if (!sourceRepos.has(directory.repo)) {
561
+ throw new Error(
562
+ `build-step context directory "${directory.path}" names repository ` +
563
+ `"${directory.repo}", which is not an image source repository`,
564
+ );
565
+ }
566
+ }
567
+ for (const input of sortedCrossRepoBuildInputs) {
568
+ for (const repo of [input.consumerRepo, input.sourceRepo]) {
569
+ if (!sourceRepos.has(repo)) {
570
+ throw new Error(
571
+ `cross-repo build input for "${input.consumerService}" names repository ` +
572
+ `"${repo}", which is not an image source repository`,
573
+ );
574
+ }
575
+ }
576
+ }
577
+ return {
578
+ schemaVersion: CURRENT_INDEX_SCHEMA_VERSION,
579
+ contractAuthority,
580
+ supplementalSubjects,
581
+ crossRepoBuildInputs: sortedCrossRepoBuildInputs,
582
+ buildStepContextDirectories,
583
+ repositoryPins,
584
+ producerWorkflowPins: mergeProducerWorkflowPins(
585
+ existsSync(runtimeImageSourceIndexPath()) ? readJson(runtimeImageSourceIndexPath()).producerWorkflowPins : [],
586
+ producerWorkflowPins,
587
+ ),
588
+ platformServiceBindings: [...platformServiceRuntimeBindings()].sort(
589
+ (left, right) =>
590
+ compareStrings(left.platformService, right.platformService),
591
+ ),
592
+ };
593
+ }
594
+
595
+ function compareSubject(left, right) {
596
+ return (
597
+ compareStrings(left.kind, right.kind) || compareStrings(left.id, right.id)
598
+ );
599
+ }
600
+
601
+ /**
602
+ * Every release pin names a first-party repository (`firstPartySources`) at
603
+ * the URL declared there. The converse is not required: a first-party
604
+ * repository is in the develop graph before any release pins it.
605
+ */
606
+ function assertPinsAreFirstParty(repositoryPins) {
607
+ const declared = firstPartySources('checking that every release pin is a first-party repository');
608
+ for (const pin of repositoryPins) {
609
+ const source = declared[pin.repo];
610
+ if (source === undefined) {
611
+ throw new Error(
612
+ `runtime-image-source-index pins ${JSON.stringify(pin.repo)}, which edition-workspace.json does not declare under "firstPartySources"`,
613
+ );
614
+ }
615
+ if (!sameRepository(source.repositoryUrl, pin.repositoryUrl, `${pin.repo} repositoryUrl`)) {
616
+ throw new Error(
617
+ `runtime-image-source-index pins ${pin.repo} at ${pin.repositoryUrl}, but "firstPartySources" declares it at ${source.repositoryUrl}`,
618
+ );
619
+ }
620
+ }
621
+ }
622
+
623
+ export async function validateRuntimeImageSourceIndex(
624
+ index,
625
+ schemas,
626
+ {
627
+ // Whether `repositoryPins` must already cover every image source repository.
628
+ // TRUE by default, and true for everyone who PRODUCES this index — that is
629
+ // this module's own contract and the assertion below is where it lives.
630
+ //
631
+ // The kernel-authority repin passes false, because it produces nothing here:
632
+ // it rewrites ONE field, `contractAuthority`, on an index somebody else
633
+ // built. Requiring coverage of it makes onboarding a new source repository
634
+ // impossible by construction — the repin refuses the stale index, and the
635
+ // rebuild that would add the missing pin refuses the un-repinned authority.
636
+ // Measured 2026-08-26 when `repos/xema-connectors` was carved out: the
637
+ // documented six-step chain deadlocked at its own first step.
638
+ assertRepositoryCoverage = true,
639
+ // WHICH kernel checkout the committed authority is re-verified against.
640
+ // Undefined keeps the default in-aggregator checkout; see `parseKernelRoot`
641
+ // for why a shared working tree must not decide this.
642
+ kernelRoot = undefined,
643
+ // The indexes this one is validated against: the committed ones by default,
644
+ // a develop snapshot's own when it injects them.
645
+ biomeIndex = readJson(biomeIndexPath()),
646
+ platformServiceIndex = readJson(
647
+ join(dataRoot(), 'platform-service-index.json'),
648
+ ),
649
+ } = {},
650
+ ) {
651
+ await validateKernelContractAuthorityFromGit({
652
+ authority: index.contractAuthority,
653
+ ...(kernelRoot === undefined ? {} : { repositoryDirectory: kernelRoot }),
654
+ schemas,
655
+ onDrift: (drift) =>
656
+ console.log(`[runtime-image-source-index] ${describeKernelContractAuthorityDrift(drift)}`),
657
+ });
658
+ // Every platform service carries an exact source identity (the kernel lock
659
+ // schema requires it), so "is it in platform-service-index.json" no longer
660
+ // separates the two kinds — it is now true of all of them. The partition is
661
+ // the declared runtime binding, read from the one authority that owns it.
662
+ const expectedBindings = platformServiceRuntimeBindings()
663
+ .map((binding) => binding.platformService)
664
+ .sort(compareStrings);
665
+ const actualBindings = index.platformServiceBindings.map(
666
+ (binding) => binding.platformService,
667
+ );
668
+ if (serializeJson(actualBindings) !== serializeJson(expectedBindings)) {
669
+ throw new Error(
670
+ 'runtime-image-source-index platform bindings do not exactly cover every declared platform service that another subject carries',
671
+ );
672
+ }
673
+ const supplemental = new Set(
674
+ index.supplementalSubjects.map(
675
+ (subject) => `${subject.kind}\0${subject.id}`,
676
+ ),
677
+ );
678
+ const expectedRepositories = [
679
+ ...new Set([
680
+ ...biomeIndex.map((biome) => biome.repo),
681
+ ...platformServiceIndex.services.map((service) => service.repo),
682
+ ...index.supplementalSubjects.map((subject) => subject.repo),
683
+ ]),
684
+ ].sort(compareStrings);
685
+ const actualRepositories = index.repositoryPins.map(
686
+ (repository) => repository.repo,
687
+ );
688
+ if (
689
+ assertRepositoryCoverage &&
690
+ serializeJson(actualRepositories) !== serializeJson(expectedRepositories)
691
+ ) {
692
+ throw new Error(
693
+ 'runtime-image-source-index repository pins do not exactly cover every image source repository',
694
+ );
695
+ }
696
+ const repositoryPins = new Map(
697
+ index.repositoryPins.map((repository) => [repository.repo, repository]),
698
+ );
699
+ for (const repository of index.repositoryPins) {
700
+ if (
701
+ canonicalPublicHttpsRepositoryUrl(
702
+ repository.repositoryUrl,
703
+ `${repository.repo} repositoryUrl`,
704
+ ) !== repository.repositoryUrl
705
+ ) {
706
+ throw new Error(`${repository.repo} repositoryUrl is not canonical`);
707
+ }
708
+ }
709
+ assertPinsAreFirstParty(index.repositoryPins);
710
+ // Every subject a biome contributes to the image plane — BOTH the standing
711
+ // services and the runtime components. A runtime component is a full image
712
+ // subject (built, signed, mirrored) that simply is never scheduled, so a
713
+ // platform service may legitimately bind to one: `opencode-image-prepuller`
714
+ // is a standing service whose whole job is to keep the `opencode-worker`
715
+ // runtime-component image warm on every node.
716
+ const biomeImageSubjects = new Set(
717
+ biomeIndex.flatMap((biome) => [
718
+ // A worker running a sibling service's image is not a subject of its own.
719
+ ...(biome.services ?? [])
720
+ .filter((service) => !Object.hasOwn(biome.workerImages ?? {}, service))
721
+ .map((service) => `service\0${service}`),
722
+ ...(biome.runtimeComponents ?? []).map(
723
+ (component) => `runtime-component\0${component}`,
724
+ ),
725
+ ]),
726
+ );
727
+ for (const binding of index.platformServiceBindings) {
728
+ const key = `${binding.subjectKind}\0${binding.subjectId}`;
729
+ if (!supplemental.has(key) && !biomeImageSubjects.has(key)) {
730
+ throw new Error(
731
+ `platform service "${binding.platformService}" binds unknown runtime subject "${binding.subjectKind}:${binding.subjectId}"`,
732
+ );
733
+ }
734
+ }
735
+ for (const subject of index.supplementalSubjects) {
736
+ if (
737
+ canonicalPublicHttpsRepositoryUrl(
738
+ subject.repositoryUrl,
739
+ `${subject.kind}:${subject.id} repositoryUrl`,
740
+ ) !== subject.repositoryUrl
741
+ ) {
742
+ throw new Error(
743
+ `${subject.kind}:${subject.id} repositoryUrl is not canonical`,
744
+ );
745
+ }
746
+ if (
747
+ !subject.path.startsWith(`${subject.repo}/`) &&
748
+ subject.path !== subject.repo
749
+ ) {
750
+ throw new Error(
751
+ `${subject.kind}:${subject.id} source path escapes ${subject.repo}`,
752
+ );
753
+ }
754
+ if (!subject.sourceManifestPath.startsWith(`${subject.repo}/`)) {
755
+ throw new Error(
756
+ `${subject.kind}:${subject.id} source manifest escapes ${subject.repo}`,
757
+ );
758
+ }
759
+ const repository = repositoryPins.get(subject.repo);
760
+ if (
761
+ !repository ||
762
+ repository.repositoryUrl !== subject.repositoryUrl ||
763
+ repository.sourceCommit !== subject.sourceCommit
764
+ ) {
765
+ throw new Error(
766
+ `${subject.kind}:${subject.id} does not match its repository pin`,
767
+ );
768
+ }
769
+ }
770
+ return index;
771
+ }
772
+
773
+ /** The follow-ups a re-pin owes; printed on stderr, because the repin itself changes only one field. */
774
+ export const REPIN_CONTRACT_AUTHORITY_FOLLOW_UPS = Object.freeze([
775
+ 'xema-edition repin-evidence-trust (with the data-root and kernel flags): the evidence trust pin binds the authority; re-pin it.',
776
+ 'Re-resolve every edition (xema-edition resolve): each images.lock.json carries the contract authority, so its normalizedImageLockDigest changes.',
777
+ 'Signed producer statements made under the old authority no longer verify: a statement binds the authority it was signed under, so they must be re-signed.',
778
+ ]);
779
+
780
+ /**
781
+ * Replace ONLY `contractAuthority` of the committed runtime image source index
782
+ * with the one trusted from the kernel checkout at the commit the caller
783
+ * states (`kernelCommit`, required; HEAD must equal it), and write it back.
784
+ *
785
+ * The document is read as raw strict JSON and the field replaced BEFORE it is
786
+ * parsed, so a stale index cannot block its own repair (the wire schema
787
+ * checks the shape of `sourcePaths` only, from contracts 0.110.3).
788
+ * The new authority's `sourcePaths` are the set the stated commit declares,
789
+ * and `buildTrustedKernelContractAuthority` requires this CLI's bundled set to
790
+ * equal it: a repin is trusted generation at HEAD.
791
+ * Every repository pin and subject is kept byte for byte. Repository coverage
792
+ * is not asserted: this repin produces no pin, and requiring coverage would
793
+ * make a stale index unrepairable.
794
+ */
795
+ export async function repinContractAuthority(schemas, kernelRoot, kernelCommit) {
796
+ const path = runtimeImageSourceIndexPath();
797
+ if (!existsSync(path)) {
798
+ throw new Error(`${path} does not exist: there is no index whose contract authority could be re-pinned`);
799
+ }
800
+ const raw = readJson(path);
801
+ const contractAuthority = await buildTrustedKernelContractAuthority({
802
+ ...(kernelRoot === undefined ? {} : { repositoryDirectory: kernelRoot }),
803
+ schemas,
804
+ kernelCommit,
805
+ });
806
+ const candidate = schemas.parseRuntimeImageSourceIndex({ ...raw, contractAuthority });
807
+ await validateRuntimeImageSourceIndex(candidate, schemas, {
808
+ assertRepositoryCoverage: false,
809
+ kernelRoot,
810
+ });
811
+ writeFileSync(path, serializeJson(candidate));
812
+ return { path, previous: raw.contractAuthority, current: contractAuthority };
813
+ }
814
+
815
+ /**
816
+ * `--kernel-root` names WHICH checkout of the kernel repository the committed
817
+ * contract authority is re-verified against. It relaxes NOTHING: the digest is
818
+ * still recomputed from `ls-tree` + `cat-file` blobs at the authority's own
819
+ * pinned commit and must match byte for byte.
820
+ *
821
+ * It exists for the reason this file already argues one function down — a
822
+ * development-state fact must not decide a release-composition question. The
823
+ * default checkout is SHARED, so it routinely sits on a stale or unrelated
824
+ * history, and the authority pin then fails an ancestry test that says nothing
825
+ * about the pin and everything about whose branch that directory is on.
826
+ * Measured 2026-09-14: the pin was refused as "not an ancestor of kernel HEAD"
827
+ * by a checkout 39 commits behind, while being a perfectly ordinary ancestor of
828
+ * the published history.
829
+ */
830
+ function parseRepoScope(argv) {
831
+ const index = argv.indexOf('--repo');
832
+ if (index === -1) {return { repoScope: undefined, remaining: argv };}
833
+ const value = argv[index + 1];
834
+ if (!value || value.startsWith('--')) {
835
+ throw new Error('--repo requires a repository path, e.g. repos/<name>');
836
+ }
837
+ return {
838
+ repoScope: value,
839
+ remaining: [...argv.slice(0, index), ...argv.slice(index + 2)],
840
+ };
841
+ }
842
+
843
+ /**
844
+ * `--at <sha>` — pin the `--repo` repository at an EXACT commit instead of at
845
+ * whatever the live resolver judges newest-built. Only meaningful with `--repo`,
846
+ * and refused without it: "pin everything at this one commit" is not a thing.
847
+ */
848
+ function parseExactCommit(argv) {
849
+ const index = argv.indexOf('--at');
850
+ if (index === -1) {return { exactCommit: undefined, remaining: argv };}
851
+ const value = argv[index + 1];
852
+ if (!value || value.startsWith('--')) {
853
+ throw new Error('--at requires a full 40-character commit sha');
854
+ }
855
+ return {
856
+ exactCommit: value,
857
+ remaining: [...argv.slice(0, index), ...argv.slice(index + 2)],
858
+ };
859
+ }
860
+
861
+ function parseKernelRoot(argv) {
862
+ const index = argv.indexOf('--kernel-root');
863
+ if (index === -1) {return { kernelRoot: undefined, remaining: argv };}
864
+ const value = argv[index + 1];
865
+ if (!value) {throw new Error('--kernel-root requires a directory');}
866
+ if (!isAbsolute(value)) {
867
+ throw new Error('--kernel-root requires an ABSOLUTE directory');
868
+ }
869
+ return {
870
+ kernelRoot: realpathSync(value),
871
+ remaining: [...argv.slice(0, index), ...argv.slice(index + 2)],
872
+ };
873
+ }
874
+
875
+ /**
876
+ * `--kernel-commit <40-hex>`: the kernel commit a NEW contract authority is
877
+ * taken from. Required wherever one is chosen (the re-pin, and a bootstrap
878
+ * `--trusted-write`), refused everywhere else.
879
+ */
880
+ function parseKernelCommit(argv) {
881
+ const index = argv.indexOf(KERNEL_COMMIT_FLAG);
882
+ if (index === -1) {return { kernelCommit: undefined, remaining: argv };}
883
+ const value = argv[index + 1];
884
+ if (!value || !COMMIT_PATTERN.test(value)) {
885
+ throw new Error(`${KERNEL_COMMIT_FLAG} requires a full 40-character commit sha`);
886
+ }
887
+ return {
888
+ kernelCommit: value,
889
+ remaining: [...argv.slice(0, index), ...argv.slice(index + 2)],
890
+ };
891
+ }
892
+
893
+ /**
894
+ * @param {{ schemas: object, repositoryPins: object[], kernelRoot?: string, producerWorkflowPins?: object[], contractAuthority?: object }} request
895
+ * `contractAuthority`, when given, IS the index's authority (a candidate that is the kernel authority states its own,
896
+ * built by `buildTrustedKernelContractAuthority` as the repin does); it is then not resolved from the committed index.
897
+ * `kernelRoot` is the kernel checkout the committed contract authority is re-verified against
898
+ * (the default checkout when omitted); `producerWorkflowPins` are the operator-stated ones a
899
+ * trusted write merges over the committed ones.
900
+ * @returns {{ platformServiceIndex, biomeIndex, runtimeImageSourceIndex }}
901
+ */
902
+ export async function buildIndexesAtPins({ schemas, repositoryPins, kernelRoot, producerWorkflowPins = [], contractAuthority }) {
903
+ const sourceAuthority = createPinnedSourceAuthority({ pins: repositoryPins });
904
+ const platformServiceIndex = buildValidatedPlatformServiceIndex(schemas, sourceAuthority);
905
+ const biomeIndex = await buildBiomeIndex(platformServiceIndex, distributionBiomeSourceRepositories(), sourceAuthority);
906
+ const runtimeImageSourceIndex = schemas.parseRuntimeImageSourceIndex(
907
+ await buildTrustedIndex(schemas, sourceAuthority, kernelRoot, undefined, {
908
+ biomeIndex,
909
+ platformServiceIndex,
910
+ producerWorkflowPins,
911
+ contractAuthority,
912
+ }),
913
+ );
914
+ await validateRuntimeImageSourceIndex(runtimeImageSourceIndex, schemas, { biomeIndex, platformServiceIndex, kernelRoot });
915
+ return { platformServiceIndex, biomeIndex, runtimeImageSourceIndex };
916
+ }
917
+
918
+ /**
919
+ * A repository JOINING the index (`--repo <repository>` with no committed pin): all three indexes,
920
+ * from ONE resolution of the joining repository, with every committed pin kept.
921
+ *
922
+ * WHY HERE AND NOT A `--repo` ON THE BIOME BUILDER. The biome builder's `--repo` was removed
923
+ * because it resolved the scoped repository LIVE a second time, so a build landing between the two
924
+ * commands split the two artifacts on exactly that repository (see `build-biome-index.mjs`). A
925
+ * joining repository has the same shape: the runtime index cannot take it until the biome index
926
+ * names its biomes, and the biome index cannot take it until a pin exists. So the joining
927
+ * repository is resolved ONCE here (`sourceAuthority`, live or `--at`), appended to the committed
928
+ * pins, and every index is derived from that one pin set by the same builders a develop snapshot
929
+ * uses; nothing else is resolved live.
930
+ *
931
+ * Every committed pin that survives must come back field for field; one that differs is refused by
932
+ * name. A committed pin whose repository is no longer a source repository is DROPPED, and the caller
933
+ * names it (`describeRepositoryPinChanges`). A second declared repository with no pin is refused by
934
+ * the pinned authority: one joining repository per write.
935
+ */
936
+ export async function buildIndexesWithJoiningRepository({
937
+ schemas,
938
+ sourceAuthority,
939
+ repoScope,
940
+ committedPins,
941
+ kernelRoot,
942
+ producerWorkflowPins = [],
943
+ }) {
944
+ if (committedPins.some((pin) => pin.repo === repoScope)) {
945
+ throw new Error(`--repo "${repoScope}" already has a committed pin: it is a refresh, not a join`);
946
+ }
947
+ const kept = committedPins.map(({ repo, repositoryUrl, sourceCommit, vendoredSourceCommit }) => ({
948
+ repo,
949
+ repositoryUrl,
950
+ sourceCommit,
951
+ vendoredSourceCommit,
952
+ }));
953
+ const repositoryPins = [...kept, repositoryIdentity(repoScope, sourceAuthority)];
954
+ const built = await buildIndexesAtPins({ schemas, repositoryPins, kernelRoot, producerWorkflowPins });
955
+ const written = new Map(built.runtimeImageSourceIndex.repositoryPins.map((pin) => [pin.repo, pin]));
956
+ const moved = kept.filter((pin) => written.has(pin.repo) && serializeJson(written.get(pin.repo)) !== serializeJson(pin));
957
+ if (moved.length > 0) {
958
+ throw new Error(
959
+ `a join must keep every committed pin, and ${moved.map((pin) => pin.repo).join(', ')} came back different ` +
960
+ '(its repositoryUrl no longer matches its origin remote?)',
961
+ );
962
+ }
963
+ if (!written.has(repoScope)) {
964
+ throw new Error(
965
+ `--repo "${repoScope}" contributes no biome, platform service or supplemental subject at ` +
966
+ `${repositoryPins.at(-1).sourceCommit}: there is nothing for it to join`,
967
+ );
968
+ }
969
+ return built;
970
+ }
971
+
972
+ /**
973
+ * What a write did to the committed `repositoryPins`, by NAME: the repositories added, the ones
974
+ * dropped (a committed pin whose repository no longer sources any subject), the ones moved, and
975
+ * how many were kept unchanged. A count alone hid a dropped pin inside "N other pin(s)
976
+ * preserved", which is the one change a reviewer must see.
977
+ */
978
+ export function describeRepositoryPinChanges(committedPins, writtenPins) {
979
+ const committed = new Map((committedPins ?? []).map((pin) => [pin.repo, pin]));
980
+ const written = new Map(writtenPins.map((pin) => [pin.repo, pin]));
981
+ const added = [...written.keys()].filter((repo) => !committed.has(repo)).sort(compareStrings);
982
+ const dropped = [...committed.keys()].filter((repo) => !written.has(repo)).sort(compareStrings);
983
+ const moved = [...written.keys()]
984
+ .filter((repo) => committed.has(repo) && serializeJson(committed.get(repo)) !== serializeJson(written.get(repo)))
985
+ .sort(compareStrings);
986
+ const kept = written.size - added.length - moved.length;
987
+ const named = (label, repos) => `${label} ${repos.length}${repos.length > 0 ? ` (${repos.join(', ')})` : ''}`;
988
+ return {
989
+ added,
990
+ dropped,
991
+ moved,
992
+ kept,
993
+ summary: `${named('added', added)}; ${named('moved', moved)}; ${named('dropped', dropped)}; kept ${kept} unchanged`,
994
+ };
995
+ }
996
+
997
+ const REPIN_FLAG = '--repin-contract-authority';
998
+ const USAGE =
999
+ 'usage: build-runtime-image-source-index.mjs (--check | --check-sources | --trusted-write | ' +
1000
+ '--repin-contract-authority --kernel-commit <40-hex-commit>) ' +
1001
+ '[--source published|workspace] [--kernel-root <absolute-checkout>] ' +
1002
+ '[--repo <repository> [--at <40-hex-commit>]] [--producer-workflow-pin <owner/repo>=<40-hex-commit> ...] ' +
1003
+ '(--trusted-write on a data root with no committed contract authority also requires --kernel-commit)\n' +
1004
+ ` build-runtime-image-source-index.mjs --trusted-write ${THIRD_PARTY_WRITER_USAGE}\n` +
1005
+ ` build-runtime-image-source-index.mjs ${CHECK_THIRD_PARTY_FLAG} [--cosign <absolute cosign path>] [${CHANNEL_FLAG} <develop|release>]`;
1006
+
1007
+ /** `--trusted-write --third-party <name>`: verify one third-party source and commit its pin, evidence and producer workflow pin. */
1008
+ async function writeThirdPartySource({ schemas, writer, cosignBinary, kernelRoot, channel }) {
1009
+ const context = loadThirdPartySourceContext({ dataRoot: dataRoot(), schemas, cosignBinary });
1010
+ try {
1011
+ const plan = await planThirdPartySourceWrite({
1012
+ request: writer,
1013
+ context,
1014
+ clientFor: createRegistryClientResolver({ dataRoot: dataRoot() }),
1015
+ dataRoot: dataRoot(),
1016
+ withProducerWorkflowPin: indexWithProducerWorkflowPin,
1017
+ channel,
1018
+ });
1019
+ await validateRuntimeImageSourceIndex(plan.candidateIndex, schemas, { kernelRoot });
1020
+ const next = commitThirdPartySourceWrite({
1021
+ dataRoot: dataRoot(),
1022
+ plan,
1023
+ indexPath: runtimeImageSourceIndexPath(),
1024
+ indexText: serializeJson(plan.candidateIndex),
1025
+ });
1026
+ console.log(`[runtime-image-source-index] ${next}`);
1027
+ } finally {
1028
+ context.dispose();
1029
+ disposeRegistrySessions();
1030
+ }
1031
+ }
1032
+
1033
+ async function main() {
1034
+ const { writer, cosignBinary, channel: statedChannel, remaining: afterThirdPartyFlags } = takeThirdPartyFlags(process.argv.slice(2));
1035
+ const { kernelRoot, remaining: afterKernelRootFlag } = parseKernelRoot(afterThirdPartyFlags);
1036
+ const { kernelCommit, remaining: afterKernelRoot } = parseKernelCommit(afterKernelRootFlag);
1037
+ const { producerWorkflowPins, remaining: afterWorkflowPins } = parseProducerWorkflowPinFlags(afterKernelRoot);
1038
+ const { repoScope, remaining: afterRepoScope } = parseRepoScope(afterWorkflowPins);
1039
+ const { exactCommit, remaining: argv } = parseExactCommit(afterRepoScope);
1040
+ const { mode, remaining } = parseSourceModeArgs(argv);
1041
+ if (exactCommit !== undefined && repoScope === undefined) {
1042
+ throw new Error('--at requires --repo: it pins ONE repository at an exact commit');
1043
+ }
1044
+ const [flag] = remaining;
1045
+ if (
1046
+ remaining.length !== 1 ||
1047
+ ![CHECK_THIRD_PARTY_FLAG, '--check', '--check-sources', '--trusted-write', REPIN_FLAG].includes(flag)
1048
+ ) {
1049
+ throw new Error(USAGE);
1050
+ }
1051
+ if (cosignBinary !== undefined && writer === undefined && flag !== CHECK_THIRD_PARTY_FLAG) {
1052
+ throw new Error(`--cosign belongs to ${THIRD_PARTY_FLAG} or ${CHECK_THIRD_PARTY_FLAG}: ${flag} runs no cosign\n${USAGE}`);
1053
+ }
1054
+ if (kernelCommit !== undefined && ![REPIN_FLAG, '--trusted-write'].includes(flag)) {
1055
+ throw new Error(`${KERNEL_COMMIT_FLAG} states the commit of a new contract authority: ${flag} chooses none\n${USAGE}`);
1056
+ }
1057
+ if (statedChannel !== undefined && flag !== CHECK_THIRD_PARTY_FLAG && writer === undefined) {
1058
+ throw new Error(`${CHANNEL_FLAG} belongs to ${THIRD_PARTY_FLAG} or ${CHECK_THIRD_PARTY_FLAG}: ${flag} takes no channel\n${USAGE}`);
1059
+ }
1060
+ const channel = statedChannel ?? EditionChannel.Release;
1061
+ assertProducerWorkflowPinsWritten(producerWorkflowPins, flag, USAGE);
1062
+ const schemas = await loadKernelDistributionSchemas();
1063
+ if (flag === CHECK_THIRD_PARTY_FLAG) {
1064
+ if (repoScope !== undefined || exactCommit !== undefined || kernelCommit !== undefined) {
1065
+ throw new Error(`${CHECK_THIRD_PARTY_FLAG} re-verifies the committed third-party pins only: it takes no --repo, --at or ${KERNEL_COMMIT_FLAG}`);
1066
+ }
1067
+ const { checked } = await checkThirdPartySourcePins({ dataRoot: dataRoot(), schemas, cosignBinary, channel });
1068
+ console.log(
1069
+ checked === 0
1070
+ ? '[runtime-image-source-index] no third-party source is pinned in this data root'
1071
+ : `[runtime-image-source-index] third-party source check passed for ${checked} pinned source(s)`,
1072
+ );
1073
+ return;
1074
+ }
1075
+ if (writer !== undefined) {
1076
+ if (flag !== '--trusted-write') {
1077
+ throw new Error(`${THIRD_PARTY_FLAG} writes a pin: it takes --trusted-write\n${USAGE}`);
1078
+ }
1079
+ if (repoScope !== undefined || exactCommit !== undefined || kernelCommit !== undefined || producerWorkflowPins.length > 0) {
1080
+ throw new Error(`${THIRD_PARTY_FLAG} writes one third-party pin: it takes no --repo, --at, ${KERNEL_COMMIT_FLAG} or ${PRODUCER_WORKFLOW_PIN_FLAG}`);
1081
+ }
1082
+ await writeThirdPartySource({ schemas, writer, cosignBinary, kernelRoot, channel });
1083
+ return;
1084
+ }
1085
+ if (flag === REPIN_FLAG) {
1086
+ if (repoScope !== undefined || exactCommit !== undefined) {
1087
+ throw new Error(`${REPIN_FLAG} replaces the contract authority only: it takes no --repo or --at`);
1088
+ }
1089
+ if (kernelCommit === undefined) {
1090
+ throw new Error(`${REPIN_FLAG} requires ${KERNEL_COMMIT_FLAG} <40-hex-commit>: the commit the authority moves to is stated, never read from the checkout\n${USAGE}`);
1091
+ }
1092
+ const { path, previous, current } = await repinContractAuthority(schemas, kernelRoot, kernelCommit);
1093
+ console.log(
1094
+ `[runtime-image-source-index] wrote ${path} (contract authority ${previous?.sourceCommit ?? 'absent'} -> ${current.sourceCommit}; every repository pin and subject preserved)`,
1095
+ );
1096
+ for (const followUp of REPIN_CONTRACT_AUTHORITY_FOLLOW_UPS) {
1097
+ console.error(`[runtime-image-source-index] owed after the re-pin: ${followUp}`);
1098
+ }
1099
+ return;
1100
+ }
1101
+ if (
1102
+ !existsSync(runtimeImageSourceIndexPath()) &&
1103
+ flag !== '--trusted-write'
1104
+ ) {
1105
+ throw new Error(`${runtimeImageSourceIndexPath()} does not exist`);
1106
+ }
1107
+ if (flag === '--check') {
1108
+ const parsed = schemas.parseRuntimeImageSourceIndex(
1109
+ readJson(runtimeImageSourceIndexPath()),
1110
+ );
1111
+ await validateRuntimeImageSourceIndex(parsed, schemas, { kernelRoot });
1112
+ console.log(
1113
+ `[runtime-image-source-index] offline check passed for ${parsed.supplementalSubjects.length} supplemental subjects and ${parsed.platformServiceBindings.length} bindings`,
1114
+ );
1115
+ return;
1116
+ }
1117
+ const committedPins = existsSync(runtimeImageSourceIndexPath())
1118
+ ? readJson(runtimeImageSourceIndexPath()).repositoryPins ?? []
1119
+ : [];
1120
+ const liveAuthority = () => createSourceAuthority({ mode, knownRepositories: declaredSourceRepositories() });
1121
+ const sourceAuthority =
1122
+ exactCommit === undefined
1123
+ ? liveAuthority()
1124
+ : createExactCommitSourceAuthority({ delegate: liveAuthority(), repo: repoScope, commit: exactCommit });
1125
+ // A `--repo` with no committed pin JOINS the index, and the biome and platform-service indexes
1126
+ // must take it in the same write: see `buildIndexesWithJoiningRepository`.
1127
+ const joining = repoScope !== undefined && !committedPins.some((pin) => pin.repo === repoScope);
1128
+ let candidate;
1129
+ let joined;
1130
+ if (joining) {
1131
+ if (committedPins.length === 0) {
1132
+ throw new Error(
1133
+ `--repo requires ${runtimeImageSourceIndexPath()} with committed repositoryPins: there are none to keep. ` +
1134
+ 'Bootstrap with --trusted-write and no --repo.',
1135
+ );
1136
+ }
1137
+ if (kernelCommit !== undefined) {
1138
+ throw new Error(`${KERNEL_COMMIT_FLAG} chooses a new contract authority; a join keeps the committed one`);
1139
+ }
1140
+ joined = await buildIndexesWithJoiningRepository({
1141
+ schemas,
1142
+ sourceAuthority,
1143
+ repoScope,
1144
+ committedPins,
1145
+ kernelRoot,
1146
+ producerWorkflowPins,
1147
+ });
1148
+ candidate = joined.runtimeImageSourceIndex;
1149
+ } else {
1150
+ candidate = schemas.parseRuntimeImageSourceIndex(
1151
+ await buildTrustedIndex(schemas, sourceAuthority, kernelRoot, repoScope, {
1152
+ biomeIndex: readJson(biomeIndexPath()),
1153
+ platformServiceIndex: readJson(join(dataRoot(), 'platform-service-index.json')),
1154
+ kernelCommit,
1155
+ producerWorkflowPins,
1156
+ }),
1157
+ );
1158
+ await validateRuntimeImageSourceIndex(candidate, schemas, { kernelRoot });
1159
+ }
1160
+ const outputs = [
1161
+ [runtimeImageSourceIndexPath(), serializeJson(candidate)],
1162
+ ...(joined === undefined
1163
+ ? []
1164
+ : [
1165
+ [biomeIndexPath(), serializeJson(joined.biomeIndex)],
1166
+ [platformServiceIndexPath(), serializeJson(joined.platformServiceIndex)],
1167
+ ]),
1168
+ ];
1169
+ if (flag === '--check-sources') {
1170
+ const stale = outputs.filter(([path, text]) => !existsSync(path) || readFileSync(path, 'utf8') !== text);
1171
+ if (stale.length > 0) {
1172
+ throw new Error(
1173
+ `${stale.map(([path]) => path).join(', ')} does not match exact trusted sources`,
1174
+ );
1175
+ }
1176
+ console.log('[runtime-image-source-index] trusted source check passed');
1177
+ return;
1178
+ }
1179
+ for (const [path, text] of outputs) {writeFileSync(path, text);}
1180
+ const changes = describeRepositoryPinChanges(committedPins, candidate.repositoryPins);
1181
+ console.log(
1182
+ `[runtime-image-source-index] wrote ${outputs.map(([path]) => path).join(', ')} (${
1183
+ repoScope === undefined
1184
+ ? 'all repository pins re-resolved'
1185
+ : `${joining ? 'joined' : 're-resolved'} ${repoScope}${
1186
+ exactCommit === undefined ? '' : ` at the exact commit ${exactCommit}`
1187
+ }`
1188
+ }; ${changes.summary})`,
1189
+ );
1190
+ }
1191
+
1192
+ if (isEntryModule(import.meta.url)) {
1193
+ main().catch((error) => {
1194
+ console.error(`[runtime-image-source-index] ${error.message}`);
1195
+ process.exitCode = 1;
1196
+ });
1197
+ }