@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.
- package/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- 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
|
+
}
|