@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,49 @@
1
+ // develop-channel-record — a member's build record read through a CHANNEL TAG (`<records repository>:<tag>`, e.g.
2
+ // `:develop`), the tag the producer's CI moves forward to its newest green record.
3
+ //
4
+ // The dev lane trusts the channel tag, not a signature: records are not signed there. One manifest is read by tag
5
+ // (never a tag listing: the pull-through proxy serves single tags only), the record is parsed against the kernel's
6
+ // schema and held to the member (its producer repository, its branch). There is no git ancestry: the tag only moves
7
+ // forward. A tag that does not exist, or a record that does not hold, is a `RecordRefused` naming the member.
8
+
9
+ import { assertRecordBranch, memberOwnerName, producerClaims, readRecord, recordsClientOf } from './develop-build-record.mjs';
10
+ import { RecordRefused } from './record-refused.mjs';
11
+
12
+ /**
13
+ * @param {object} request
14
+ * @param {object} request.kernel the kernel's distribution exports (requireBuildRecordKernel)
15
+ * @param {(authority: string) => object} request.clientFor
16
+ * @param {{repo: string, repositoryUrl: string}} request.member
17
+ * @param {string} request.branch
18
+ * @param {string} request.channel the tag
19
+ * @returns {Promise<{record: object, snapshot: string, repository: string}>} `snapshot` is `<run>.<attempt>`
20
+ */
21
+ export async function loadChannelRecord({ kernel, clientFor, member, branch, channel }) {
22
+ const { repo } = member;
23
+ const { ref, repository, client } = recordsClientOf({ clientFor, member });
24
+ const { record } = await readRecord({ kernel, client, repository, repo, tag: channel, absentIsRefusal: true });
25
+ const { snapshot } = producerClaims({ record, repo, memberRepository: memberOwnerName(member.repositoryUrl) });
26
+ assertRecordBranch({ record, repo, tag: channel, branch });
27
+ return { record, snapshot, repository: ref };
28
+ }
29
+
30
+ /**
31
+ * The record a selection PINNED for a member, read by its own immutable record tag (`buildRecordTag`, e.g.
32
+ * `develop-3038-1`), never through the moving channel tag: `snapshot` is the `<run>.<attempt>` the selection wrote.
33
+ * The record must be that run's (its producer claims the same snapshot) and of the branch. A channel that moved after
34
+ * selection does not touch this read.
35
+ *
36
+ * @returns {Promise<{record: object, repository: string, tag: string}>}
37
+ */
38
+ export async function loadSelectedRecord({ kernel, clientFor, member, branch, snapshot }) {
39
+ const { repo } = member;
40
+ const run = /^(\d+)\.(\d+)$/.exec(String(snapshot));
41
+ if (run === null) {throw new RecordRefused(repo, `the selection's snapshot ${JSON.stringify(snapshot)} is not <run>.<attempt>`);}
42
+ const tag = kernel.buildRecordTag({ branch, producer: { kind: 'github-actions', runNumber: Number(run[1]), runAttempt: Number(run[2]) } });
43
+ const { ref, repository, client } = recordsClientOf({ clientFor, member });
44
+ const { record } = await readRecord({ kernel, client, repository, repo, tag, absentIsRefusal: true });
45
+ const claims = producerClaims({ record, repo, memberRepository: memberOwnerName(member.repositoryUrl) });
46
+ if (claims.snapshot !== snapshot) {throw new RecordRefused(repo, `record ${ref}:${tag} is run ${claims.snapshot}, not the selected ${snapshot}`);}
47
+ assertRecordBranch({ record, repo, tag, branch });
48
+ return { record, repository: ref, tag };
49
+ }
@@ -0,0 +1,65 @@
1
+ // develop-channel-satellites — the satellite check of the dev lane (`--channel <tag>`), a consistency check, not a
2
+ // signature check.
3
+ //
4
+ // It requires a satellite entry's {name, digest} to be listed in the producer build record the selection PINNED, read
5
+ // by that record's own tag (develop-channel-record.mjs `loadSelectedRecord`), at the commit the snapshot pins for the
6
+ // producer. The channel tag is never read here: a channel that moved after selection (a newer green build) does not
7
+ // affect the run. Every refused claim is named at once; a registry that could not answer is a plain Error.
8
+
9
+ import { withChannelReader } from './develop-build-record.mjs';
10
+ import { loadSelectedRecord } from './develop-channel-record.mjs';
11
+ import { firstPartySources } from './first-party-source-ids.mjs';
12
+ import { ImageBuildRecordError, satelliteProducerOf, subjectKeyOf } from './images-from-record.mjs';
13
+ import { RecordRefused } from './record-refused.mjs';
14
+
15
+ const CHANNEL_BRANCH = 'develop';
16
+
17
+ /**
18
+ * The producer as a record member: its `owner/name`, the URL its record's producer repository is held to and, when
19
+ * its declared source declares a develop store, the root its records live under.
20
+ */
21
+ const producerMember = (producer, { developImageRegistry }) => ({
22
+ repo: producer,
23
+ repositoryUrl: `https://github.com/${producer}.git`,
24
+ ...(developImageRegistry === undefined ? {} : { developImageRegistry }),
25
+ });
26
+
27
+ /**
28
+ * @param {object} request
29
+ * @param {object} [request.declaredSources] `firstPartySources` (tests pass it): a producer's declared `developImageRegistry` holds its records
30
+ * @param {string} request.channel the tag
31
+ * @param {object} request.selection the parsed selection (`snapshot`: source repo -> `<run>.<attempt>`): each satellite
32
+ * is checked against the record it pinned, never the channel's current tip
33
+ * @param {Function} [request.withReader] the kernel and registry client scope (tests replace it)
34
+ * @returns {(claims: {subject: object, entry: object}[]) => Promise<void>}
35
+ */
36
+ export function createChannelSatelliteVerifier({ channel, selection, env = process.env, withReader = withChannelReader, declaredSources = firstPartySources('locating a satellite producer\'s build records'), ...readerInjections }) {
37
+ return async (claims) => {
38
+ if (claims.length === 0) {return;}
39
+ await withReader({ env, ...readerInjections }, async ({ kernel, clientFor }) => {
40
+ const refused = [];
41
+ for (const { subject, entry } of claims) {
42
+ try {
43
+ const producer = satelliteProducerOf(subject);
44
+ const member = producerMember(producer, declaredSources[subject.repo]);
45
+ const snapshot = selection.snapshot?.[subject.repo];
46
+ if (snapshot === undefined) {throw new RecordRefused(producer, `the selection pins no record of ${subject.repo}`);}
47
+ const { record, repository, tag } = await loadSelectedRecord({ kernel, clientFor, member, branch: CHANNEL_BRANCH, snapshot });
48
+ const named = `the selected record ${repository}:${tag}`;
49
+ if (record.commit !== entry.sourceSha) {
50
+ throw new RecordRefused(producer, `${named} is at ${record.commit}, but the snapshot pins ${entry.sourceSha}`);
51
+ }
52
+ if (!record.images.some((listed) => listed.name === entry.repository && listed.digest === entry.digest)) {
53
+ throw new RecordRefused(producer, `${named} does not list ${entry.repository}@${entry.digest}`);
54
+ }
55
+ } catch (error) {
56
+ if (!(error instanceof RecordRefused)) {throw error;}
57
+ refused.push(`${subjectKeyOf(subject)}: ${error.message}`);
58
+ }
59
+ }
60
+ if (refused.length > 0) {
61
+ throw new ImageBuildRecordError(`${refused.length} satellite image(s) of the build record are not verified against channel ${channel}:\n ${refused.join('\n ')}`);
62
+ }
63
+ });
64
+ };
65
+ }
@@ -0,0 +1,96 @@
1
+ // develop-chart-roster — the develop chart roster of a deploy checkout, as the
2
+ // deploy repository itself computes it.
3
+ //
4
+ // A develop chart's version is `0.0.0-h<16 hex>`, the first 16 hex of its full
5
+ // content digest (xema-deploy scripts/lib/chart-content-digest.mjs,
6
+ // developChartVersionFromDigest). That function belongs to the deploy
7
+ // repository, and so does the index version derived from the roster: publish-
8
+ // charts on refs/heads/develop runs the same code to publish the charts and
9
+ // close the set with its `xema-chart-set` index. So the CLI never reimplements
10
+ // or vendors it: it runs the entry point of the frozen deploy checkout
11
+ // (`scripts/chart-content-digest.mjs --develop-roster --json`), which is the
12
+ // code the publisher ran for that commit, and validates the one document it
13
+ // prints. A vendored copy would pin the CLI to one revision of a hash whose
14
+ // owner is the deploy commit being resolved.
15
+
16
+ import { execFileSync } from 'node:child_process';
17
+ import { join } from 'node:path';
18
+
19
+ import { parseStrictJson } from './strict-json.mjs';
20
+
21
+ /** The schema string the deploy entry point stamps on its develop roster. */
22
+ export const DEVELOP_CHART_ROSTER_SCHEMA = 'xema-develop-chart-roster/1';
23
+
24
+ /** The entry point, relative to the deploy checkout, and its arguments. */
25
+ export const DEVELOP_CHART_ROSTER_ENTRY = 'scripts/chart-content-digest.mjs';
26
+ export const DEVELOP_CHART_ROSTER_ARGS = Object.freeze(['--develop-roster', '--json']);
27
+
28
+ /** A develop channel coordinate: revision 0 and the content digest's first 16 hex. */
29
+ export const DEVELOP_CHART_VERSION_PATTERN = /^0\.0\.0-h[0-9a-f]{16}$/;
30
+
31
+ /** A chart name as Helm and the registry address it. */
32
+ const CHART_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
33
+
34
+ const ROSTER_KEYS = 'indexVersion,members,schema';
35
+ const MEMBER_KEYS = 'name,version';
36
+
37
+ /**
38
+ * Validate the develop roster document `document` (from `source`) and return
39
+ * `{indexVersion, members: [{name, version}]}`. Every rule is strict: the exact
40
+ * schema string and keys, develop coordinates only, and members sorted by name
41
+ * with no name twice.
42
+ */
43
+ export function parseDevelopChartRoster(document, source) {
44
+ const fail = (why) => {
45
+ throw new Error(`${source} is not a ${DEVELOP_CHART_ROSTER_SCHEMA} roster: ${why}`);
46
+ };
47
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {fail('it is not a JSON object');}
48
+ if (document.schema !== DEVELOP_CHART_ROSTER_SCHEMA) {fail(`schema must be "${DEVELOP_CHART_ROSTER_SCHEMA}" (got ${JSON.stringify(document.schema)})`);}
49
+ const keys = Object.keys(document).sort().join(',');
50
+ if (keys !== ROSTER_KEYS) {fail(`it must carry exactly ${ROSTER_KEYS} (got ${keys})`);}
51
+ if (!DEVELOP_CHART_VERSION_PATTERN.test(String(document.indexVersion))) {
52
+ fail(`indexVersion must be a develop coordinate 0.0.0-h<16 hex> (got ${JSON.stringify(document.indexVersion)})`);
53
+ }
54
+ if (!Array.isArray(document.members) || document.members.length === 0) {fail('members must be a non-empty array');}
55
+ let previous = '';
56
+ const members = document.members.map((member, position) => {
57
+ if (!member || typeof member !== 'object' || Array.isArray(member) || Object.keys(member).sort().join(',') !== MEMBER_KEYS) {
58
+ fail(`member ${position} must be exactly {name, version}`);
59
+ }
60
+ if (!CHART_NAME_PATTERN.test(String(member.name))) {fail(`member ${position} has a malformed chart name ${JSON.stringify(member.name)}`);}
61
+ if (!DEVELOP_CHART_VERSION_PATTERN.test(String(member.version))) {
62
+ fail(`${member.name} has no develop coordinate 0.0.0-h<16 hex> (got ${JSON.stringify(member.version)})`);
63
+ }
64
+ if (member.name <= previous) {fail(`members must be sorted by name with no name twice (${member.name} follows ${previous})`);}
65
+ previous = member.name;
66
+ return { name: member.name, version: member.version };
67
+ });
68
+ return { indexVersion: document.indexVersion, members };
69
+ }
70
+
71
+ /**
72
+ * The develop roster of the deploy checkout at `deployDir`: its own entry point
73
+ * run with this node, in that checkout, with only PATH from the environment (the
74
+ * entry point needs git and nothing else; no credential reaches it).
75
+ */
76
+ export function readDevelopChartRoster(deployDir) {
77
+ const entry = join(deployDir, DEVELOP_CHART_ROSTER_ENTRY);
78
+ let stdout;
79
+ try {
80
+ stdout = execFileSync(process.execPath, [entry, ...DEVELOP_CHART_ROSTER_ARGS], {
81
+ cwd: deployDir,
82
+ encoding: 'utf8',
83
+ env: { PATH: process.env.PATH ?? '' },
84
+ maxBuffer: 16 * 1024 * 1024,
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ });
87
+ } catch (error) {
88
+ const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
89
+ throw new Error(
90
+ `the deploy checkout's develop roster failed (node ${DEVELOP_CHART_ROSTER_ENTRY} ${DEVELOP_CHART_ROSTER_ARGS.join(' ')} in ${deployDir}): ${stderr || error.message}`,
91
+ { cause: error },
92
+ );
93
+ }
94
+ const source = `${DEVELOP_CHART_ROSTER_ENTRY} ${DEVELOP_CHART_ROSTER_ARGS.join(' ')}`;
95
+ return parseDevelopChartRoster(parseStrictJson(stdout, source), source);
96
+ }
@@ -0,0 +1,147 @@
1
+ // develop-chart-set — the chart-set lock of a develop edition.
2
+ //
3
+ // A develop edition renders the charts of its frozen deploy commit. The deploy
4
+ // checkout computes its own develop roster (develop-chart-roster.mjs: every
5
+ // chart at `0.0.0-h<16 hex>` and the set's index version), and xema-deploy
6
+ // publish-charts on refs/heads/develop publishes those charts and closes the
7
+ // set with a `xema-chart-set` index at that index version, the same artifact
8
+ // a release closes its set with. So a develop lock is resolved exactly as a
9
+ // release one is (resolve-chart-set.mjs), only at a version the deploy commit
10
+ // names instead of the newest the registry holds: read the index at the
11
+ // roster's index version, require its members to be the roster, seal every
12
+ // member against the registry, and build the same schemaVersion 4 lock. The
13
+ // committed release lock is never read.
14
+ //
15
+ // TWO PUBLISHERS. A develop lock lists the publisher whose repository is the
16
+ // deploy pin role's (this resolution) followed by the release infra publisher,
17
+ // taken verbatim from the data root's committed chart-set.lock.json. Infra
18
+ // charts have no develop channel: the committed release entry is the only
19
+ // published infra set, and the deploy engine plans and applies scope infra on
20
+ // every channel, so a develop deployment needs it to move infra (pgbouncer,
21
+ // openfga) through the pipeline. The data root pin is already in the
22
+ // develop tag, so a moved infra entry moves the tag. A committed lock that is
23
+ // absent, or lacks exactly one infra publisher, is refused: no fallback, no
24
+ // develop-without-infra mode.
25
+ //
26
+ // NOT PUBLISHED IS A REFUSAL. An index or member the registry does not serve
27
+ // (404) is refused by name and version: publish-charts for the deploy commit
28
+ // has not finished green. There is no fallback to any other set.
29
+
30
+ import { basename } from 'node:path';
31
+
32
+ import { RefusalError } from './collected-refusals.mjs';
33
+ import { readDevelopChartRoster } from './develop-chart-roster.mjs';
34
+ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
35
+ import { deployRoot } from './edition-workspace.mjs';
36
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
37
+ import { deployPinKey } from './pin-source.mjs';
38
+ import {
39
+ CHART_SET_LOCK_FILE,
40
+ buildLock,
41
+ chartSetLockPath,
42
+ chartRegistryClient,
43
+ chartSetPublishers,
44
+ indexRosterAndDigest,
45
+ readLock,
46
+ registryChartDigest,
47
+ sealRoster,
48
+ } from './resolve-chart-set.mjs';
49
+
50
+ const NOT_FOUND = 404;
51
+
52
+ /** The publisher whose repository is the deploy pin role's; exactly one must be configured. */
53
+ function deployPublisher() {
54
+ const deployRepo = basename(deployPinKey());
55
+ const publishers = chartSetPublishers().filter((publisher) => basename(publisher.repo) === deployRepo);
56
+ if (publishers.length !== 1) {
57
+ throw new RefusalError(
58
+ `${publishers.length} configured chart publishers have repository ${deployRepo} (the deploy pin role); exactly one must, ` +
59
+ 'since a develop chart set is that publisher\'s set at the deploy commit',
60
+ );
61
+ }
62
+ return publishers[0];
63
+ }
64
+
65
+ /** The configured publishers that are not the deploy pin role's: the infra publisher(s) a release lock carries. */
66
+ function infraPublisherIndexes() {
67
+ const deployRepo = basename(deployPinKey());
68
+ return chartSetPublishers().filter((publisher) => basename(publisher.repo) !== deployRepo).map((publisher) => publisher.index);
69
+ }
70
+
71
+ /** The ONE infra publisher entry of the data root's committed release lock, unchanged. */
72
+ function committedInfraPublisher() {
73
+ const path = chartSetLockPath();
74
+ const committed = readLock(path);
75
+ if (committed === null) {
76
+ throw new RefusalError(`${path} is absent or unreadable; a develop edition carries the release infra chart set from it`);
77
+ }
78
+ assertRosteredChartSetLock(committed, path);
79
+ const indexes = infraPublisherIndexes();
80
+ const found = committed.publishers.filter((publisher) => indexes.includes(publisher.index));
81
+ if (indexes.length !== 1 || found.length !== 1) {
82
+ throw new RefusalError(
83
+ `${path} carries ${found.length} infra publisher entries (${indexes.length} infra publishers configured); exactly one is required`,
84
+ );
85
+ }
86
+ return found[0];
87
+ }
88
+
89
+ /** Run `read`; a registry 404 becomes the "not published yet" refusal naming `what` and the publisher's run for the deploy commit. */
90
+ async function published(read, what, { publisher, xemaDeploySha }) {
91
+ try {
92
+ return await read();
93
+ } catch (error) {
94
+ if (error?.status !== NOT_FOUND) {throw error;}
95
+ throw new RefusalError(`${what} is not published yet; ${publisher.repo} publish-charts for ${xemaDeploySha} not green`, { cause: error });
96
+ }
97
+ }
98
+
99
+ /** Refuse an index whose members are not exactly the roster's names at the roster's versions. */
100
+ function assertIndexIsRoster(indexRoster, members, label) {
101
+ const expected = members.map(({ name, version }) => `${name} ${version}`);
102
+ const actual = Object.entries(indexRoster).map(([name, member]) => `${name} ${member.version}`);
103
+ const missing = expected.filter((line) => !actual.includes(line));
104
+ const extra = actual.filter((line) => !expected.includes(line));
105
+ if (missing.length > 0 || extra.length > 0) {
106
+ throw new RefusalError(
107
+ `${label} does not carry the deploy checkout's develop roster: ` +
108
+ `missing [${missing.join(', ')}], unexpected [${extra.join(', ')}]`,
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * The develop chart-set lock for the deploy commit `xemaDeploySha`, whose frozen
115
+ * checkout is `deployDir`. `readRoster(deployDir)` returns `{indexVersion, members}`;
116
+ * `client` is the chart registry client the release resolver uses.
117
+ */
118
+ export async function resolveDevelopChartSet({ xemaDeploySha, deployDir = deployRoot(), readRoster = readDevelopChartRoster, client }) {
119
+ if (!COMMIT_PATTERN.test(xemaDeploySha ?? '')) {
120
+ throw new RefusalError(`the deploy checkout SHA is not a 40-hex commit: ${JSON.stringify(xemaDeploySha)}`);
121
+ }
122
+ const publisher = deployPublisher();
123
+ const infraPublisher = committedInfraPublisher();
124
+ const { indexVersion, members } = readRoster(deployDir);
125
+ const registry = client ?? chartRegistryClient();
126
+ const label = `${publisher.index}@${indexVersion}`;
127
+ const { roster, indexDigest } = await published(
128
+ () => indexRosterAndDigest(registry, publisher.index, indexVersion),
129
+ `the chart-set index ${label}`,
130
+ { publisher, xemaDeploySha },
131
+ );
132
+ assertIndexIsRoster(roster, members, label);
133
+ const lookup = registryChartDigest(registry);
134
+ const charts = await sealRoster(
135
+ roster,
136
+ (name, version) => published(() => lookup(name, version), `chart ${name}:${version}`, { publisher, xemaDeploySha }),
137
+ label,
138
+ );
139
+ const lock = buildLock(
140
+ { [publisher.index]: indexVersion },
141
+ { [publisher.index]: charts },
142
+ { [publisher.index]: indexDigest },
143
+ [publisher],
144
+ );
145
+ lock.publishers.push(infraPublisher);
146
+ return assertRosteredChartSetLock(lock, CHART_SET_LOCK_FILE);
147
+ }
@@ -0,0 +1,29 @@
1
+ // develop-current-resolution — the members that keep the CURRENT develop resolution instead of a signed build record.
2
+ //
3
+ // A member keeps its current resolution (develop HEAD freeze plus the registry probe) only when the data root declares
4
+ // it: `currentResolution: "<ticket>"` on its `firstPartySources` entry in edition-workspace.json, the ticket that
5
+ // removes the exception (today xema-docs, whose develop lane is unsigned by owner decision and which leaves the
6
+ // edition). It is a declared exception, not a fallback: absent means none, and every other member without a record
7
+ // still refuses.
8
+
9
+ import { compareStrings } from './lib.mjs';
10
+
11
+ /** Whether a member (or its `firstPartySources` entry) keeps its current resolution. */
12
+ export const keepsCurrentResolution = (member) => Object.hasOwn(member, 'currentResolution');
13
+
14
+ /** The graph `members` that declare a current resolution, repo -> ticket, keys sorted: what a selection records. */
15
+ export function currentResolutionOf(members) {
16
+ return Object.fromEntries(
17
+ members
18
+ .filter(keepsCurrentResolution)
19
+ .map(({ repo, currentResolution }) => [repo, currentResolution])
20
+ .sort(([a], [b]) => compareStrings(a, b)),
21
+ );
22
+ }
23
+
24
+ /** Why a selection's `currentResolution` (absent = none) is not exactly the one the graph `members` declare; null when it is. */
25
+ export function currentResolutionProblem(declared, members) {
26
+ const expected = currentResolutionOf(members);
27
+ if (JSON.stringify(declared ?? {}) === JSON.stringify(expected)) {return null;}
28
+ return `"currentResolution" is ${JSON.stringify(declared)}, not ${JSON.stringify(expected)}, the members the data root declares keep their current resolution`;
29
+ }
@@ -0,0 +1,142 @@
1
+ // develop-edition-invariants — the plane invariants a DEVELOP candidate asserts
2
+ // itself, against the develop graph it froze: the capability-domain
3
+ // reservation table and biome confinement.
4
+ //
5
+ // On develop these live ONLY here. `xema-edition check-authored` reads no
6
+ // resolved edition, and `xema-edition check` (the release path) grades the
7
+ // same two invariants over the committed locks.
8
+ //
9
+ // RESERVATION. Ownership is a property of the platform, not of what an edition
10
+ // preloads (see `buildCapabilityDomainOwners`): every edition carries the table
11
+ // built from the FULL index. The release check proves it by comparing the
12
+ // committed locks with each other. A single candidate proves the same thing
13
+ // alone: its table must be exactly the one the frozen graph's full biome index
14
+ // builds, so any two candidates over one graph carry the identical table. It
15
+ // must also be non-empty (an empty table makes nothing first-party), and the
16
+ // kernel's reader must accept it (one owner per domain, valid domains).
17
+ //
18
+ // CONFINEMENT. The data root's `confinedBiomes` name biomes whose contents must
19
+ // not reach an unadmitted installation. The candidate is one graded edition:
20
+ // a declaration naming a biome the frozen index does not carry guards nothing
21
+ // and is refused; shipping a confined biome without an admission is refused; an
22
+ // admission of this edition for a biome it does not ship is a stale permission
23
+ // and is refused; a declaration without a substantive consequence is refused.
24
+
25
+ import { RefusalError } from './collected-refusals.mjs';
26
+ import { editionWorkspaceConfig } from './edition-config.mjs';
27
+ import { buildCapabilityDomainOwners, serializeJson } from './lib.mjs';
28
+
29
+ /** Why a develop candidate was refused. */
30
+ export const DevelopEditionInvariantReason = Object.freeze({
31
+ RESERVATION_TABLE_EMPTY: 'reservation-table-empty',
32
+ RESERVATION_TABLE_NOT_PLATFORM_TABLE: 'reservation-table-not-platform-table',
33
+ RESERVATION_TABLE_REFUSED_BY_KERNEL: 'reservation-table-refused-by-kernel',
34
+ CONFINED_BIOME_UNKNOWN: 'confined-biome-unknown',
35
+ CONFINED_BIOME_UNADMITTED: 'confined-biome-unadmitted',
36
+ CONFINEMENT_ADMISSION_STALE: 'confinement-admission-stale',
37
+ CONFINEMENT_CONSEQUENCE_MISSING: 'confinement-consequence-missing',
38
+ });
39
+
40
+ /** The shortest `consequence` that can name what a confinement protects. */
41
+ const MIN_CONSEQUENCE_LENGTH = 40;
42
+
43
+ /** A develop candidate broke a plane invariant. `violations` carries every one, each with its reason. */
44
+ export class DevelopEditionInvariantError extends RefusalError {
45
+ /** @param {string} id @param {{ reason: string, detail: string }[]} violations */
46
+ constructor(id, violations) {
47
+ super(
48
+ `develop edition "${id}" breaks ${violations.length} plane invariant(s):\n` +
49
+ violations.map(({ reason, detail }) => ` - [${reason}] ${detail}`).join('\n'),
50
+ );
51
+ this.name = 'DevelopEditionInvariantError';
52
+ this.distributionId = id;
53
+ this.violations = violations;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The reservation violations of `lock`, whose graph's full biome index is
59
+ * `biomeIndex`. `resolveFirstPartyCapabilityDomains` is the kernel's reader.
60
+ */
61
+ export function reservationViolations({ lock, biomeIndex, resolveFirstPartyCapabilityDomains }) {
62
+ const R = DevelopEditionInvariantReason;
63
+ const table = lock.capabilityDomainOwners;
64
+ if (!Array.isArray(table) || table.length === 0) {
65
+ return [{
66
+ reason: R.RESERVATION_TABLE_EMPTY,
67
+ detail: 'capabilityDomainOwners is missing or empty: an empty reservation table makes NOTHING first-party (fail-open)',
68
+ }];
69
+ }
70
+ const violations = [];
71
+ try {
72
+ resolveFirstPartyCapabilityDomains(lock);
73
+ } catch (err) {
74
+ violations.push({ reason: R.RESERVATION_TABLE_REFUSED_BY_KERNEL, detail: err.message });
75
+ }
76
+ if (serializeJson(table) !== serializeJson(buildCapabilityDomainOwners(biomeIndex))) {
77
+ violations.push({
78
+ reason: R.RESERVATION_TABLE_NOT_PLATFORM_TABLE,
79
+ detail:
80
+ 'capabilityDomainOwners is not the table the frozen graph\'s full biome index builds. Ownership must not vary ' +
81
+ 'by edition, or a trimmed edition leaves domains org-bindable',
82
+ });
83
+ }
84
+ return violations;
85
+ }
86
+
87
+ /**
88
+ * The confinement violations of edition `id` shipping `lock`, under the data
89
+ * root's `declarations`, with `knownBiomeIds` the frozen index's biome ids.
90
+ */
91
+ export function confinementViolations({ id, lock, declarations, knownBiomeIds }) {
92
+ const R = DevelopEditionInvariantReason;
93
+ const shipped = new Set(lock.biomes.map((biome) => biome.id));
94
+ const violations = [];
95
+ for (const decl of declarations) {
96
+ if (typeof decl.consequence !== 'string' || decl.consequence.trim().length < MIN_CONSEQUENCE_LENGTH) {
97
+ violations.push({
98
+ reason: R.CONFINEMENT_CONSEQUENCE_MISSING,
99
+ detail: `confinement declaration "${decl.biomeId}" carries no substantive \`consequence\`; a refusal would name a rule without naming what it protects`,
100
+ });
101
+ }
102
+ if (!knownBiomeIds.has(decl.biomeId)) {
103
+ violations.push({
104
+ reason: R.CONFINED_BIOME_UNKNOWN,
105
+ detail:
106
+ `confinement declaration names biome "${decl.biomeId}", which the frozen biome index does not carry: it would ` +
107
+ 'guard nothing. Repoint it to the renamed biome, or delete it if the biome is gone',
108
+ });
109
+ continue;
110
+ }
111
+ const admitted = decl.admittedEditions.includes(id);
112
+ if (shipped.has(decl.biomeId) && !admitted) {
113
+ violations.push({
114
+ reason: R.CONFINED_BIOME_UNADMITTED,
115
+ detail:
116
+ `ships biome "${decl.biomeId}", which holds ${decl.holds}. That is a CONFINEMENT decision, not a packaging one: ` +
117
+ `${decl.consequence}. If intended, add "${id}" to its admittedEditions in edition-workspace.json in its own commit, naming who agreed`,
118
+ });
119
+ }
120
+ if (!shipped.has(decl.biomeId) && admitted) {
121
+ violations.push({
122
+ reason: R.CONFINEMENT_ADMISSION_STALE,
123
+ detail: `the admission "${decl.biomeId}" -> "${id}" suppresses NOTHING: the edition does not ship the biome. Delete it`,
124
+ });
125
+ }
126
+ }
127
+ return violations;
128
+ }
129
+
130
+ /**
131
+ * Assert both invariants on develop candidate `id`. Throws
132
+ * {@link DevelopEditionInvariantError} naming every violation; returns the
133
+ * counts it graded otherwise.
134
+ */
135
+ export function assertDevelopEditionInvariants({ id, lock, biomeIndex, schemas, declarations = editionWorkspaceConfig().confinedBiomes ?? [] }) {
136
+ const violations = [
137
+ ...reservationViolations({ lock, biomeIndex, resolveFirstPartyCapabilityDomains: schemas.resolveFirstPartyCapabilityDomains }),
138
+ ...confinementViolations({ id, lock, declarations, knownBiomeIds: new Set(biomeIndex.map((entry) => entry.id)) }),
139
+ ];
140
+ if (violations.length > 0) {throw new DevelopEditionInvariantError(id, violations);}
141
+ return { reservedDomains: lock.capabilityDomainOwners.length, confinedBiomes: declarations.length };
142
+ }