@xemahq/xema 0.26.0 → 0.27.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.
- package/README.md +70 -37
- package/dist/assets/distribution-assets.manifest.json +1 -2
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +5 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +2 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +18 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +2 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +31 -7
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +1 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -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-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +14 -6
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -156
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dist-check — CI guard for the distribution packaging plane. Verifies:
|
|
3
|
+
// 1. committed biome-index.json identities/order are valid offline
|
|
4
|
+
// (`--check-sources` additionally scans a trusted clean aggregator);
|
|
5
|
+
// 2. packaging contract tests are green (closure, immutability, parity);
|
|
6
|
+
// 3. every distribution under distributions/*/xema-distribution.json resolves
|
|
7
|
+
// cleanly AND its committed distribution.lock.json matches a fresh resolve;
|
|
8
|
+
// 4. every distribution's committed images.lock.json is STRUCTURALLY in sync
|
|
9
|
+
// with the committed distribution lock — the same actual runtime subjects,
|
|
10
|
+
// complete platform bindings, and bundled web-biome set;
|
|
11
|
+
// 5. the evidence trust policy the data root declares (if any): its
|
|
12
|
+
// trusted-root digest and the data-root pin of the policy and of this
|
|
13
|
+
// CLI's verifier agree, and its authority configuration is either
|
|
14
|
+
// explicitly unarmed or fully armed (never partially configured); a data
|
|
15
|
+
// root declaring none says so, and is refused if it holds an undeclared
|
|
16
|
+
// policy.
|
|
17
|
+
//
|
|
18
|
+
// The distribution lock is compared in FULL (timestamp normalized out): its
|
|
19
|
+
// `inputHash` is deterministic over (biomes + platformServices + manifest), so
|
|
20
|
+
// any real packaging drift is caught. The image lock is compared
|
|
21
|
+
// STRUCTURALLY ONLY — `contentHash` is deliberately excluded by the default
|
|
22
|
+
// offline check. A content hash is source-derived from an exact Git commit and
|
|
23
|
+
// changes whenever that committed source path/type/mode/content changes.
|
|
24
|
+
// Uncommitted and ignored working-tree files are never inputs. Trusted
|
|
25
|
+
// regeneration verifies exact commits with `--check-sources`.
|
|
26
|
+
// It is never treated as an OCI digest or sufficient reuse evidence. Nonzero
|
|
27
|
+
// exit on any structural drift or resolver error.
|
|
28
|
+
|
|
29
|
+
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
AuthorizationLockCheck,
|
|
36
|
+
checkEditionAuthorizationLock,
|
|
37
|
+
loadKernelAuthorizationComposer,
|
|
38
|
+
} from './authorization-base.mjs';
|
|
39
|
+
import { verifyBiomeContentHashes } from './biome-content.mjs';
|
|
40
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
41
|
+
import {
|
|
42
|
+
EDITION_CHART_SET_FILE,
|
|
43
|
+
EditionChartSetCheck,
|
|
44
|
+
fleetChartSetLockPath,
|
|
45
|
+
assertRosteredChartSetLock,
|
|
46
|
+
checkEditionChartSet,
|
|
47
|
+
} from './edition-chart-set.mjs';
|
|
48
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
49
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
50
|
+
import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, kernelPackageDir, platformServiceIndexPath, runtimeImageSourceIndexPath, workspaceArgv } from './edition-workspace.mjs';
|
|
51
|
+
import { checkEvidenceTrustPolicy } from './evidence-trust-policy.mjs';
|
|
52
|
+
import {
|
|
53
|
+
compareIndexEntries,
|
|
54
|
+
comparableDistributionLock,
|
|
55
|
+
loadKernelDistributionSchemas,
|
|
56
|
+
serializeJson,
|
|
57
|
+
validateImmutableBiomeIndex,
|
|
58
|
+
validateImmutableDistributionLock,
|
|
59
|
+
validateImmutableImageLock,
|
|
60
|
+
validateServiceDependencyOwnerParity,
|
|
61
|
+
} from './lib.mjs';
|
|
62
|
+
import { commandFor } from './remedy-command.mjs';
|
|
63
|
+
import { resolveDistributionWithIndex } from './resolve-distribution.mjs';
|
|
64
|
+
import {
|
|
65
|
+
resolveDistributionDeclaredSources,
|
|
66
|
+
resolveDistributionRequiredPlatforms,
|
|
67
|
+
externalSourceTrees,
|
|
68
|
+
resolveImages,
|
|
69
|
+
resolveComposedProcessStructure,
|
|
70
|
+
resolveImageStructure,
|
|
71
|
+
sourceTreeLocator,
|
|
72
|
+
} from './resolve-images.mjs';
|
|
73
|
+
import { withoutSealedDigests } from './seal-image-digests.mjs';
|
|
74
|
+
import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
|
|
75
|
+
|
|
76
|
+
if (isEntryModule(import.meta.url)) {
|
|
77
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache, WorkspaceRoot.SourceHash]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
|
|
82
|
+
/** Strip the volatile timestamp so committed-vs-fresh locks compare cleanly. */
|
|
83
|
+
function normalize(lock) {
|
|
84
|
+
const { resolvedAt: _resolvedAt, ...rest } = lock;
|
|
85
|
+
return serializeJson(rest);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Outcome of a single committed-vs-fresh lock comparison. */
|
|
89
|
+
const LockCheck = Object.freeze({
|
|
90
|
+
GREEN: 'green',
|
|
91
|
+
MISSING: 'missing',
|
|
92
|
+
STALE: 'stale',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compare a freshly-resolved lock against its committed file (timestamp
|
|
97
|
+
* normalized out). Pure — returns a {@link LockCheck} verdict; the caller owns
|
|
98
|
+
* messaging and exit handling.
|
|
99
|
+
*
|
|
100
|
+
* ── THIS IS THE REBUILDABLE LOCK, AND THAT IS WHY A PLAIN DIFF IS VALID ─────
|
|
101
|
+
*
|
|
102
|
+
* `distribution.lock.json` and `images.lock.json` live side by side in every
|
|
103
|
+
* edition directory and are DIFFERENT CLASSES. Only one of them may be checked
|
|
104
|
+
* this way, and the reader is who gets it wrong, so it is said at the read
|
|
105
|
+
* rather than at a definition:
|
|
106
|
+
*
|
|
107
|
+
* distribution.lock.json REBUILDABLE. A fresh resolve REPRODUCES it, byte
|
|
108
|
+
* for byte once `resolvedAt` is normalised out. It
|
|
109
|
+
* records a SELECTION derived from the manifests and
|
|
110
|
+
* the pins, so regenerating it recomputes the same
|
|
111
|
+
* answer from the same inputs. Measured 2026-09-28:
|
|
112
|
+
* ZERO of the 9 committed distribution locks carries
|
|
113
|
+
* an `imageDigest`, or any `sha256:` at all.
|
|
114
|
+
*
|
|
115
|
+
* images.lock.json SEALED. A fresh resolve does NOT reproduce it —
|
|
116
|
+
* `resolve-images.mjs` emits no `imageDigest` at all
|
|
117
|
+
* and says so in its own header, and the digests are
|
|
118
|
+
* written afterwards by `seal-image-digests.mjs`.
|
|
119
|
+
* Measured 2026-09-28 across the 9 committed locks:
|
|
120
|
+
* 523 subjects, 523 of them sealed. So regenerating
|
|
121
|
+
* that file does not recompute an answer — it
|
|
122
|
+
* DISCARDS 523 approved digest bindings, and a plain
|
|
123
|
+
* regenerate-and-diff reports every one of them as
|
|
124
|
+
* drift.
|
|
125
|
+
*
|
|
126
|
+
* `checkImagesLock` below therefore subtracts the seal before comparing
|
|
127
|
+
* (`withoutSealedDigests`, plus a purely structural pass). Do not "simplify"
|
|
128
|
+
* the two reads into one shared helper: they agree on shape and disagree on
|
|
129
|
+
* the only thing that matters.
|
|
130
|
+
*/
|
|
131
|
+
async function compareCommittedLock(lockPath, fresh) {
|
|
132
|
+
if (!existsSync(lockPath)) {return LockCheck.MISSING;}
|
|
133
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
134
|
+
const raw = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
135
|
+
validateImmutableDistributionLock(raw);
|
|
136
|
+
return comparableDistributionLock(schemas, raw) === comparableDistributionLock(schemas, fresh)
|
|
137
|
+
? LockCheck.GREEN
|
|
138
|
+
: LockCheck.STALE;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check one distribution's committed distribution.lock.json against a fresh
|
|
143
|
+
* resolve. Returns true on green. The image lock is only meaningful when the
|
|
144
|
+
* distribution lock is green (resolve-images reads it), so this gates that step.
|
|
145
|
+
*
|
|
146
|
+
* Returns the INDEX the fresh lock was resolved from (platform entries plus the
|
|
147
|
+
* edition's source-walked ones) on green, `null` otherwise — the authorization
|
|
148
|
+
* check below looks the lock's biomes up in that same index.
|
|
149
|
+
*/
|
|
150
|
+
async function checkDistributionLock(id) {
|
|
151
|
+
const lockPath = join(editionsRoot(), id, 'distribution.lock.json');
|
|
152
|
+
try {
|
|
153
|
+
const { lock: fresh, index } = await resolveDistributionWithIndex(id);
|
|
154
|
+
const verdict = await compareCommittedLock(lockPath, fresh);
|
|
155
|
+
if (verdict === LockCheck.MISSING) {
|
|
156
|
+
console.error(
|
|
157
|
+
`[dist:check] ${id}: distribution.lock.json is missing. Run \`${commandFor('resolve-distribution.mjs', id)}\`.`,
|
|
158
|
+
);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (verdict === LockCheck.STALE) {
|
|
162
|
+
console.error(
|
|
163
|
+
`[dist:check] ${id}: distribution.lock.json is out of date. Run \`${commandFor('resolve-distribution.mjs', id)}\`.`,
|
|
164
|
+
);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
console.log(
|
|
168
|
+
`[dist:check] ${id}: lock up to date (${fresh.biomes.length} biomes).`,
|
|
169
|
+
);
|
|
170
|
+
return index;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`[dist:check] ${id}: resolver error — ${err.message}`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check one edition's committed `authorization.lock.json` against a fresh
|
|
179
|
+
* composition by the PINNED kernel composer, over the edition's committed
|
|
180
|
+
* distribution lock. Only meaningful once that lock is green, so the caller
|
|
181
|
+
* gates on it. Returns true on green.
|
|
182
|
+
*
|
|
183
|
+
* Like `distribution.lock.json` this is a REBUILDABLE artifact — composition
|
|
184
|
+
* is deterministic over the pinned kernel and the pinned module bytes — so the
|
|
185
|
+
* whole lock is compared. The `.fga.yaml` tests are not re-run: an identical
|
|
186
|
+
* `modelHash` is the model the sealer tested.
|
|
187
|
+
*/
|
|
188
|
+
async function checkAuthorizationLock(id, kernel, index) {
|
|
189
|
+
const editionDir = join(editionsRoot(), id);
|
|
190
|
+
const remedy = `Run \`${commandFor('resolve-distribution.mjs', id)}\` with XEMA_FGA_CLI set.`;
|
|
191
|
+
try {
|
|
192
|
+
const lock = JSON.parse(readFileSync(join(editionDir, 'distribution.lock.json'), 'utf8'));
|
|
193
|
+
const verdict = checkEditionAuthorizationLock({
|
|
194
|
+
distributionId: id,
|
|
195
|
+
editionDir,
|
|
196
|
+
lockBiomes: lock.biomes,
|
|
197
|
+
index,
|
|
198
|
+
kernel,
|
|
199
|
+
serialize: serializeJson,
|
|
200
|
+
});
|
|
201
|
+
if (verdict === AuthorizationLockCheck.GREEN) {
|
|
202
|
+
console.log(
|
|
203
|
+
kernel === null
|
|
204
|
+
? `[dist:check] ${id}: no authorization base (the pinned kernel ships no composer).`
|
|
205
|
+
: `[dist:check] ${id}: authorization.lock.json up to date (generation ${kernel.currentPlatformGeneration}).`,
|
|
206
|
+
);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const why = {
|
|
210
|
+
[AuthorizationLockCheck.MISSING]: 'authorization.lock.json is missing — the pinned kernel composes an authorization base for every edition.',
|
|
211
|
+
[AuthorizationLockCheck.STALE]: 'authorization.lock.json is out of date.',
|
|
212
|
+
[AuthorizationLockCheck.NOT_SEALABLE]: 'authorization.lock.json is committed, but the pinned kernel ships no composer that could reproduce it.',
|
|
213
|
+
}[verdict];
|
|
214
|
+
console.error(`[dist:check] ${id}: ${why} ${remedy}`);
|
|
215
|
+
return false;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`[dist:check] ${id}: authorization base error — ${err.message}`);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Structural view of an image lock: actual runtime subjects, platform bindings,
|
|
224
|
+
* and web-biome bundle inputs with the build-time `contentHash` removed. Exact
|
|
225
|
+
* repository commits remain part of the offline authority. Two locks
|
|
226
|
+
* with the same structure ship the same images and bundle the same web biomes,
|
|
227
|
+
* regardless of current source content (which the build re-hashes anyway).
|
|
228
|
+
*/
|
|
229
|
+
function imageStructure(lock) {
|
|
230
|
+
return serializeJson({
|
|
231
|
+
distributionId: lock.distributionId,
|
|
232
|
+
contractAuthority: lock.contractAuthority,
|
|
233
|
+
// The sealed `imageDigest` is removed FIRST, through the seal module's own
|
|
234
|
+
// view: a resolver never produces one, so a committed lock that has been
|
|
235
|
+
// sealed differs from a fresh resolve by exactly that field — and read raw
|
|
236
|
+
// it was reported "structurally out of date" with a remedy (re-resolve)
|
|
237
|
+
// that would have UNSEALED it. See `withoutSealedDigests`.
|
|
238
|
+
subjects: withoutSealedDigests(lock).subjects.map(
|
|
239
|
+
({ contentHash: _contentHash, sourceManifestPath: _sourceManifestPath, ...rest }) => rest,
|
|
240
|
+
),
|
|
241
|
+
webBiomes: lock.webBiomes ?? [],
|
|
242
|
+
platformServiceBindings: lock.platformServiceBindings ?? [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check one distribution's committed images.lock.json against the canonical
|
|
248
|
+
* image roster derived from its committed distribution lock. The default is
|
|
249
|
+
* structural; `checkSourceEvidence` additionally re-hashes trusted source
|
|
250
|
+
* trees. Returns true on green.
|
|
251
|
+
*
|
|
252
|
+
* ── THIS IS THE SEALED LOCK. IT IS NEVER COMPARED WHOLE ─────────────────────
|
|
253
|
+
*
|
|
254
|
+
* See `compareCommittedLock` above for the two classes. The consequence here
|
|
255
|
+
* is mechanical: neither comparison below reads the committed file whole.
|
|
256
|
+
* `imageStructure(...)` compares only the SHAPE (which subjects, which web
|
|
257
|
+
* biomes, which bindings), and the source-evidence pass compares
|
|
258
|
+
* `withoutSealedDigests(committed)` against a fresh resolve — the subtraction
|
|
259
|
+
* that makes the two sides describe the same thing. A `normalize(committed)
|
|
260
|
+
* === normalize(fresh)` here, which is exactly what the sibling check does and
|
|
261
|
+
* exactly what it looks like this should do, would report all 523 sealed
|
|
262
|
+
* subjects as stale on a clean tree and send the reader to re-resolve, which
|
|
263
|
+
* is the operation that destroys the seal.
|
|
264
|
+
*/
|
|
265
|
+
async function checkImagesLock(id, checkSourceEvidence) {
|
|
266
|
+
const imageLockPath = join(editionsRoot(), id, 'images.lock.json');
|
|
267
|
+
try {
|
|
268
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
269
|
+
// A pure READ of the committed lock: the tolerant reader, never the writer.
|
|
270
|
+
const distributionLock = validateImmutableDistributionLock(
|
|
271
|
+
schemas.parseDistributionLockForReading(
|
|
272
|
+
JSON.parse(
|
|
273
|
+
readFileSync(
|
|
274
|
+
join(editionsRoot(), id, 'distribution.lock.json'),
|
|
275
|
+
'utf8',
|
|
276
|
+
),
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
const runtimeSourceIndex = schemas.parseRuntimeImageSourceIndex(
|
|
281
|
+
JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8')),
|
|
282
|
+
);
|
|
283
|
+
const requiredPlatforms = resolveDistributionRequiredPlatforms(schemas, id);
|
|
284
|
+
// The composed-process subjects are read from each producing repository's
|
|
285
|
+
// DECLARATION at the pinned commit — the one structural input not in the
|
|
286
|
+
// committed lock — through the same function the source resolve uses, so
|
|
287
|
+
// the two cannot disagree on which composed subjects exist.
|
|
288
|
+
const structure = resolveComposedProcessStructure(
|
|
289
|
+
distributionLock,
|
|
290
|
+
resolveImageStructure(
|
|
291
|
+
distributionLock,
|
|
292
|
+
runtimeSourceIndex,
|
|
293
|
+
schemas,
|
|
294
|
+
requiredPlatforms,
|
|
295
|
+
resolveDistributionDeclaredSources(schemas, id),
|
|
296
|
+
{ biomeIndex: JSON.parse(readFileSync(biomeIndexPath(), 'utf8')) },
|
|
297
|
+
),
|
|
298
|
+
schemas,
|
|
299
|
+
requiredPlatforms,
|
|
300
|
+
);
|
|
301
|
+
const fresh = {
|
|
302
|
+
distributionId: id,
|
|
303
|
+
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
304
|
+
...structure,
|
|
305
|
+
};
|
|
306
|
+
if (!existsSync(imageLockPath)) {
|
|
307
|
+
console.error(
|
|
308
|
+
`[dist:check] ${id}: images.lock.json is missing. Run \`${commandFor('resolve-images.mjs', id)}\`.`,
|
|
309
|
+
);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
const committed = validateImmutableImageLock(
|
|
313
|
+
schemas.parseImageLock(JSON.parse(readFileSync(imageLockPath, 'utf8'))),
|
|
314
|
+
schemas,
|
|
315
|
+
);
|
|
316
|
+
if (
|
|
317
|
+
serializeJson(committed.contractAuthority) !==
|
|
318
|
+
serializeJson(runtimeSourceIndex.contractAuthority)
|
|
319
|
+
) {
|
|
320
|
+
console.error(
|
|
321
|
+
`[dist:check] ${id}: images.lock.json kernel contract authority does not exactly match runtime-image-source-index.json.`,
|
|
322
|
+
);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const inSync = imageStructure(committed) === imageStructure(fresh);
|
|
326
|
+
if (!inSync) {
|
|
327
|
+
console.error(
|
|
328
|
+
`[dist:check] ${id}: images.lock.json is structurally out of date ` +
|
|
329
|
+
`(server-image set or web-biome set differs from the distribution). ` +
|
|
330
|
+
`Run \`${commandFor('resolve-images.mjs', id)}\`.`,
|
|
331
|
+
);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (checkSourceEvidence) {
|
|
335
|
+
const { imageLock: sourceResolved } = await resolveImages(id);
|
|
336
|
+
// The same subtraction as the structural view above: the source resolve
|
|
337
|
+
// carries no digest, and the committed lock may.
|
|
338
|
+
if (
|
|
339
|
+
normalize(withoutSealedDigests(committed)) !==
|
|
340
|
+
normalize(sourceResolved)
|
|
341
|
+
) {
|
|
342
|
+
console.error(
|
|
343
|
+
`[dist:check] ${id}: images.lock.json source content hashes are out of date. ` +
|
|
344
|
+
`Run \`${commandFor('resolve-images.mjs', id)}\` only from trusted exact Git commits. ` +
|
|
345
|
+
`These hashes are source-derived tags, not OCI manifest digests.`,
|
|
346
|
+
);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
console.log(
|
|
351
|
+
`[dist:check] ${id}: images lock up to date (${fresh.subjects.length} actual runtime subjects, ${fresh.webBiomes.length} web biomes` +
|
|
352
|
+
`, ${fresh.platformServiceBindings.length} platform bindings` +
|
|
353
|
+
`, ${committed.sourceHashAlgorithm}` +
|
|
354
|
+
(checkSourceEvidence ? ', source hashes verified' : '') +
|
|
355
|
+
').',
|
|
356
|
+
);
|
|
357
|
+
return true;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(`[dist:check] ${id}: image resolver error — ${err.message}`);
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function checkCommittedRuntimeImageSourceIndex(checkSources) {
|
|
365
|
+
try {
|
|
366
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
367
|
+
schemas.parseRuntimeImageSourceIndex(
|
|
368
|
+
JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8')),
|
|
369
|
+
);
|
|
370
|
+
execFileSync(
|
|
371
|
+
process.execPath,
|
|
372
|
+
[
|
|
373
|
+
join(HERE, 'build-runtime-image-source-index.mjs'),
|
|
374
|
+
checkSources ? '--check-sources' : '--check',
|
|
375
|
+
...workspaceArgv(),
|
|
376
|
+
],
|
|
377
|
+
{ stdio: 'inherit' },
|
|
378
|
+
);
|
|
379
|
+
return true;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error(
|
|
382
|
+
`[dist:check] committed runtime-image source index FAILED: ${error.message}`,
|
|
383
|
+
);
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function checkCommittedIndex() {
|
|
389
|
+
try {
|
|
390
|
+
const index = validateImmutableBiomeIndex(
|
|
391
|
+
JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
|
|
392
|
+
);
|
|
393
|
+
const sorted = [...index].sort(compareIndexEntries);
|
|
394
|
+
if (serializeJson(index) !== serializeJson(sorted)) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
'entries are not in deterministic tier/id/target/repo order',
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const keys = new Set();
|
|
400
|
+
for (const entry of index) {
|
|
401
|
+
if (!Array.isArray(entry.serviceDependencies)) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`biome "${entry.id}" has no serviceDependencies projection`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const key = `${entry.id}\0${entry.target}\0${entry.repo}`;
|
|
407
|
+
if (keys.has(key)) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`duplicate immutable source identity for biome "${entry.id}" (${entry.target}, ${entry.repo})`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
keys.add(key);
|
|
413
|
+
}
|
|
414
|
+
console.log(
|
|
415
|
+
`[dist:check] committed biome index: green — ${index.length} immutable, deterministically ordered entries.`,
|
|
416
|
+
);
|
|
417
|
+
return true;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error(
|
|
420
|
+
`[dist:check] committed biome index FAILED: ${error.message}`,
|
|
421
|
+
);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function checkCommittedPlatformServiceIndex() {
|
|
427
|
+
try {
|
|
428
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
429
|
+
const index = schemas.parsePlatformServiceSourceIndex(
|
|
430
|
+
JSON.parse(readFileSync(platformServiceIndexPath(), 'utf8')),
|
|
431
|
+
);
|
|
432
|
+
validateServiceDependencyOwnerParity(
|
|
433
|
+
validateImmutableBiomeIndex(
|
|
434
|
+
JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
|
|
435
|
+
),
|
|
436
|
+
index,
|
|
437
|
+
);
|
|
438
|
+
execFileSync(
|
|
439
|
+
process.execPath,
|
|
440
|
+
[join(HERE, 'build-platform-service-index.mjs'), '--check', ...workspaceArgv()],
|
|
441
|
+
{ stdio: 'inherit' },
|
|
442
|
+
);
|
|
443
|
+
console.log(
|
|
444
|
+
`[dist:check] committed platform-service index: green — ${index.services.length} exact source identities.`,
|
|
445
|
+
);
|
|
446
|
+
return true;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error(
|
|
449
|
+
`[dist:check] committed platform-service index FAILED: ${error.message}`,
|
|
450
|
+
);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function discoverDistributionIds() {
|
|
456
|
+
if (!existsSync(editionsRoot())) {
|
|
457
|
+
console.error(`[dist:check] ${editionsRoot()} does not exist.`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
const ids = readdirSync(editionsRoot(), { withFileTypes: true })
|
|
461
|
+
.filter(
|
|
462
|
+
(d) =>
|
|
463
|
+
d.isDirectory() &&
|
|
464
|
+
existsSync(join(editionsRoot(), d.name, 'xema-distribution.json')),
|
|
465
|
+
)
|
|
466
|
+
.map((d) => d.name)
|
|
467
|
+
.sort();
|
|
468
|
+
if (ids.length === 0) {
|
|
469
|
+
console.error('[dist:check] no distributions found to check.');
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
return ids;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Guard for the capability-domain OWNERSHIP (reservation) table.
|
|
477
|
+
*
|
|
478
|
+
* This repo has no test runner (no package.json, no CI workflow of its own) —
|
|
479
|
+
* `dist:check` IS its gate — so the invariant is asserted here, where it
|
|
480
|
+
* actually runs, rather than in a spec file nothing would execute.
|
|
481
|
+
*
|
|
482
|
+
* THE INVARIANT THAT MATTERS: **every edition carries the IDENTICAL table.**
|
|
483
|
+
*
|
|
484
|
+
* Ownership is a property of the platform, not of what an edition preloads.
|
|
485
|
+
* An earlier design put the claim on each `LockedBiome`, which made a trimmed
|
|
486
|
+
* edition (`minimal`, `appliance-lean`) leave `kb`/`agent`/… unowned — so an
|
|
487
|
+
* org admin could install the owning biome from the store and THEN bind
|
|
488
|
+
* `kb:page.read@1` to a URL they control, silently shadowing the platform
|
|
489
|
+
* data plane. The table is now built from the FULL biome index and reserves
|
|
490
|
+
* the domain even when its owning biome is absent from that edition — which
|
|
491
|
+
* is exactly the case the reservation exists to cover.
|
|
492
|
+
*
|
|
493
|
+
* Also re-asserts the two structural rules, through the KERNEL's own reader:
|
|
494
|
+
* (a) two biomes claiming the SAME domain is refused (ambiguous ownership has
|
|
495
|
+
* no safe resolution — fail the build, never pick a winner);
|
|
496
|
+
* (b) a domain in NO table entry is not first-party (so a claim that never
|
|
497
|
+
* made it into a resolved, committed, drift-checked lock carries no
|
|
498
|
+
* weight), and an invalid domain is refused rather than silently carried.
|
|
499
|
+
*
|
|
500
|
+
* @param {string[]} ids every distribution id discovered in this repo.
|
|
501
|
+
* @returns {Promise<boolean>} true when green.
|
|
502
|
+
*/
|
|
503
|
+
async function checkOwnershipInvariant(ids) {
|
|
504
|
+
const { resolveFirstPartyCapabilityDomains } =
|
|
505
|
+
await loadKernelDistributionSchemas();
|
|
506
|
+
|
|
507
|
+
const lockOf = (capabilityDomainOwners) => ({
|
|
508
|
+
schemaVersion: 1,
|
|
509
|
+
distributionId: 'dist-check-selftest',
|
|
510
|
+
biomes: [],
|
|
511
|
+
platformServices: [],
|
|
512
|
+
capabilityDomainOwners,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const failures = [];
|
|
516
|
+
|
|
517
|
+
// (a) collision → throws.
|
|
518
|
+
try {
|
|
519
|
+
resolveFirstPartyCapabilityDomains(
|
|
520
|
+
lockOf([
|
|
521
|
+
{ domain: 'acme', biomeId: 'alpha' },
|
|
522
|
+
{ domain: 'acme', biomeId: 'beta' },
|
|
523
|
+
]),
|
|
524
|
+
);
|
|
525
|
+
failures.push(
|
|
526
|
+
'two biomes claiming the same domain "acme" did NOT throw — an ambiguous ownership claim must fail the resolve',
|
|
527
|
+
);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (!err.message.includes('acme')) {
|
|
530
|
+
failures.push(
|
|
531
|
+
`collision threw, but the message does not name the domain: ${err.message}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// (b) a listed domain is first-party; an unlisted one is not.
|
|
537
|
+
const owners = resolveFirstPartyCapabilityDomains(
|
|
538
|
+
lockOf([{ domain: 'acme', biomeId: 'alpha' }]),
|
|
539
|
+
);
|
|
540
|
+
if (owners.get('acme') !== 'alpha') {
|
|
541
|
+
failures.push(
|
|
542
|
+
`a listed domain did not resolve to its owner (got ${String(owners.get('acme'))}, want "alpha")`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (resolveFirstPartyCapabilityDomains(lockOf([])).has('acme')) {
|
|
546
|
+
failures.push(
|
|
547
|
+
'a domain listed in NO table entry resolved as first-party — a claim outside the lock must carry no weight',
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// (c) an invalid domain is refused rather than silently carried.
|
|
552
|
+
try {
|
|
553
|
+
resolveFirstPartyCapabilityDomains(
|
|
554
|
+
lockOf([{ domain: 'Acme!', biomeId: 'alpha' }]),
|
|
555
|
+
);
|
|
556
|
+
failures.push('an invalid capability domain "Acme!" was accepted');
|
|
557
|
+
} catch {
|
|
558
|
+
/* expected */
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// (d) THE headline invariant — every committed edition carries the same,
|
|
562
|
+
// non-empty table. An edition-specific table is the fail-open bug.
|
|
563
|
+
let reference;
|
|
564
|
+
for (const id of ids) {
|
|
565
|
+
const lockPath = join(editionsRoot(), id, 'distribution.lock.json');
|
|
566
|
+
if (!existsSync(lockPath)) {continue;}
|
|
567
|
+
const table = validateImmutableDistributionLock(
|
|
568
|
+
JSON.parse(readFileSync(lockPath, 'utf8')),
|
|
569
|
+
).capabilityDomainOwners;
|
|
570
|
+
if (!Array.isArray(table) || table.length === 0) {
|
|
571
|
+
failures.push(
|
|
572
|
+
`distribution "${id}": capabilityDomainOwners is missing or empty — an empty reservation table means NOTHING is first-party (fail-open)`,
|
|
573
|
+
);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const serialized = serializeJson(table);
|
|
577
|
+
if (reference === undefined) {
|
|
578
|
+
reference = { id, serialized, size: table.length };
|
|
579
|
+
} else if (serialized !== reference.serialized) {
|
|
580
|
+
failures.push(
|
|
581
|
+
`distribution "${id}" carries a DIFFERENT capabilityDomainOwners table than "${reference.id}" — ` +
|
|
582
|
+
`ownership must not vary by edition, or a trimmed edition silently leaves domains org-bindable`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (failures.length > 0) {
|
|
588
|
+
for (const f of failures) {
|
|
589
|
+
console.error(`[dist:check] ownership-invariant FAILED: ${f}`);
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
console.log(
|
|
594
|
+
`[dist:check] capability-domain reservation table: green — ${reference?.size ?? 0} domains, identical across all ${ids.length} editions.`,
|
|
595
|
+
);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
600
|
+
// BIOME CONFINEMENT — which editions may ship a biome whose CONTENTS must not
|
|
601
|
+
// reach a customer installation.
|
|
602
|
+
//
|
|
603
|
+
// Two different facts, one mechanism, because the question is identical:
|
|
604
|
+
// "which resolved locks may carry this biome, and what breaks if another one
|
|
605
|
+
// does?" The ANSWER differs per declaration and is carried as data.
|
|
606
|
+
//
|
|
607
|
+
// `licensing` holds two private keys: the Ed25519 key that signs long-lived
|
|
608
|
+
// customer licences, and the RSA key that signs registry tokens. The licence
|
|
609
|
+
// is verified OFFLINE by @xemahq/license-verifier against a CLOSED,
|
|
610
|
+
// compiled-in trust store — including on an air-gapped appliance that never
|
|
611
|
+
// reaches this service. So whoever holds the issuing key can mint any tier,
|
|
612
|
+
// unlimited seats, accepted fleet-wide, and revocation is unreachable
|
|
613
|
+
// offline.
|
|
614
|
+
//
|
|
615
|
+
// `fleet-management` holds the Xema-VENDOR directory of customers and their
|
|
616
|
+
// installations — `Client`, `CustomerRepo`, `CustomerDistribution`,
|
|
617
|
+
// `CustomerImageEntitlement`, `CustomerDeployment`, `Appliance`,
|
|
618
|
+
// `ProvisioningProfile`. That is commercial operations, not the generic
|
|
619
|
+
// product model: inside a customer's own installation people are
|
|
620
|
+
// `Organization` / `Subject` / `ExternalSubject` / `Space`, and a
|
|
621
|
+
// customer-managed or offline Xema must not need our commercial inventory to
|
|
622
|
+
// function. An edition that shipped it would put every OTHER customer's name,
|
|
623
|
+
// slug, deployment targets, appliance serials and delivery grants into one
|
|
624
|
+
// customer's installation.
|
|
625
|
+
//
|
|
626
|
+
// WHAT ACTUALLY HOLDS THEM OUT TODAY, AND HOW THIN THAT IS
|
|
627
|
+
// `core` selects { tier: "base" }. BOTH biomes ARE tier "base", first-party,
|
|
628
|
+
// deliverable — they match core's selector on every axis but ONE:
|
|
629
|
+
// `e.audience !== 'operator'` in resolve-distribution.mjs. A single enum
|
|
630
|
+
// comparison, in a file nobody edits when they are editing an edition.
|
|
631
|
+
//
|
|
632
|
+
// And an explicit { "biome": "<id>" } include is the SUPPORTED way to bypass
|
|
633
|
+
// that comparison: naming an operator biome is how you force it in. That is a
|
|
634
|
+
// one-line edit to an edition manifest which reads in review as a packaging
|
|
635
|
+
// diff and IS a key-custody — or a commercial-inventory — decision. The same
|
|
636
|
+
// shape .claude/rules/menu-audience-tiers.md forbids from the other direction
|
|
637
|
+
// — a permission derived from a packaging label — arriving here as an
|
|
638
|
+
// exposure derived from a selector.
|
|
639
|
+
//
|
|
640
|
+
// WHY THIS ASSERTS ON THE RESOLVED LOCK, NOT ON THE SELECTOR
|
|
641
|
+
// Because there are three ways in and only the OUTPUT sees all of them: an
|
|
642
|
+
// explicit `{ biome }` include, a widened `{ tier }`/`{ origin }` branch in
|
|
643
|
+
// the resolver, and an `audience` change in biome-index.json. A gate that
|
|
644
|
+
// read the selector would be blind to the other two.
|
|
645
|
+
//
|
|
646
|
+
// A comment explaining why licensing is absent from core is worth nothing
|
|
647
|
+
// beside an assertion that fails when someone puts it there.
|
|
648
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Biomes whose contents must not reach an unadmitted installation, and the
|
|
652
|
+
* editions each is ADMITTED to. They are the data root's
|
|
653
|
+
* (`confinedBiomes` in edition-workspace.json): the library owns the rule and
|
|
654
|
+
* the schema, never which biome is confined or which edition may carry it.
|
|
655
|
+
* A data root that confines nothing declares no section and has no admissions
|
|
656
|
+
* to check. Adding an id to `admittedEditions` is the decision — make it
|
|
657
|
+
* deliberately, in its own commit, and say who agreed.
|
|
658
|
+
*
|
|
659
|
+
* `consequence` is not decoration: it is what the failure message tells the
|
|
660
|
+
* next reader is at stake, and it differs per declaration. A generic "this
|
|
661
|
+
* biome is confined" message would leave them weighing a packaging diff.
|
|
662
|
+
*/
|
|
663
|
+
const confinedBiomes = () => editionWorkspaceConfig().confinedBiomes ?? [];
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* The decision, as a pure function so it can be exercised against synthetic
|
|
667
|
+
* input below rather than only against a tree that happens to be clean today.
|
|
668
|
+
*
|
|
669
|
+
* @param {{biomeId: string, holds: string, consequence: string, admittedEditions: readonly string[]}[]} declarations
|
|
670
|
+
* @param {Set<string>} knownBiomeIds every id biome-index.json carries
|
|
671
|
+
* @param {Map<string, string[]>} shippingByBiome biomeId -> edition ids shipping it
|
|
672
|
+
* @returns {string[]}
|
|
673
|
+
*/
|
|
674
|
+
function confinementFailures(declarations, knownBiomeIds, shippingByBiome, gradedEditions) {
|
|
675
|
+
const failures = [];
|
|
676
|
+
for (const decl of declarations) {
|
|
677
|
+
// POSITIVE CONTROL, and the reason this is not decoration: if the biome id
|
|
678
|
+
// ever stops resolving — a rename, a carve, a typo — every lock scan below
|
|
679
|
+
// looks for a name nothing can carry, finds nothing, and the gate passes
|
|
680
|
+
// while guarding nothing. That is the green-while-executing-nothing shape,
|
|
681
|
+
// so an unresolvable declaration is a FAILURE, never a skip.
|
|
682
|
+
if (!knownBiomeIds.has(decl.biomeId)) {
|
|
683
|
+
failures.push(
|
|
684
|
+
`confinement declaration names biome "${decl.biomeId}", which biome-index.json does not carry. ` +
|
|
685
|
+
'Either the biome was renamed — repoint this declaration — or it is genuinely gone and the ' +
|
|
686
|
+
'declaration should be deleted. It must not be left pointing at nothing: this gate would ' +
|
|
687
|
+
'then scan every lock for a name none of them can hold and pass without checking anything.',
|
|
688
|
+
);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const shipping = shippingByBiome.get(decl.biomeId) ?? [];
|
|
692
|
+
for (const edition of shipping) {
|
|
693
|
+
if (decl.admittedEditions.includes(edition)) {continue;}
|
|
694
|
+
failures.push(
|
|
695
|
+
`edition "${edition}" ships biome "${decl.biomeId}", which holds ${decl.holds}. ` +
|
|
696
|
+
`That is a CONFINEMENT decision, not a packaging one: ${decl.consequence}. ` +
|
|
697
|
+
'If this is intended, add the edition to admittedEditions in the ' +
|
|
698
|
+
'"confinedBiomes" section of edition-workspace.json in its own commit, naming who agreed.',
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
for (const edition of decl.admittedEditions) {
|
|
702
|
+
if (shipping.includes(edition)) {continue;}
|
|
703
|
+
// AN UNGRADED EDITION IS UNJUDGED, NOT STALE.
|
|
704
|
+
//
|
|
705
|
+
// `shipping` is built only from the locks this run actually READ, so
|
|
706
|
+
// "not shipping" and "not in the corpus" are the same absence here — and
|
|
707
|
+
// they have opposite meanings. Judged over a partial corpus, every
|
|
708
|
+
// admission naming an edition outside it reports as dead permission and
|
|
709
|
+
// the remedy printed is "Delete it", which would delete a live admission
|
|
710
|
+
// on the evidence of a lock the run never opened.
|
|
711
|
+
//
|
|
712
|
+
// It is not a customer-root accommodation: the same false finding fires
|
|
713
|
+
// in the vendor's own repository for any edition that is declared and not
|
|
714
|
+
// yet resolved. The corpus was always the right fence; nothing stated it
|
|
715
|
+
// while the corpus was always the full set.
|
|
716
|
+
if (!gradedEditions.has(edition)) {continue;}
|
|
717
|
+
failures.push(
|
|
718
|
+
`confinement admission "${decl.biomeId}" -> "${edition}" suppresses NOTHING: that edition does ` +
|
|
719
|
+
'not ship the biome. A standing admission for an edition that does not use it is a permission ' +
|
|
720
|
+
'nobody reviewed still sitting open. Delete it.',
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return failures;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* @param {string[]} ids every discovered distribution id
|
|
729
|
+
* @returns {boolean} true when the plane is within its declared confinement
|
|
730
|
+
*/
|
|
731
|
+
function checkConfinementScope(ids) {
|
|
732
|
+
const index = JSON.parse(readFileSync(biomeIndexPath(), 'utf8'));
|
|
733
|
+
const entries = Array.isArray(index) ? index : (index.biomes ?? []);
|
|
734
|
+
const knownBiomeIds = new Set(entries.map((entry) => entry?.id).filter(Boolean));
|
|
735
|
+
|
|
736
|
+
const shippingByBiome = new Map();
|
|
737
|
+
// THE LOCKS COME FROM THE EDITION CONTENT ROOT, never from `HERE`.
|
|
738
|
+
//
|
|
739
|
+
// `ids` is discovered under editionsRoot() and the locks were read from HERE,
|
|
740
|
+
// so the two agreed only while tooling and content were one directory. Point
|
|
741
|
+
// the root at a customer's `distribution/` and every `existsSync` below
|
|
742
|
+
// answered false, every id was skipped, `shippingByBiome` stayed empty — and
|
|
743
|
+
// `confinementFailures` over an empty map reports nothing. The confinement
|
|
744
|
+
// check PASSED while grading not one edition, which is worse than failing:
|
|
745
|
+
// it is the one shape that reads as protection in review.
|
|
746
|
+
let locksRead = 0;
|
|
747
|
+
/** The ids whose lock this run actually OPENED — the corpus, not the roster. */
|
|
748
|
+
const gradedEditions = new Set();
|
|
749
|
+
for (const id of ids) {
|
|
750
|
+
const lockPath = join(editionsRoot(), id, 'distribution.lock.json');
|
|
751
|
+
// A single unresolved edition is legitimate — it has a manifest and no lock
|
|
752
|
+
// yet, and the lock-currency check above is what speaks to that. ALL of them
|
|
753
|
+
// being unreadable is not, and is refused below rather than skipped.
|
|
754
|
+
if (!existsSync(lockPath)) {continue;}
|
|
755
|
+
locksRead += 1;
|
|
756
|
+
gradedEditions.add(id);
|
|
757
|
+
const lock = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
758
|
+
for (const biome of lock.biomes ?? []) {
|
|
759
|
+
const biomeId = biome?.id ?? biome?.biomeId;
|
|
760
|
+
if (!biomeId) {continue;}
|
|
761
|
+
if (!shippingByBiome.has(biomeId)) {shippingByBiome.set(biomeId, []);}
|
|
762
|
+
shippingByBiome.get(biomeId).push(id);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// The predicate, exercised against synthetic input. Without this the gate is
|
|
767
|
+
// only ever run against a tree that is already clean, which proves nothing
|
|
768
|
+
// about what it does when the tree is not.
|
|
769
|
+
const probe = [
|
|
770
|
+
{ biomeId: 'probe', holds: 'a probe key', consequence: 'a probe consequence', admittedEditions: ['allowed'] },
|
|
771
|
+
];
|
|
772
|
+
const known = new Set(['probe']);
|
|
773
|
+
const graded = new Set(['allowed', 'sneaked']);
|
|
774
|
+
const cases = [
|
|
775
|
+
[new Map([['probe', ['allowed']]]), graded, 0, 'an admitted edition is silent'],
|
|
776
|
+
[new Map([['probe', ['allowed', 'sneaked']]]), graded, 1, 'an unadmitted edition must fail'],
|
|
777
|
+
[new Map(), graded, 1, 'an admission that suppresses nothing must fail as stale'],
|
|
778
|
+
// THE CORPUS FENCE, both ways. The pair is the point: the SAME empty
|
|
779
|
+
// `shipping` is a stale admission when the edition was graded and silence
|
|
780
|
+
// when it was not. Without the second case a corpus regression is invisible,
|
|
781
|
+
// because the first one passes either way.
|
|
782
|
+
[new Map(), new Set(), 0, 'an admission for an edition OUTSIDE the corpus is unjudged, not stale'],
|
|
783
|
+
[
|
|
784
|
+
new Map([['probe', ['sneaked']]]),
|
|
785
|
+
new Set(),
|
|
786
|
+
1,
|
|
787
|
+
'confinement itself still fires on an ungraded corpus — only the STALE arm is scoped',
|
|
788
|
+
],
|
|
789
|
+
];
|
|
790
|
+
const selfTest = [];
|
|
791
|
+
for (const [shipping, corpus, want, why] of cases) {
|
|
792
|
+
const got = confinementFailures(probe, known, shipping, corpus).length;
|
|
793
|
+
if (got !== want) {selfTest.push(`confinement self-test: expected ${want} failure(s), got ${got} — ${why}`);}
|
|
794
|
+
}
|
|
795
|
+
if (
|
|
796
|
+
confinementFailures(
|
|
797
|
+
[{ biomeId: 'gone', holds: 'x', consequence: 'y', admittedEditions: [] }],
|
|
798
|
+
known,
|
|
799
|
+
new Map(),
|
|
800
|
+
graded,
|
|
801
|
+
).length !== 1
|
|
802
|
+
) {
|
|
803
|
+
selfTest.push('confinement self-test: a declaration naming an unknown biome must fail, not pass vacuously');
|
|
804
|
+
}
|
|
805
|
+
// Each declaration must name what is at stake. A blank `consequence` would
|
|
806
|
+
// still fail the right editions and would tell whoever hit it nothing, which
|
|
807
|
+
// is how an assertion becomes a speed bump.
|
|
808
|
+
for (const decl of confinedBiomes()) {
|
|
809
|
+
if (typeof decl.consequence !== 'string' || decl.consequence.trim().length < 40) {
|
|
810
|
+
selfTest.push(
|
|
811
|
+
`confinement declaration "${decl.biomeId}" carries no substantive \`consequence\`; the failure ` +
|
|
812
|
+
'message would name a rule without naming what it protects.',
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// A ZERO-LOCK SCAN IS A FAILURE, not a pass. Without this the only signal that
|
|
818
|
+
// the check graded nothing is a corpus line nobody reads, and the exit code of
|
|
819
|
+
// a vacuous run is identical to the exit code of a clean one.
|
|
820
|
+
const emptyCorpus =
|
|
821
|
+
ids.length > 0 && locksRead === 0
|
|
822
|
+
? [
|
|
823
|
+
`confinement read 0 of ${String(ids.length)} edition lock(s) under ${editionsRoot()}. ` +
|
|
824
|
+
'Nothing was graded, so this is not evidence that the plane is confined. Either the ' +
|
|
825
|
+
'editions root is wrong, or no edition in it has been resolved yet.',
|
|
826
|
+
]
|
|
827
|
+
: [];
|
|
828
|
+
|
|
829
|
+
const failures = [
|
|
830
|
+
...selfTest,
|
|
831
|
+
...emptyCorpus,
|
|
832
|
+
...confinementFailures(confinedBiomes(), knownBiomeIds, shippingByBiome, gradedEditions),
|
|
833
|
+
];
|
|
834
|
+
if (failures.length > 0) {
|
|
835
|
+
console.error('✘ [dist-check] biome confinement:');
|
|
836
|
+
for (const failure of failures) {console.error(` - ${failure}`);}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
console.log(
|
|
840
|
+
`[dist-check] biome confinement: ${confinedBiomes().length} confined biome(s) checked against ` +
|
|
841
|
+
`${String(locksRead)} of ${String(ids.length)} edition lock(s) under ${editionsRoot()}; ` +
|
|
842
|
+
`${confinedBiomes().reduce((n, d) => n + d.admittedEditions.length, 0)} admission(s).`,
|
|
843
|
+
);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* The verify half of the seal: every biome's `biomeContentHash` is RECOMPUTED
|
|
849
|
+
* from the manifest, the git tree at the pinned commit and the sealed image
|
|
850
|
+
* digests, and a mismatch is refused naming the biome. The kernel cannot do
|
|
851
|
+
* this ("sealed" is only a field). Returns true on green.
|
|
852
|
+
*/
|
|
853
|
+
async function checkBiomeContentHashes(id) {
|
|
854
|
+
try {
|
|
855
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
856
|
+
const lock = schemas.parseDistributionLock(
|
|
857
|
+
JSON.parse(readFileSync(join(editionsRoot(), id, 'distribution.lock.json'), 'utf8')),
|
|
858
|
+
);
|
|
859
|
+
const imageLock = schemas.parseImageLock(
|
|
860
|
+
JSON.parse(readFileSync(join(editionsRoot(), id, 'images.lock.json'), 'utf8')),
|
|
861
|
+
);
|
|
862
|
+
const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
|
|
863
|
+
await verifyBiomeContentHashes({
|
|
864
|
+
schemas,
|
|
865
|
+
lock,
|
|
866
|
+
imageLock,
|
|
867
|
+
locate: sourceTreeLocator(externalSourceTrees(lock)),
|
|
868
|
+
vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
|
|
869
|
+
});
|
|
870
|
+
console.log(`[dist:check] ${id}: ${lock.biomes.length} biomeContentHash(es) recomputed from their sources.`);
|
|
871
|
+
return true;
|
|
872
|
+
} catch (err) {
|
|
873
|
+
console.error(`[dist:check] ${id}: ${err.message}`);
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function main() {
|
|
879
|
+
let failed = false;
|
|
880
|
+
// The tests spawned below resolve the editions root from THEIR OWN argv and
|
|
881
|
+
// env; a root stated here by --editions-root must reach them, or they
|
|
882
|
+
// would grade the vendor's editions under a customer's dist-check.
|
|
883
|
+
const checkSources = process.argv.includes('--check-sources');
|
|
884
|
+
|
|
885
|
+
// 0. The pinned kernel speaks an edition protocol this CLI supports. This is
|
|
886
|
+
// the ONE comparison between the CLI and the kernel; a refusal throws
|
|
887
|
+
// EditionProtocolMismatchError and stops the check before anything is read.
|
|
888
|
+
const protocol = assertKernelEditionProtocol(kernelPackageDir());
|
|
889
|
+
console.log(
|
|
890
|
+
`[dist:check] the kernel speaks edition protocol v${protocol.version} (${protocol.contentDigest}), which this CLI supports.`,
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
// 1. Offline committed-index validation is the default authority boundary.
|
|
894
|
+
// A live sibling-source scan is an explicit trusted regeneration audit only;
|
|
895
|
+
// CI/shared working trees must not make unrelated uncommitted sources into
|
|
896
|
+
// packaging inputs.
|
|
897
|
+
if (!checkCommittedIndex()) {failed = true;}
|
|
898
|
+
if (!(await checkCommittedPlatformServiceIndex())) {failed = true;}
|
|
899
|
+
if (!(await checkCommittedRuntimeImageSourceIndex(checkSources))) {
|
|
900
|
+
failed = true;
|
|
901
|
+
}
|
|
902
|
+
if (!(await checkEvidenceTrustPolicy())) {failed = true;}
|
|
903
|
+
if (checkSources) {
|
|
904
|
+
try {
|
|
905
|
+
execFileSync(
|
|
906
|
+
process.execPath,
|
|
907
|
+
[join(HERE, 'build-biome-index.mjs'), '--check', ...workspaceArgv()],
|
|
908
|
+
{ stdio: 'inherit' },
|
|
909
|
+
);
|
|
910
|
+
execFileSync(
|
|
911
|
+
process.execPath,
|
|
912
|
+
[join(HERE, 'build-platform-service-index.mjs'), '--check-sources', ...workspaceArgv()],
|
|
913
|
+
{ stdio: 'inherit' },
|
|
914
|
+
);
|
|
915
|
+
} catch {
|
|
916
|
+
failed = true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const distributionIds = discoverDistributionIds();
|
|
921
|
+
|
|
922
|
+
if (!checkConfinementScope(distributionIds)) {failed = true;}
|
|
923
|
+
|
|
924
|
+
// 1b. Capability-domain reservation table (see checkOwnershipInvariant).
|
|
925
|
+
if (!(await checkOwnershipInvariant(distributionIds))) {failed = true;}
|
|
926
|
+
|
|
927
|
+
// The pinned kernel's authorization composer (null when the pin predates it).
|
|
928
|
+
let authorizationKernel;
|
|
929
|
+
try {
|
|
930
|
+
authorizationKernel = await loadKernelAuthorizationComposer({
|
|
931
|
+
kernelPackageDir: kernelPackageDir(),
|
|
932
|
+
});
|
|
933
|
+
} catch (err) {
|
|
934
|
+
console.error(`[dist:check] ${err.message}`);
|
|
935
|
+
failed = true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// The fleet chart-set pin every edition must carry (edition-chart-set.mjs).
|
|
939
|
+
let chartSetPinText = null;
|
|
940
|
+
try {
|
|
941
|
+
chartSetPinText = readFileSync(fleetChartSetLockPath(), 'utf8');
|
|
942
|
+
assertRosteredChartSetLock(JSON.parse(chartSetPinText), EDITION_CHART_SET_FILE, EditionChannel.Release);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error(`[dist:check] chart-set pin: ${err.message}. Run \`${commandFor('resolve-chart-set.mjs')}\`.`);
|
|
945
|
+
failed = true;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// 3 + 4 + 5 + 6 + 7. Every distribution: distribution.lock, images.lock,
|
|
949
|
+
// authorization.lock, chart-set.lock.
|
|
950
|
+
for (const id of distributionIds) {
|
|
951
|
+
if (chartSetPinText !== null) {
|
|
952
|
+
const verdict = checkEditionChartSet(join(editionsRoot(), id), chartSetPinText);
|
|
953
|
+
if (verdict === EditionChartSetCheck.GREEN) {
|
|
954
|
+
console.log(`[dist:check] ${id}: ${EDITION_CHART_SET_FILE} carries the fleet chart-set pin.`);
|
|
955
|
+
} else {
|
|
956
|
+
console.error(
|
|
957
|
+
`[dist:check] ${id}: ${EDITION_CHART_SET_FILE} is ${verdict === EditionChartSetCheck.MISSING ? 'missing' : 'not the fleet pin'} — ` +
|
|
958
|
+
`an edition carries the chart generation it is rendered with. Run \`${commandFor('resolve-chart-set.mjs')}\`.`,
|
|
959
|
+
);
|
|
960
|
+
failed = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const resolutionIndex = await checkDistributionLock(id);
|
|
964
|
+
if (resolutionIndex === null) {
|
|
965
|
+
// A stale/missing distribution lock would only make the image check emit a
|
|
966
|
+
// misleading error (resolve-images reads it) — let the distribution-lock
|
|
967
|
+
// failure stand as the single actionable message for this distribution.
|
|
968
|
+
failed = true;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const imagesLockGreen = await checkImagesLock(id, checkSources);
|
|
972
|
+
if (!imagesLockGreen) {failed = true;}
|
|
973
|
+
else if (!(await checkBiomeContentHashes(id))) {failed = true;}
|
|
974
|
+
if (authorizationKernel !== undefined && !(await checkAuthorizationLock(id, authorizationKernel, resolutionIndex))) {
|
|
975
|
+
failed = true;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (failed) {process.exit(1);}
|
|
980
|
+
console.log('[dist:check] all distributions green.');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
main().catch((err) => {
|
|
984
|
+
console.error(`[dist:check] ${err.message}`);
|
|
985
|
+
process.exit(1);
|
|
986
|
+
});
|