@xemahq/edition-tooling 0.0.0-stage → 0.1.0

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,112 @@
1
+ // develop-snapshot-inputs — every input of a DEVELOP edition's resolve, built
2
+ // from ONE frozen snapshot of pins and nothing else.
3
+ //
4
+ // The release resolvers read their inputs from committed files: the platform
5
+ // service index, the biome index, the runtime image source index and the
6
+ // edition chain. A develop edition has no committed copy of any of them at its
7
+ // pins, so each is REBUILT here by the release builder itself, over a pinned
8
+ // source authority whose pins are the snapshot's. There is no second builder:
9
+ // the same functions that regenerate the committed indexes produce the
10
+ // snapshot's, and they are handed to the release resolvers as injected inputs
11
+ // (`resolveDistributionWithIndex(id, { inputs })`, `resolveImages(id, { inputs,
12
+ // lock })`). A snapshot whose pins equal the committed ones therefore resolves
13
+ // to the committed locks, which `develop-snapshot-inputs.test.mjs` proves.
14
+ //
15
+ // No fallback: the authority has no delegate, so a repository the snapshot does
16
+ // not pin refuses by name instead of resolving to a branch tip.
17
+
18
+ import { execFileSync } from 'node:child_process';
19
+
20
+ import { RefusalError } from './collected-refusals.mjs';
21
+ import { dataRoot, deployRoot, editionsRoot, kernelRepoDir, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
22
+ import { buildIndexesAtPins } from './indexes-at-pins.mjs';
23
+ import { buildTrustedKernelContractAuthority } from './kernel-contract-authority.mjs';
24
+ import {
25
+ dataPinKey,
26
+ EDITIONS_ROOT_PIN_KEY,
27
+ kernelPinKey,
28
+ deployPinKey,
29
+ applyPinSource,
30
+ snapshotPinSource,
31
+ validateSnapshotPins,
32
+ } from './pin-source.mjs';
33
+ import { composeEdition } from './resolve-distribution.mjs';
34
+ import { readStrictJsonFile } from './strict-json.mjs';
35
+
36
+ function git(dir, args) {
37
+ return execFileSync('git', ['-C', dir, ...args], {
38
+ encoding: 'utf8',
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ }).trim();
41
+ }
42
+
43
+ /**
44
+ * A checkout the resolve reads must BE the frozen commit and be clean: the vendor
45
+ * tree is walked for parents and runs the builders, the editions root holds the
46
+ * edition and its adopted authorization lock.
47
+ */
48
+ export function assertFrozenCheckout({ dir, pins, key }) {
49
+ const pinned = pins[key];
50
+ if (pinned === undefined) {throw new RefusalError(`the snapshot has no pin "${key}" for ${dir}`);}
51
+ const head = git(dir, ['rev-parse', 'HEAD']);
52
+ if (head !== pinned) {
53
+ throw new RefusalError(`${dir} is at ${head}, but the snapshot pins "${key}" at ${pinned}`);
54
+ }
55
+ const dirty = git(dir, ['status', '--porcelain']);
56
+ if (dirty.length > 0) {
57
+ throw new RefusalError(`${dir} is not clean, so it is not the frozen commit ${pinned} ("${key}"):\n${dirty}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Every checkout a develop resolve reads: the vendor tree, the kernel the
63
+ * authorization lock is sealed with, the chart tree the chart-set lock digests,
64
+ * and the editions root when it is another repository.
65
+ */
66
+ export function assertFrozenCheckouts(pins) {
67
+ assertFrozenCheckout({ dir: dataRoot(), pins, key: dataPinKey() });
68
+ assertFrozenCheckout({ dir: kernelRepoDir(), pins, key: kernelPinKey() });
69
+ assertFrozenCheckout({ dir: deployRoot(), pins, key: deployPinKey() });
70
+ if (editionsRoot() !== dataRoot()) {
71
+ assertFrozenCheckout({ dir: editionsRoot(), pins, key: EDITIONS_ROOT_PIN_KEY });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {{ id: string, pins: Record<string,string>, schemas: object, kernelRoot?: string, contractAuthority?: object }} request
77
+ * `kernelRoot` and `contractAuthority`: a candidate that is the kernel authority states its own (see buildIndexesAtPins).
78
+ * @returns the injected inputs for the release resolvers, plus the pin source.
79
+ */
80
+ export async function buildSnapshotInputs({ id, pins, schemas, kernelRoot, contractAuthority }) {
81
+ const pinSource = snapshotPinSource(pins);
82
+ const composed = composeEdition(schemas.parseDistribution, id, pinSource);
83
+ // A stated `contractAuthority` (a candidate that IS the kernel authority) replaces the committed one BEFORE the
84
+ // parse, as `repinContractAuthority` does: the develop index may still name an authority of another repository,
85
+ // which the schema refuses and which is irrelevant to this resolve. The schema itself is untouched.
86
+ const committedDocument = readStrictJsonFile(runtimeImageSourceIndexPath());
87
+ const committedIndex = schemas.parseRuntimeImageSourceIndex(
88
+ contractAuthority === undefined ? committedDocument : { ...committedDocument, contractAuthority },
89
+ );
90
+ validateSnapshotPins(pins, {
91
+ declaredSourceNames: composed.declaredSourceNames,
92
+ editionsRootExternal: editionsRoot() !== dataRoot(),
93
+ });
94
+ const { platformServiceIndex, biomeIndex, runtimeImageSourceIndex } = await buildIndexesAtPins({
95
+ schemas,
96
+ repositoryPins: applyPinSource(committedIndex, pinSource).repositoryPins,
97
+ kernelRoot,
98
+ contractAuthority,
99
+ });
100
+ return { pinSource, composed, platformServiceIndex, biomeIndex, runtimeImageSourceIndex };
101
+ }
102
+
103
+ /**
104
+ * The develop resolve's inputs: `buildSnapshotInputs`, with the kernel authority BUILT from the pinned member checkout
105
+ * (`kernelRepoDir()`, at the kernel pin) instead of the index's committed one, which names another repository's history.
106
+ * `build` is the authority builder (a test stands in for it).
107
+ */
108
+ export async function buildDevelopSnapshotInputs({ id, pins, schemas, build = buildTrustedKernelContractAuthority }) {
109
+ const kernelRoot = kernelRepoDir();
110
+ const contractAuthority = await build({ repositoryDirectory: kernelRoot, schemas, kernelCommit: pins[kernelPinKey()] });
111
+ return buildSnapshotInputs({ id, pins, schemas, kernelRoot, contractAuthority });
112
+ }
@@ -0,0 +1,20 @@
1
+ // develop-store-root — the grammar of a first-party source's develop store root (`developImageRegistry` in
2
+ // edition-workspace.json). A leaf module: the workspace parser and the develop coordinate both read it.
3
+
4
+ import { DEVELOP_IMAGE_NAMESPACE_SEGMENT } from './edition-constants.mjs';
5
+
6
+ const REGISTRY_HOST = '[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]+)?';
7
+ const REGISTRY_PATH_SEGMENT = '[a-z0-9]+(?:[._-][a-z0-9]+)*';
8
+ const DEVELOP_IMAGE_REGISTRY = new RegExp(`^${REGISTRY_HOST}(?:/${REGISTRY_PATH_SEGMENT})+$`);
9
+
10
+ /** Why `value` is not a develop store root (`<host[:port]>/<path>`, lowercase, no scheme, tag, digest, trailing slash or `/dev`); null when it is. */
11
+ export function developImageRegistryProblem(value) {
12
+ if (typeof value !== 'string' || !DEVELOP_IMAGE_REGISTRY.test(value)) {
13
+ return `must be a lowercase <host[:port]>/<path> store root with no scheme, tag, digest or trailing slash; got ${JSON.stringify(value)}`;
14
+ }
15
+ if (value.split('/').at(-1) === DEVELOP_IMAGE_NAMESPACE_SEGMENT) {
16
+ return `must not end in the "${DEVELOP_IMAGE_NAMESPACE_SEGMENT}" lane segment (the develop lane adds it); got ${JSON.stringify(value)}`;
17
+ }
18
+ return null;
19
+ }
20
+
@@ -0,0 +1,172 @@
1
+ import { compareStrings } from './string-order.mjs';
2
+
3
+ /** A `runtime-image` edge target that is neither `{biomeId, componentKey}` nor `{supplementalRuntimeComponentId}`, or is both. */
4
+ export class DispatchedEdgeError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'DispatchedEdgeError';
8
+ }
9
+ }
10
+
11
+ /** A dispatched `supplemental:<id>` the runtime-image source index does not declare as a supplemental subject. */
12
+ export class UndeclaredSupplementalRuntimeComponentError extends Error {
13
+ constructor(supplementalRuntimeComponentId, dispatchingBiomes, context) {
14
+ super(
15
+ `${context}: supplemental runtime component "${supplementalRuntimeComponentId}", dispatched by biome ` +
16
+ `${dispatchingBiomes.map((id) => `"${id}"`).join(', ')}, is not declared as a runtime-component subject ` +
17
+ 'by the runtime image source index — refusing rather than shipping the dispatcher without the image it launches.',
18
+ );
19
+ this.name = 'UndeclaredSupplementalRuntimeComponentError';
20
+ this.supplementalRuntimeComponentId = supplementalRuntimeComponentId;
21
+ this.dispatchingBiomes = dispatchingBiomes;
22
+ }
23
+ }
24
+
25
+ // The `supplementalRuntimeComponentId` field is read as plain data. Since edition
26
+ // protocol v10 the kernel types it (`DispatchedRuntimeComponentSchema`, a union of
27
+ // two strict objects); this projection keeps reading it as data and refuses a target
28
+ // that is neither form, or both.
29
+ const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
30
+
31
+ /** True for the supplemental form of a projected edge. */
32
+ export const isSupplementalEdge = (edge) => edge.supplementalRuntimeComponentId !== undefined;
33
+
34
+ /** The identity of a projected edge: `supplemental:<id>` or `<biomeId>\0<componentKey>`. */
35
+ export const dispatchedEdgeIdentity = (edge) =>
36
+ isSupplementalEdge(edge)
37
+ ? `supplemental:${edge.supplementalRuntimeComponentId}`
38
+ : `${edge.biomeId}\0${edge.componentKey}`;
39
+
40
+ /**
41
+ * Project one generated-index `runtime-image` edge target into the typed union
42
+ * the index entry carries. A target that is neither form, or both, is refused
43
+ * naming the dispatching biome; no `undefined` identity can form.
44
+ */
45
+ export function projectDispatchedTarget(dispatchingBiomeId, target) {
46
+ const supplemental = target?.supplementalRuntimeComponentId;
47
+ const biomeForm = target?.biomeId !== undefined || target?.componentKey !== undefined;
48
+ if (supplemental !== undefined) {
49
+ if (biomeForm || !isNonEmptyString(supplemental)) {
50
+ throw new DispatchedEdgeError(
51
+ `biome "${dispatchingBiomeId}" has a runtime-image edge target that mixes supplementalRuntimeComponentId with ` +
52
+ 'biomeId/componentKey, or names an empty id — a target is exactly one form',
53
+ );
54
+ }
55
+ return { supplementalRuntimeComponentId: supplemental };
56
+ }
57
+ if (!isNonEmptyString(target?.biomeId) || !isNonEmptyString(target?.componentKey)) {
58
+ throw new DispatchedEdgeError(
59
+ `biome "${dispatchingBiomeId}" has a runtime-image edge target that is neither { biomeId, componentKey } nor ` +
60
+ '{ supplementalRuntimeComponentId }',
61
+ );
62
+ }
63
+ return { biomeId: target.biomeId, componentKey: target.componentKey };
64
+ }
65
+
66
+ const edgeLabel = (edge) =>
67
+ isSupplementalEdge(edge) ? `supplemental:${edge.supplementalRuntimeComponentId}` : `${edge.biomeId}/${edge.componentKey}`;
68
+
69
+ /**
70
+ * The runtime components an edition must carry because a biome it rosters
71
+ * DISPATCHES them: the kernel's `runtime-image` edges, projected per biome into
72
+ * the index as `dispatchesRuntimeComponents` and followed here against the
73
+ * index entry of each target biome.
74
+ *
75
+ * "Needs that image" and nothing else. The target biome is NOT rostered — it is
76
+ * not installed, booted or required — only its runtime component's image
77
+ * enters the edition, as a runtime-component row of the image lock.
78
+ *
79
+ * The closure is one step by construction: the kernel admits
80
+ * `dispatchesRuntimeComponents` on a `service` component only, and a target is
81
+ * a runtime component, which is never a standing service — so a dispatched
82
+ * image dispatches nothing further, and an unrostered target biome's own
83
+ * services (and their edges) are not in the edition at all.
84
+ *
85
+ * Targets are resolved against FIRST-PARTY server entries only: the image plane
86
+ * reads a target's coordinates from the first-party biome index, which carries
87
+ * no biome drawn from a declared source. A biome drawn from a declared source
88
+ * that dispatches is refused for the same reason.
89
+ *
90
+ * A supplemental edge (`{supplementalRuntimeComponentId}`) names no biome: it
91
+ * is carried as a row `{ supplementalRuntimeComponentId, consumers }` without
92
+ * a biome lookup.
93
+ *
94
+ * Returns one row per distinct (target biome, component key), sorted, with the
95
+ * rostered biomes that dispatch it. Every unresolvable edge is refused, naming
96
+ * the consumer and the target.
97
+ *
98
+ * @param {string} context the edition the refusal names, e.g. `distribution "appliance-base"`
99
+ * @param {object[]} consumers the rostered index entries
100
+ * @param {object[]} index the index the targets are resolved against
101
+ */
102
+ export function resolveDispatchedRuntimeComponents(context, consumers, index) {
103
+ const rows = new Map();
104
+ for (const consumer of [...consumers].sort((a, b) => compareStrings(a.id, b.id))) {
105
+ const edges = consumer.dispatchesRuntimeComponents ?? [];
106
+ if (edges.length === 0) {continue;}
107
+ if (consumer.source !== undefined) {
108
+ throw new Error(
109
+ `${context}: biome "${consumer.id}", drawn from declared source "${consumer.source}", dispatches ` +
110
+ `runtime components (${edges.map(edgeLabel).join(', ')}). ` +
111
+ 'A dispatched image is resolved from the first-party biome index, which carries no biome from a ' +
112
+ 'declared source — refusing rather than shipping the dispatcher without the images it launches.',
113
+ );
114
+ }
115
+ for (const edge of edges) {
116
+ if (isSupplementalEdge(edge)) {
117
+ // Carried as a requirement owned by the dispatching biome. Whether the
118
+ // runtime image source index declares it is the image plane's refusal.
119
+ const supplementalKey = dispatchedEdgeIdentity(edge);
120
+ const row = rows.get(supplementalKey) ?? {
121
+ supplementalRuntimeComponentId: edge.supplementalRuntimeComponentId,
122
+ consumers: [],
123
+ };
124
+ if (!row.consumers.includes(consumer.id)) {row.consumers.push(consumer.id);}
125
+ rows.set(supplementalKey, row);
126
+ continue;
127
+ }
128
+ const edgeText = `biome "${consumer.id}" dispatches runtime component "${edge.componentKey}" of biome "${edge.biomeId}"`;
129
+ const candidates = index.filter(
130
+ (entry) => entry.id === edge.biomeId && entry.target === 'server' && entry.source === undefined,
131
+ );
132
+ if (candidates.length === 0) {
133
+ throw new Error(
134
+ `${context}: ${edgeText}, but the biome index has no first-party server biome "${edge.biomeId}" — ` +
135
+ 'refusing rather than shipping the dispatcher without the image it launches.',
136
+ );
137
+ }
138
+ if (candidates.length > 1) {
139
+ throw new Error(
140
+ `${context}: ${edgeText}, but the biome index has ${candidates.length} first-party server biomes ` +
141
+ `"${edge.biomeId}" (${candidates.map((entry) => entry.repo).join(', ')}), so the target is ambiguous.`,
142
+ );
143
+ }
144
+ const [target] = candidates;
145
+ const image = (target.runtimeComponentKeys ?? {})[edge.componentKey];
146
+ if (image === undefined) {
147
+ const keys = Object.keys(target.runtimeComponentKeys ?? {});
148
+ throw new Error(
149
+ `${context}: ${edgeText}, but "${edge.biomeId}" declares no runtime component keyed ` +
150
+ `"${edge.componentKey}" (its runtime components: ${keys.length > 0 ? keys.join(', ') : 'none'}). ` +
151
+ 'Either no component has that key, or it is a standing service or worker — a dispatched ' +
152
+ 'component must be one its own biome never schedules.',
153
+ );
154
+ }
155
+ const key = `${target.id}\0${edge.componentKey}`;
156
+ const row = rows.get(key) ?? { target, componentKey: edge.componentKey, image, consumers: [] };
157
+ if (!row.consumers.includes(consumer.id)) {row.consumers.push(consumer.id);}
158
+ rows.set(key, row);
159
+ }
160
+ }
161
+ return [...rows.entries()]
162
+ .sort(([a], [b]) => compareStrings(a, b))
163
+ .map(([, row]) => row);
164
+ }
165
+
166
+ /** Sort order of projected edges: biome edges by (biomeId, componentKey), then supplemental edges by id. */
167
+ export function compareDispatchedEdges(a, b) {
168
+ if (isSupplementalEdge(a) !== isSupplementalEdge(b)) {return isSupplementalEdge(a) ? 1 : -1;}
169
+ return isSupplementalEdge(a)
170
+ ? compareStrings(a.supplementalRuntimeComponentId, b.supplementalRuntimeComponentId)
171
+ : compareStrings(a.biomeId, b.biomeId) || compareStrings(a.componentKey, b.componentKey);
172
+ }