@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,100 @@
1
+ {
2
+ "schema": "xema.edition-contract/v3",
3
+ "vendoring": "This file is the single source of the edition artifact coordinates and names. A consumer vendors it BYTE-IDENTICAL from a stated xema-cli commit and never edits it. It reads both repository namespace segments (repositories.<channel>.namespaceSegment) and the repository prefix (repositoryPrefix) only from this file, and refuses a contract that has no `repositories`.",
4
+ "artifactType": "application/vnd.xema.edition.v1+json",
5
+ "repositoryPrefix": "xema-edition-",
6
+ "repositories": {
7
+ "release": {
8
+ "namespaceSegment": "editions"
9
+ },
10
+ "develop": {
11
+ "namespaceSegment": "develop-editions"
12
+ }
13
+ },
14
+ "descriptorFile": "xema-edition.json",
15
+ "descriptorSchema": "xema.edition/v2",
16
+ "descriptorKinds": [
17
+ "resolved",
18
+ "sealed"
19
+ ],
20
+ "sealOf": {
21
+ "pattern": "^[a-z0-9.-]+(:[0-9]+)?(/[a-z0-9._-]+)+@sha256:[0-9a-f]{64}$",
22
+ "names": "the digest-pinned ref (<registry>/<path>@sha256:<64 hex>) of the one resolved edition a sealed edition seals; a sealed descriptor carries it and no other descriptor does. A consumer reads the pattern from here (a JavaScript RegExp source, anchored) and restates it nowhere",
23
+ "repository": "beyond the pattern: the repository of sealOf (everything before @) is an edition repository, <registry root>/<repositories.<channel>.namespaceSegment>/<repositoryPrefix><distributionId>, whose channel is sealOf.channel and whose distributionId is the sealed descriptor's distributionId",
24
+ "channel": "release",
25
+ "sealedEdition": "a seal is release-only: only a resolved edition of sealOf.channel is sealed (a resolved edition of any other channel is refused), and the sealed edition is pushed to the repository of that channel (repositories.<sealOf.channel>) and records provenance.channel sealOf.channel"
26
+ },
27
+ "locks": [
28
+ "distribution.lock.json",
29
+ "images.lock.json",
30
+ "authorization.lock.json",
31
+ "chart-set.lock.json"
32
+ ],
33
+ "offeringLayers": {
34
+ "catalogue": "catalogue.json"
35
+ },
36
+ "provenancePins": {
37
+ "catalogue": "the sha256 digest (sha256:<64 hex>) of the offering artifact a customer edition was resolved against",
38
+ "definition": "the sha256 digest (sha256:<64 hex>) of the canonical JSON of the customer definition a customer edition was resolved from (vendor selection, image registry mirror, Store publisher anchors, sources); every other pin is a 40-hex commit",
39
+ "customerForms": {
40
+ "names": "a customer edition is one whose pins carry the definition pin. It records its vendor side in exactly one form, closed per kind and channel: the catalogue form, or the catalogue-free form. Every other edition carries neither form's marker: no catalogue pin. The kinds and channels each form admits are enforced by the writers and this CLI's readers (the descriptor parse); the catalogue-free form being develop only also rests on only the develop resolve identity being able to sign one. The deploy root's admission does not enforce the forms: it re-vendors this contract and reads neither provenancePins nor the forms",
41
+ "catalogue": {
42
+ "pins": "the catalogue pin, the definition pin, and a sources/<name> commit per source the definition declares; nothing else. The distribution lock names the catalogue artifact by catalogueDigest, never by catalogueContentDigest",
43
+ "kinds": {
44
+ "resolved": ["develop", "release"],
45
+ "sealed": ["release"]
46
+ }
47
+ },
48
+ "catalogue-free": {
49
+ "pins": "no catalogue pin: the vendor side was resolved from the vendor members in the same call, so the edition records the vendor member pins (keys matching vendorMemberPinPattern, at least one) as 40-hex commits, beside the definition pin, the \"definition-repository\" commit (the commit of the customer definition repository the definition was read at), the sources/<name> commits and the other 40-hex pins of the resolve. The catalogue the vendor step wrote is named in the distribution lock only, by catalogueContentDigest (sha256 of its exact catalogue.json bytes, kernel edition protocol v37), never by catalogueDigest: no catalogue artifact exists",
50
+ "kinds": {
51
+ "resolved": ["develop"]
52
+ }
53
+ },
54
+ "vendorMemberPinPattern": "^(?:repos|submodules)/[a-z0-9][a-z0-9-]*$"
55
+ }
56
+ },
57
+ "imageNamespaceSegment": "dev",
58
+ "evidenceTrust": {
59
+ "states": {
60
+ "armed": "the data root declares an evidence trust policy that is pinned and ARMED on the channel of the edition",
61
+ "not-armed": "a RESOLVED edition of the develop channel only (vendor or customer): its data root declares no policy, or one that is UNARMED on the develop channel. A customer develop edition always records it while a develop catalogue carries no edition-catalogue (E4) evidence. A sealed edition, and a resolved edition of any other channel, never records not-armed"
62
+ },
63
+ "statesByKind": {
64
+ "resolved": {
65
+ "develop": [
66
+ "armed",
67
+ "not-armed"
68
+ ],
69
+ "release": [
70
+ "armed"
71
+ ]
72
+ },
73
+ "sealed": {
74
+ "release": [
75
+ "armed"
76
+ ]
77
+ }
78
+ },
79
+ "refusal": {
80
+ "code": "EVIDENCE_TRUST_REQUIRED_UNARMED",
81
+ "when": "an edition is written while the data root's evidence trust policy is not armed on the channel of the edition (none declared, or declared but UNARMED or PARTIALLY ARMED there) and statesByKind admits only armed for its kind on that channel (every sealed edition; a resolved edition of any channel but develop): no descriptor or artifact is written, and no configuration key admits one. A resolved develop edition records not-armed instead, except under a policy PARTIALLY ARMED on develop, which is refused for every edition"
82
+ }
83
+ },
84
+ "signers": {
85
+ "oidcIssuer": "https://token.actions.githubusercontent.com",
86
+ "identity": "the exact keyless certificate identity of a publisher is https://github.com/<repository>/<workflow>@<ref>, issued by oidcIssuer and by nothing else; no wildcard, and no identity signs both kinds",
87
+ "publishers": {
88
+ "values": "the publisher of an edition of each kind on each channel is data root data, never this contract's: <data root>/edition-workspace.json editionPublishers.<kind>.<channel>, each exactly publishers.shape; a publisher the data root does not declare is refused by name, with no default and no fallback",
89
+ "channelsByKind": {
90
+ "resolved": ["develop", "release"],
91
+ "sealed": ["release"]
92
+ },
93
+ "shape": {
94
+ "repository": "^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9._-]+$",
95
+ "workflow": "^\\.github/workflows/[A-Za-z0-9._-]+\\.ya?ml$",
96
+ "ref": "^refs/heads/[A-Za-z0-9._/-]+$"
97
+ }
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,115 @@
1
+ // edition-image-seal — seal image subjects against the repositories of one channel.
2
+ //
3
+ // The vendor release sealer verifies `<imageRef>:<contentHash>` and writes the digest
4
+ // the registry served. This does the same for an edition of either channel, with the
5
+ // SAME verifier (`verifyDistributionPlatforms`, required platforms enforced)
6
+ // and the SAME digest decision (`planDigestSeal`). Only the repository differs:
7
+ // develop the repository the producer's develop lane pushes to (`developImageRef`, contract C1)
8
+ // release the subject's own imageRef (the identity)
9
+ // It never builds or rebuilds an image: a coordinate that is not there is refused, and the digest
10
+ // sealed is the digest the registry serves for `<repository>:<contentHash>`.
11
+ //
12
+ // A coordinate that is not there is "not built yet": the producer's CI is not
13
+ // green or still running. A third-party subject (a pinned OCI biome's image) is never re-homed and never "not built":
14
+ // it keeps its pinned imageRef and is verified BY the pin's manifest digest (third-party-image-pins.mjs). EVERY such subject is collected and named at once;
15
+ // any other failure (a 403, a malformed index, an absent required platform)
16
+ // aborts as it does in the release.
17
+
18
+ import { developImageRefFor } from './develop-image-coordinate.mjs';
19
+ import { notBuiltYetError } from './develop-image-probe.mjs';
20
+ import { EditionChannel } from './edition-channel.mjs';
21
+ import { editionsRoot } from './edition-workspace.mjs';
22
+ import { imageSubjectIdentity } from '@xemahq/contracts/image-subject-identity';
23
+ import { validateImmutableImageLock, loadKernelDistributionSchemas, } from './lib.mjs';
24
+ import { isNotPublishedYet, planDigestSeal } from './seal-image-digests.mjs';
25
+ import { imageSubjectKey, pinnedManifestDigests, thirdPartyImagePins } from './third-party-image-pins.mjs';
26
+ import { verifyDistributionPlatforms } from './verify-image-platforms.mjs';
27
+
28
+ /** The repository a channel's own image subjects are verified and sealed in, from the subject and its declared source. */
29
+ const CHANNEL_IMAGE_REF = Object.freeze({
30
+ [EditionChannel.Develop]: developImageRefFor,
31
+ [EditionChannel.Release]: (subject) => subject.imageRef,
32
+ });
33
+
34
+ /**
35
+ * The repository `channel` seals `subject` (`{ repo?, imageRef: the release ref }`) in; `declaredSources` is
36
+ * `firstPartySources`, whose `developImageRegistry` roots the develop ref of a first-party subject.
37
+ */
38
+ export function channelImageRef(channel, subject, declaredSources) {
39
+ const toChannelRef = CHANNEL_IMAGE_REF[channel];
40
+ if (toChannelRef === undefined) {throw new Error(`cannot seal image subjects for channel ${JSON.stringify(channel)}`);}
41
+ return toChannelRef(subject, declaredSources);
42
+ }
43
+
44
+ /**
45
+ * The row-level seal: every subject re-homed to its channel's repository, verified
46
+ * there for its required platforms, and given the digest the registry served.
47
+ * It takes SUBJECTS, not a lock, so a caller that owns only some rows of a lock
48
+ * (a customer's own image rows) seals exactly those.
49
+ *
50
+ * @param {object} request
51
+ * @param {string} request.channel `develop` or `release`: which repositories the subjects are sealed in
52
+ * @param {string} request.distributionId the edition the subjects belong to (named in refusals)
53
+ * @param {object[]} request.subjects image subjects (release refs, unsealed)
54
+ * @param {object} request.declaredSources `firstPartySources` (`{}` for a customer's own rows, which are no first-party source)
55
+ * @param {{byRepository: (repository: string) => object|undefined}} [request.thirdPartyPins] the data root's third-party
56
+ * source pins: a subject of a pinned repository keeps its imageRef and is sealed at the pin's manifest digest
57
+ * @returns the subjects with the channel's refs and verified digests
58
+ */
59
+ export async function sealImageSubjects({
60
+ channel,
61
+ distributionId,
62
+ subjects,
63
+ thirdPartyPins = undefined,
64
+ declaredSources,
65
+ distributionsRoot = editionsRoot(),
66
+ env = process.env,
67
+ fetchImpl = globalThis.fetch,
68
+ run = null,
69
+ progress = null,
70
+ }) {
71
+ const pinnedImages = thirdPartyPins === undefined ? new Map() : thirdPartyImagePins({ pins: thirdPartyPins, subjects });
72
+ const channelSubjects = subjects.map((subject) => ({
73
+ ...subject,
74
+ imageRef: pinnedImages.has(imageSubjectKey(subject)) ? subject.imageRef : channelImageRef(channel, subject, declaredSources),
75
+ }));
76
+ const report = await verifyDistributionPlatforms({
77
+ imageLock: { distributionId, subjects: channelSubjects },
78
+ distributionsRoot,
79
+ env,
80
+ fetchImpl,
81
+ run,
82
+ collectFailures: true,
83
+ progress,
84
+ pinnedDigests: pinnedManifestDigests(pinnedImages),
85
+ });
86
+ const aborting = report.failures.find(({ error }) => !isNotPublishedYet(error));
87
+ if (aborting) {throw aborting.error;}
88
+ if (report.failures.length > 0) {
89
+ throw notBuiltYetError(distributionId, report.failures.map(({ subject }) => subject), channel);
90
+ }
91
+ const { writes, unchanged } = planDigestSeal({ subjects: channelSubjects, verified: report.subjects });
92
+ const digests = new Map([...writes, ...unchanged].map(({ key, digest }) => [key, digest]));
93
+ return channelSubjects.map((subject) => ({
94
+ ...subject,
95
+ imageDigest: digests.get(imageSubjectIdentity(subject)),
96
+ }));
97
+ }
98
+
99
+ /**
100
+ * The seal of a WHOLE images lock (the vendor snapshot): every subject sealed,
101
+ * and the result parsed as the kernel's image lock.
102
+ *
103
+ * @param {object} request
104
+ * @param {object} request.imageLock the snapshot images lock (release refs, unsealed)
105
+ * @returns the images lock with the channel's refs and verified digests, kernel-validated
106
+ */
107
+ export async function sealImages({ imageLock, ...sealOptions }) {
108
+ const subjects = await sealImageSubjects({
109
+ distributionId: imageLock.distributionId,
110
+ subjects: imageLock.subjects,
111
+ ...sealOptions,
112
+ });
113
+ const schemas = await loadKernelDistributionSchemas();
114
+ return validateImmutableImageLock(schemas.parseImageLock({ ...imageLock, subjects }), schemas);
115
+ }
@@ -0,0 +1,10 @@
1
+ // edition-kind — the closed vocabularies of an edition descriptor (xema.edition/v2) that the
2
+ // contract (edition-contract.json) names: the lock KIND and the evidence trust STATE. They are
3
+ // enums here so a reader branches on names, and edition-contract-parse holds the contract to
4
+ // exactly these sets: a contract that names another kind or state is refused, never read.
5
+
6
+ /** What an edition IS: a lock resolved by a resolve run, or the seal of one exact resolved edition (`sealOf`). */
7
+ export const EditionKind = Object.freeze({ Resolved: 'resolved', Sealed: 'sealed' });
8
+
9
+ /** Whether the data root's editions are evidence-gated, as the descriptor records it. */
10
+ export const EvidenceTrustState = Object.freeze({ Armed: 'armed', NotArmed: 'not-armed' });
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // `xema-edition list`: the offering ids of an editions root, as JSON.
3
+ //
4
+ // usage: xema-edition list --editions-root <dir>
5
+ // stdout: {"schemaVersion":1,"editions":["<id>",…]} in canonical id order.
6
+
7
+ import { discoverDistributionIds } from './distribution-paths.mjs';
8
+ import {
9
+ WorkspaceRoot,
10
+ configureEditionWorkspaceFromArgv,
11
+ editionsRoot,
12
+ isEntryModule,
13
+ } from './edition-workspace.mjs';
14
+ import { serializeJson } from './lib.mjs';
15
+
16
+ export function listEditions({ editionsRoot: root }) {
17
+ return { schemaVersion: 1, editions: discoverDistributionIds(root) };
18
+ }
19
+
20
+ function main() {
21
+ const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Editions]);
22
+ if (rest.length > 0) {
23
+ throw new Error('usage: xema-edition list --editions-root <dir>');
24
+ }
25
+ process.stdout.write(serializeJson(listEditions({ editionsRoot: editionsRoot() })));
26
+ }
27
+
28
+ if (isEntryModule(import.meta.url)) {
29
+ try {
30
+ main();
31
+ } catch (error) {
32
+ console.error(`[edition-list] ${error.message}`);
33
+ process.exitCode = 1;
34
+ }
35
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // `xema-edition locks <id>`: one edition's validated locks, as JSON.
3
+ //
4
+ // usage: xema-edition locks <id> --channel <release|develop> --data-root <dir> --editions-root <dir> --kernel <dir>
5
+ //
6
+ // The locks are read and validated exactly as the evidence verifier reads
7
+ // them: `images.lock.json` and `distribution.lock.json` under the kernel
8
+ // schema, immutable coordinates only, and the image lock's kernel contract
9
+ // authority equal to the data root's runtime image source index. The
10
+ // distribution lock is read sealed or unsealed alike; sealing is enforced only
11
+ // by the release producer (`sealed-distribution-lock.mjs`). A consumer
12
+ // reads this output instead of parsing or validating a lock itself.
13
+
14
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
15
+ import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
16
+ import {
17
+ WorkspaceRoot,
18
+ configureEditionWorkspaceFromArgv,
19
+ dataRoot,
20
+ editionsRoot,
21
+ isEntryModule,
22
+ } from './edition-workspace.mjs';
23
+ import { loadValidatedLocks } from './image-evidence.mjs';
24
+ import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
25
+
26
+ const USAGE =
27
+ `usage: xema-edition locks <id> --channel <${Object.values(EditionChannel).join('|')}> --data-root <dir> --editions-root <dir> --kernel <dir>`;
28
+
29
+ export async function readEditionLocks({ distributionId, dataRoot: data, editionsRoot: editions, schemas }) {
30
+ assertCanonicalDistributionId(distributionId);
31
+ const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
32
+ await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions });
33
+ return {
34
+ schemaVersion: 1,
35
+ distributionId,
36
+ contractAuthority: runtimeSourceIndex.contractAuthority,
37
+ distributionLock,
38
+ imageLock,
39
+ repositoryPins: [...repositoryPins.values()].sort((left, right) =>
40
+ compareStrings(left.repo, right.repo),
41
+ ),
42
+ };
43
+ }
44
+
45
+ async function main() {
46
+ const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
47
+ WorkspaceRoot.Data,
48
+ WorkspaceRoot.Editions,
49
+ WorkspaceRoot.Kernel,
50
+ ]);
51
+ if (rest.length === 0 || rest[0].startsWith('--')) {
52
+ throw new Error(USAGE);
53
+ }
54
+ // D10 review: --channel no longer affects lock reading; remove with its vendor-ops callers.
55
+ try {
56
+ parseChannelArgs(rest.slice(1));
57
+ } catch (error) {
58
+ throw new Error(`${error.message}\n${USAGE}`);
59
+ }
60
+ const schemas = await loadKernelDistributionSchemas();
61
+ process.stdout.write(
62
+ serializeJson(
63
+ await readEditionLocks({
64
+ distributionId: rest[0],
65
+ dataRoot: dataRoot(),
66
+ editionsRoot: editionsRoot(),
67
+ schemas,
68
+ }),
69
+ ),
70
+ );
71
+ }
72
+
73
+ if (isEntryModule(import.meta.url)) {
74
+ main().catch((error) => {
75
+ console.error(`[edition-locks] ${error.message}`);
76
+ process.exitCode = 1;
77
+ });
78
+ }
@@ -0,0 +1,183 @@
1
+ // edition-pin-verbs — `xema-edition pin` and `xema-edition unpin`: the only
2
+ // supported way to change a vendor edition's `sourcePins` (edition-source-pins.mjs).
3
+ //
4
+ // `pin` validates the hold with the SAME checks a resolve applies
5
+ // (`effectiveRepositoryPins`: graph repository, not the kernel, not equal to the
6
+ // shared pin; `assertEditionPinsAuthorized`: commit present, contained in
7
+ // origin/main, built), writes the entry into the edition's definition keeping
8
+ // its formatting and key order, and re-resolves the edition. `unpin` removes one
9
+ // entry (or all), drops the key when empty, and re-resolves. Both are all or
10
+ // nothing: a failed re-resolve restores the edition directory byte for byte.
11
+
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
16
+ import {
17
+ assertEditionPinsAuthorized,
18
+ effectiveRepositoryPins,
19
+ parseEditionSourcePins,
20
+ SOURCE_PINS_KEY,
21
+ } from './edition-source-pins.mjs';
22
+ import { editionsRoot, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
23
+ import { loadKernelDistributionSchemas } from './lib.mjs';
24
+ import { writeResolvedLockSet } from './resolve-edition.mjs';
25
+ import { loadBuildLedger } from './source-authority.mjs';
26
+ import { readStrictJsonFile } from './strict-json.mjs';
27
+
28
+ export const PIN_USAGE =
29
+ 'usage: xema-edition pin <edition> <repo> <ref> --until "<exit condition>" --data-root <dir> --editions-root <dir> --sources-root <dir> --kernel <dir>';
30
+ export const UNPIN_USAGE =
31
+ 'usage: xema-edition unpin <edition> [<repo>] --data-root <dir> --editions-root <dir> --sources-root <dir> --kernel <dir>';
32
+
33
+ const UNTIL_FLAG = '--until';
34
+
35
+ /** `{ positional, until }` from the arguments after the root flags. */
36
+ export function parsePinArgs(args) {
37
+ const positional = [];
38
+ let until;
39
+ for (let i = 0; i < args.length; i += 1) {
40
+ const argument = args[i];
41
+ if (argument === UNTIL_FLAG || argument.startsWith(`${UNTIL_FLAG}=`)) {
42
+ if (until !== undefined) {throw new Error(`${UNTIL_FLAG} given twice\n${PIN_USAGE}`);}
43
+ until = argument === UNTIL_FLAG ? args[(i += 1)] : argument.slice(UNTIL_FLAG.length + 1);
44
+ } else if (argument.startsWith('--')) {
45
+ throw new Error(`unexpected argument ${JSON.stringify(argument)}\n${PIN_USAGE}`);
46
+ } else {
47
+ positional.push(argument);
48
+ }
49
+ }
50
+ return { positional, until };
51
+ }
52
+
53
+ const definitionPath = (id) => join(editionsRoot(), id, 'xema-distribution.json');
54
+
55
+ /** The file text and its parsed document; the text is what a rewrite preserves. */
56
+ function readDefinition(id) {
57
+ assertCanonicalDistributionId(id);
58
+ const path = definitionPath(id);
59
+ let text;
60
+ try {
61
+ text = readFileSync(path, 'utf8');
62
+ } catch {
63
+ throw new Error(`edition "${id}" not found: ${path} does not exist`);
64
+ }
65
+ return { path, text, document: readStrictJsonFile(path) };
66
+ }
67
+
68
+ /** The indent unit (a tab or the spaces of the first indented line) and the trailing newline of `text`. */
69
+ function formatOf(text) {
70
+ const indent = /^([ \t]+)\S/mu.exec(text)?.[1] ?? ' ';
71
+ return { indent, trailingNewline: text.endsWith('\n') };
72
+ }
73
+
74
+ /** `document` serialized in the format of the file it was read from. */
75
+ function render(document, text) {
76
+ const { indent, trailingNewline } = formatOf(text);
77
+ return `${JSON.stringify(document, null, indent)}${trailingNewline ? '\n' : ''}`;
78
+ }
79
+
80
+ /** The document with `sourcePins` set (appended last when new, in place when present) or, for none, removed. */
81
+ function withSourcePins(document, sourcePins) {
82
+ if (sourcePins.length === 0) {
83
+ const { [SOURCE_PINS_KEY]: _removed, ...rest } = document;
84
+ return rest;
85
+ }
86
+ return { ...document, [SOURCE_PINS_KEY]: sourcePins };
87
+ }
88
+
89
+ /** Every file under `directory` (relative path to bytes), the pre-command state a failed command returns to. */
90
+ function snapshotDirectory(directory) {
91
+ const files = new Map();
92
+ const walk = (current) => {
93
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
94
+ const path = join(current, entry.name);
95
+ if (entry.isDirectory()) {walk(path);} else {files.set(path, readFileSync(path));}
96
+ }
97
+ };
98
+ walk(directory);
99
+ return files;
100
+ }
101
+
102
+ /** Return `directory` to `snapshot` byte for byte: rewrite what changed, delete what the command added. */
103
+ function restoreDirectory(directory, snapshot) {
104
+ const walk = (current) => {
105
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
106
+ const path = join(current, entry.name);
107
+ if (entry.isDirectory()) {walk(path);} else if (!snapshot.has(path)) {rmSync(path);}
108
+ }
109
+ };
110
+ walk(directory);
111
+ for (const [path, bytes] of snapshot) {
112
+ if (!existsSync(path) || !readFileSync(path).equals(bytes)) {
113
+ mkdirSync(dirname(path), { recursive: true });
114
+ writeFileSync(path, bytes);
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Write the definition and re-resolve, all or nothing: on any failure the
121
+ * edition directory (definition and every lock the resolve wrote) is restored
122
+ * to its exact prior bytes and the original error is rethrown.
123
+ */
124
+ async function writeAndReresolve({ id, path, rendered }) {
125
+ const directory = dirname(path);
126
+ const snapshot = snapshotDirectory(directory);
127
+ try {
128
+ writeFileSync(path, rendered);
129
+ await writeResolvedLockSet(id);
130
+ } catch (error) {
131
+ restoreDirectory(directory, snapshot);
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Hold `repo` of edition `id` at `ref` until `until`, then re-resolve it. An
138
+ * existing hold of the same repository is replaced.
139
+ */
140
+ export async function pinEdition({ id, repo, ref, until, schemas, log = console.log }) {
141
+ const { path, text, document } = readDefinition(id);
142
+ const current = parseEditionSourcePins(document[SOURCE_PINS_KEY], id);
143
+ const entry = parseEditionSourcePins([{ repo, ref, until }], id)[0];
144
+ const sourcePins = current.some((pin) => pin.repo === repo)
145
+ ? current.map((pin) => (pin.repo === repo ? entry : pin))
146
+ : [...current, entry];
147
+ const committed = schemas.parseRuntimeImageSourceIndex(readStrictJsonFile(runtimeImageSourceIndexPath()));
148
+ effectiveRepositoryPins(committed.repositoryPins, sourcePins, { id, contractAuthority: committed.contractAuthority });
149
+ assertEditionPinsAuthorized({ id, sourcePins: [entry], ledger: loadBuildLedger() });
150
+ await writeAndReresolve({ id, path, rendered: render(withSourcePins(document, sourcePins), text) });
151
+ log(`[edition:pin] ${id}: holding ${repo} @${ref.slice(0, 12)} until: ${until}`);
152
+ return sourcePins;
153
+ }
154
+
155
+ /** Remove the hold on `repo` (or every hold) of edition `id`, then re-resolve it. */
156
+ export async function unpinEdition({ id, repo, log = console.log }) {
157
+ const { path, text, document } = readDefinition(id);
158
+ const current = parseEditionSourcePins(document[SOURCE_PINS_KEY], id);
159
+ if (current.length === 0) {
160
+ log(`[edition:unpin] ${id}: holds nothing`);
161
+ return current;
162
+ }
163
+ if (repo !== undefined && !current.some((pin) => pin.repo === repo)) {
164
+ throw new Error(`edition "${id}" does not hold ${repo} (it holds: ${current.map((pin) => pin.repo).join(', ')})`);
165
+ }
166
+ const sourcePins = repo === undefined ? [] : current.filter((pin) => pin.repo !== repo);
167
+ await writeAndReresolve({ id, path, rendered: render(withSourcePins(document, sourcePins), text) });
168
+ log(`[edition:unpin] ${id}: released ${repo ?? current.map((pin) => pin.repo).join(', ')}`);
169
+ return sourcePins;
170
+ }
171
+
172
+ export async function runPin(args) {
173
+ const { positional, until } = parsePinArgs(args);
174
+ if (positional.length !== 3 || until === undefined) {throw new Error(PIN_USAGE);}
175
+ const [id, repo, ref] = positional;
176
+ await pinEdition({ id, repo, ref, until, schemas: await loadKernelDistributionSchemas() });
177
+ }
178
+
179
+ export async function runUnpin(args) {
180
+ const { positional, until } = parsePinArgs(args);
181
+ if (positional.length < 1 || positional.length > 2 || until !== undefined) {throw new Error(UNPIN_USAGE);}
182
+ await unpinEdition({ id: positional[0], repo: positional[1] });
183
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // `xema-edition pin <edition> <repo> <ref> --until "<exit condition>"`: hold one
3
+ // repository of a vendor edition at an exact commit, then re-resolve the edition.
4
+ // The logic is edition-pin-verbs.mjs; see edition-source-pins.mjs for the trust it applies.
5
+ //
6
+ // usage: xema-edition pin <edition> <repo> <ref> --until "<exit condition>" --data-root <dir> --editions-root <dir> --sources-root <dir> --kernel <dir>
7
+
8
+ import { runPin } from './edition-pin-verbs.mjs';
9
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
10
+
11
+ if (isEntryModule(import.meta.url)) {
12
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
13
+ runPin(process.argv.slice(2)).catch((error) => {
14
+ console.error(`[edition:pin] ${error.message}`);
15
+ process.exit(1);
16
+ });
17
+ }
@@ -0,0 +1,116 @@
1
+ // edition-pinned-inputs — the ONE per-edition source of the indexes an edition
2
+ // resolves from, and of the repository pins it resolves at.
3
+ //
4
+ // An edition that holds no repository (no `sourcePins`, edition-source-pins.mjs)
5
+ // reads the committed data-root files exactly as before: nothing is rebuilt.
6
+ // An edition that holds repositories has its three indexes rebuilt at ITS
7
+ // effective pins by the release builders (indexes-at-pins.mjs), after every
8
+ // held commit is authorized. Every per-edition reader (resolve, images, check,
9
+ // seal, catalogue, biome manifests) takes its indexes and pins from here, so a
10
+ // held edition carries the held commit everywhere and none can read the shared
11
+ // pin by the back door.
12
+ //
13
+ // A develop snapshot replaces ALL pins and does not come through here:
14
+ // `sourcePins` do not apply on the develop channel.
15
+
16
+ import { createHash } from 'node:crypto';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+
19
+ import { assertEditionPinsAuthorized, effectiveRepositoryPins, EditionSourcePinsError, readKernelEditionSourcePins } from './edition-source-pins.mjs';
20
+ import { biomeIndexPath, dataRoot, editionsRoot, platformServiceIndexPath, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
21
+ import { buildIndexesAtPins } from './indexes-at-pins.mjs';
22
+ import { commandFor } from './remedy-command.mjs';
23
+ import { committedRepositoryPins, loadBuildLedger } from './source-authority.mjs';
24
+ import { readStrictJsonFile } from './strict-json.mjs';
25
+
26
+ function readCommitted(path, remedy) {
27
+ if (!existsSync(path)) {
28
+ throw new Error(`${path} not found — ${remedy}`);
29
+ }
30
+ return readStrictJsonFile(path);
31
+ }
32
+
33
+ /**
34
+ * The bypass guard: the rebuilt runtime index must carry exactly the effective
35
+ * pins on both axes. A builder that re-decided a held repository would
36
+ * otherwise resolve it at a commit nobody authorized.
37
+ */
38
+ function assertRebuiltPinsAreEffective({ id, runtimeImageSourceIndex, repositoryPins }) {
39
+ const rebuilt = new Map(runtimeImageSourceIndex.repositoryPins.map((pin) => [pin.repo, pin]));
40
+ const mismatches = repositoryPins.filter((pin) => {
41
+ const actual = rebuilt.get(pin.repo);
42
+ return actual === undefined || actual.sourceCommit !== pin.sourceCommit || actual.vendoredSourceCommit !== pin.vendoredSourceCommit;
43
+ });
44
+ if (mismatches.length > 0 || rebuilt.size !== repositoryPins.length) {
45
+ throw new EditionSourcePinsError(
46
+ `edition "${id}": the indexes rebuilt at its held pins do not carry the effective repository pins ` +
47
+ `(${mismatches.map((pin) => pin.repo).join(', ') || 'repository set differs'}); refusing to resolve`,
48
+ );
49
+ }
50
+ }
51
+
52
+ const memo = new Map();
53
+
54
+ async function heldInputs({ id, sourcePins, committed, schemas }) {
55
+ const repositoryPins = effectiveRepositoryPins(committed.repositoryPins, sourcePins, { id, contractAuthority: committed.contractAuthority });
56
+ assertEditionPinsAuthorized({ id, sourcePins, ledger: loadBuildLedger() });
57
+ const built = await buildIndexesAtPins({ schemas, repositoryPins });
58
+ assertRebuiltPinsAreEffective({ id, runtimeImageSourceIndex: built.runtimeImageSourceIndex, repositoryPins });
59
+ return { ...built, repositoryPins, held: true, sourcePins };
60
+ }
61
+
62
+ /**
63
+ * An edition that holds nothing reads the committed data-root files, each ONLY
64
+ * when a reader asks for it, as each reader read its own file before: a
65
+ * resolve needs no runtime image source index, a seal needs no platform-service
66
+ * index. A missing file refuses at the point of use, naming itself.
67
+ */
68
+ function committedInputs({ schemas, sourcePins }) {
69
+ const cache = new Map();
70
+ const once = (name, read) => {
71
+ if (!cache.has(name)) {cache.set(name, read());}
72
+ return cache.get(name);
73
+ };
74
+ return {
75
+ get runtimeImageSourceIndex() {
76
+ return once('runtime', () => schemas.parseRuntimeImageSourceIndex(readStrictJsonFile(runtimeImageSourceIndexPath())));
77
+ },
78
+ get biomeIndex() {
79
+ return once('biome', () => readCommitted(biomeIndexPath(), `run \`${commandFor('build-biome-index.mjs')}\` first`));
80
+ },
81
+ get platformServiceIndex() {
82
+ return once('platform', () =>
83
+ schemas.parsePlatformServiceSourceIndex(
84
+ readCommitted(platformServiceIndexPath(), 'run the trusted platform-service index generator first'),
85
+ ),
86
+ );
87
+ },
88
+ get repositoryPins() {
89
+ return effectiveRepositoryPins(committedRepositoryPins(), sourcePins);
90
+ },
91
+ held: false,
92
+ sourcePins,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * @param {string} id the edition
98
+ * @param {{ schemas: object }} kernel the loaded kernel distribution schemas
99
+ * @returns {Promise<{ runtimeImageSourceIndex, biomeIndex, platformServiceIndex, repositoryPins, held: boolean, sourcePins }>}
100
+ *
101
+ * The held (rebuilt) result is memoized per configured roots, edition, declared
102
+ * pins and committed runtime index, so one process pays the rebuild once.
103
+ */
104
+ export async function editionPinnedInputs(id, { schemas }) {
105
+ const sourcePins = readKernelEditionSourcePins(id, schemas.parseDistribution);
106
+ if (sourcePins.length === 0) {return committedInputs({ schemas, sourcePins });}
107
+ const indexText = readFileSync(runtimeImageSourceIndexPath(), 'utf8');
108
+ const committed = schemas.parseRuntimeImageSourceIndex(readStrictJsonFile(runtimeImageSourceIndexPath()));
109
+ const key = JSON.stringify([dataRoot(), editionsRoot(), sourcesRoot(), id, sourcePins, createHash('sha256').update(indexText).digest('hex')]);
110
+ if (!memo.has(key)) {
111
+ const pending = heldInputs({ id, sourcePins, committed, schemas });
112
+ memo.set(key, pending);
113
+ pending.catch(() => memo.delete(key));
114
+ }
115
+ return memo.get(key);
116
+ }