@xemahq/xema 0.42.0 → 0.44.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/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +75 -6
- package/dist/edition/verify-image-evidence.bundle.provenance.json +71 -47
- package/dist/runtime/edition/verb-map.d.ts +1 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +42 -0
- package/edition/authored-source-check.mjs +135 -0
- package/edition/build-platform-service-index.mjs +1 -1
- package/edition/build-runtime-image-source-index.mjs +25 -1
- package/edition/canonical-repository-url.mjs +35 -0
- package/edition/checkout-indexed-source-repositories.mjs +2 -1
- package/edition/develop-edition-invariants.mjs +141 -0
- package/edition/develop-edition-resolve.mjs +7 -1
- package/edition/develop-edition.mjs +3 -4
- package/edition/develop-freeze.mjs +8 -7
- package/edition/develop-graph.mjs +26 -0
- package/edition/develop-snapshot-inputs.mjs +0 -1
- package/edition/dist-check.mjs +108 -63
- package/edition/edition-config.mjs +27 -1
- package/edition/first-party-source-ids.mjs +15 -12
- package/edition/git-source-auth.mjs +1 -1
- package/edition/lib.mjs +0 -32
- package/edition/pin-source.mjs +21 -14
- package/edition/workspace-sections.mjs +35 -5
- package/package.json +2 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// authored-source-check — `xema edition check-authored`: grades the AUTHORED
|
|
3
|
+
// distribution source, the develop lifecycle's gate. It is its own mode, not
|
|
4
|
+
// the release check with assertions turned off: `xema edition check`
|
|
5
|
+
// (dist-check.mjs) stays the full release-path grade, and nothing here can
|
|
6
|
+
// make it skip anything.
|
|
7
|
+
//
|
|
8
|
+
// Authored and release state are separate lifecycles. What is authored is
|
|
9
|
+
// graded here, offline; what only a release carries is not read: no lock
|
|
10
|
+
// content (the committed distribution / images / authorization / chart-set
|
|
11
|
+
// locks; the trust-arming rule counts lock files by presence), and nothing that
|
|
12
|
+
// needs a source checkout at a committed `repositoryPins` commit. No edition is
|
|
13
|
+
// resolved here: the plane invariants over a resolved edition (the
|
|
14
|
+
// capability-domain reservation table and biome confinement) are asserted on
|
|
15
|
+
// develop by the develop resolve against the graph it froze
|
|
16
|
+
// (develop-edition-invariants.mjs), and on the release path by
|
|
17
|
+
// `xema edition check` over the committed locks. One place per assertion.
|
|
18
|
+
//
|
|
19
|
+
// KEPT (each refuses):
|
|
20
|
+
// - the kernel speaks a supported edition protocol;
|
|
21
|
+
// - biome-index.json: immutable entries, deterministic order, no duplicate
|
|
22
|
+
// source identity, a serviceDependencies projection on every entry;
|
|
23
|
+
// - platform-service-index.json: parses, matches the declared
|
|
24
|
+
// platformServiceSources offline, service-dependency owner parity;
|
|
25
|
+
// - runtime-image-source-index.json: the offline `--check`;
|
|
26
|
+
// - the evidence trust policy, its data-root pin and its arming assessment;
|
|
27
|
+
// - the data-root readers (store source repositories, publisher trust);
|
|
28
|
+
// - the fleet chart-set pin is a rostered chart-set lock;
|
|
29
|
+
// - the pinned kernel's authorization composer loads;
|
|
30
|
+
// - the ownership self-tests: the kernel's capability-domain reader refuses
|
|
31
|
+
// two owners of one domain and an invalid domain, and counts no unlisted
|
|
32
|
+
// domain as first-party (synthetic tables, no lock);
|
|
33
|
+
// - every edition definition is structurally valid: the kernel parses it
|
|
34
|
+
// (its `sourcePins` included) and it composes.
|
|
35
|
+
//
|
|
36
|
+
// NOT HERE:
|
|
37
|
+
// - lock currency, sealed hashes, the authorization lock, each edition's
|
|
38
|
+
// chart-set.lock.json copy, platform-service ownership at pinned commits,
|
|
39
|
+
// `--check-sources` (refused): release state, `xema edition check`;
|
|
40
|
+
// - the definition resolve, reservations, confinement: the develop resolve.
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
checkDataRootPlane,
|
|
44
|
+
discoverDistributionIds,
|
|
45
|
+
loadPlaneAuthorizationKernel,
|
|
46
|
+
ownershipSelfTestFailures,
|
|
47
|
+
readFleetChartSetPin,
|
|
48
|
+
} from './dist-check.mjs';
|
|
49
|
+
import { describeEditionHolds } from './edition-source-pins.mjs';
|
|
50
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
|
|
51
|
+
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
52
|
+
import { composeEdition } from './resolve-distribution.mjs';
|
|
53
|
+
|
|
54
|
+
/** The release-path flag this mode refuses rather than ignores. */
|
|
55
|
+
const CHECK_SOURCES_FLAG = '--check-sources';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether edition `id`'s definition is structurally valid: the kernel parses it
|
|
59
|
+
* and it composes. Nothing is resolved and no index or source is read.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Promise<string | null>} the refusal, or null when valid
|
|
62
|
+
*/
|
|
63
|
+
export async function definitionRefusal(id) {
|
|
64
|
+
try {
|
|
65
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
66
|
+
composeEdition(schemas.parseDistribution, id);
|
|
67
|
+
return null;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return err.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The ownership self-tests (dist-check.mjs): the kernel's capability-domain
|
|
75
|
+
* reader, against synthetic tables. Lock-free. Returns true when green.
|
|
76
|
+
*/
|
|
77
|
+
async function checkOwnershipSelfTests(resolveFirstPartyCapabilityDomains) {
|
|
78
|
+
let failures;
|
|
79
|
+
try {
|
|
80
|
+
failures = ownershipSelfTestFailures(resolveFirstPartyCapabilityDomains);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
failures = [`the kernel's capability-domain reader threw on a valid table: ${err.message}`];
|
|
83
|
+
}
|
|
84
|
+
for (const failure of failures) {console.error(`[dist:check-authored] ownership self-test FAILED: ${failure}`);}
|
|
85
|
+
if (failures.length === 0) {console.log('[dist:check-authored] ownership self-tests: green.');}
|
|
86
|
+
return failures.length === 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The authored check over the configured roots. Returns true when green.
|
|
91
|
+
* `resolveFirstPartyCapabilityDomains` is the kernel's reader unless a test
|
|
92
|
+
* stands in for it.
|
|
93
|
+
*/
|
|
94
|
+
export async function checkAuthoredSource({ resolveFirstPartyCapabilityDomains } = {}) {
|
|
95
|
+
let green = await checkDataRootPlane({ checkSources: false });
|
|
96
|
+
const reader = resolveFirstPartyCapabilityDomains ?? (await loadKernelDistributionSchemas()).resolveFirstPartyCapabilityDomains;
|
|
97
|
+
if (!(await checkOwnershipSelfTests(reader))) {green = false;}
|
|
98
|
+
|
|
99
|
+
const distributionIds = discoverDistributionIds();
|
|
100
|
+
for (const line of describeEditionHolds(distributionIds)) {console.log(line);}
|
|
101
|
+
for (const id of distributionIds) {
|
|
102
|
+
const refusal = await definitionRefusal(id);
|
|
103
|
+
if (refusal === null) {
|
|
104
|
+
console.log(`[dist:check-authored] ${id}: the definition is valid.`);
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`[dist:check-authored] ${id}: the definition is invalid — ${refusal}`);
|
|
107
|
+
green = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { green: authorizationKernelGreen } = await loadPlaneAuthorizationKernel();
|
|
112
|
+
if (!authorizationKernelGreen) {green = false;}
|
|
113
|
+
if (readFleetChartSetPin() === null) {green = false;}
|
|
114
|
+
|
|
115
|
+
return green;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
if (process.argv.includes(CHECK_SOURCES_FLAG)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${CHECK_SOURCES_FLAG} audits the indexes against source checkouts, which is release state: ` +
|
|
122
|
+
`run \`xema edition check ${CHECK_SOURCES_FLAG}\`.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!(await checkAuthoredSource())) {process.exit(1);}
|
|
126
|
+
console.log('[dist:check-authored] the authored distribution source is green.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isEntryModule(import.meta.url)) {
|
|
130
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
console.error(`[dist:check-authored] ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
|
|
6
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
6
7
|
import { platformServiceIndexPath , WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
|
|
7
8
|
import {
|
|
8
|
-
canonicalPublicHttpsRepositoryUrl,
|
|
9
9
|
loadKernelDistributionSchemas,
|
|
10
10
|
readGitJsonAtCommit,
|
|
11
11
|
serializeJson,
|
|
@@ -5,10 +5,12 @@ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
|
5
5
|
import { isAbsolute, join } from 'node:path';
|
|
6
6
|
|
|
7
7
|
import { buildValidatedPlatformServiceIndex } from './build-platform-service-index.mjs';
|
|
8
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
8
9
|
import { connectorCrossRepoBuildInputs } from './connector-source-authority.mjs';
|
|
9
10
|
import { editionWorkspaceConfig, workspaceSection } from './edition-config.mjs';
|
|
10
11
|
import { biomeIndexPath, dataRoot, platformServiceIndexPath, runtimeImageSourceIndexPath, WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
|
|
11
12
|
import { COMMIT_PATTERN } from './evidence-grammars.mjs';
|
|
13
|
+
import { firstPartySources } from './first-party-source-ids.mjs';
|
|
12
14
|
import {
|
|
13
15
|
buildTrustedKernelContractAuthority,
|
|
14
16
|
KERNEL_COMMIT_FLAG,
|
|
@@ -17,7 +19,6 @@ import {
|
|
|
17
19
|
} from './kernel-contract-authority.mjs';
|
|
18
20
|
import {
|
|
19
21
|
buildBiomeIndex,
|
|
20
|
-
canonicalPublicHttpsRepositoryUrl,
|
|
21
22
|
compareStrings,
|
|
22
23
|
distributionBiomeSourceRepositories,
|
|
23
24
|
loadKernelDistributionSchemas,
|
|
@@ -572,6 +573,28 @@ function compareSubject(left, right) {
|
|
|
572
573
|
);
|
|
573
574
|
}
|
|
574
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Every release pin names a first-party repository (`firstPartySources`) at
|
|
578
|
+
* the URL declared there. The converse is not required: a first-party
|
|
579
|
+
* repository is in the develop graph before any release pins it.
|
|
580
|
+
*/
|
|
581
|
+
function assertPinsAreFirstParty(repositoryPins) {
|
|
582
|
+
const declared = firstPartySources('checking that every release pin is a first-party repository');
|
|
583
|
+
for (const pin of repositoryPins) {
|
|
584
|
+
const source = declared[pin.repo];
|
|
585
|
+
if (source === undefined) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`runtime-image-source-index pins ${JSON.stringify(pin.repo)}, which edition-workspace.json does not declare under "firstPartySources"`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (source.repositoryUrl !== pin.repositoryUrl) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`runtime-image-source-index pins ${pin.repo} at ${pin.repositoryUrl}, but "firstPartySources" declares it at ${source.repositoryUrl}`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
575
598
|
export async function validateRuntimeImageSourceIndex(
|
|
576
599
|
index,
|
|
577
600
|
schemas,
|
|
@@ -658,6 +681,7 @@ export async function validateRuntimeImageSourceIndex(
|
|
|
658
681
|
throw new Error(`${repository.repo} repositoryUrl is not canonical`);
|
|
659
682
|
}
|
|
660
683
|
}
|
|
684
|
+
assertPinsAreFirstParty(index.repositoryPins);
|
|
661
685
|
// Every subject a biome contributes to the image plane — BOTH the standing
|
|
662
686
|
// services and the runtime components. A runtime component is a full image
|
|
663
687
|
// subject (built, signed, mirrored) that simply is never scheduled, so a
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The canonical form of a public HTTPS repository URL, and the refusals that
|
|
2
|
+
// define it. Leaf module: the workspace config and the git source hold read it
|
|
3
|
+
// without importing the edition library.
|
|
4
|
+
|
|
5
|
+
export function canonicalPublicHttpsRepositoryUrl(raw, context) {
|
|
6
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.trim() !== raw) {
|
|
7
|
+
throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
|
|
8
|
+
}
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(`${context} is not a valid URL`);
|
|
14
|
+
}
|
|
15
|
+
if (parsed.protocol !== 'https:') {
|
|
16
|
+
throw new Error(`${context} must use HTTPS`);
|
|
17
|
+
}
|
|
18
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${context} must not contain username/password credentials`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
parsed.search.length > 0 ||
|
|
25
|
+
parsed.hash.length > 0 ||
|
|
26
|
+
raw.includes('?') ||
|
|
27
|
+
raw.includes('#')
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(`${context} must not contain a query or fragment`);
|
|
30
|
+
}
|
|
31
|
+
if (parsed.hostname.length === 0) {
|
|
32
|
+
throw new Error(`${context} must identify a repository host`);
|
|
33
|
+
}
|
|
34
|
+
return parsed.toString();
|
|
35
|
+
}
|
|
@@ -250,7 +250,8 @@ const SNAPSHOT_PINS_FLAG = '--snapshot-pins';
|
|
|
250
250
|
|
|
251
251
|
/**
|
|
252
252
|
* `--snapshot-pins <file>` is the only argument: with it, the plan is the
|
|
253
|
-
*
|
|
253
|
+
* develop graph (the data root's firstPartySources) at that snapshot (the
|
|
254
|
+
* develop edition's frozen pins), never the committed index's repositoryPins;
|
|
254
255
|
* without it, the committed index untouched.
|
|
255
256
|
*/
|
|
256
257
|
export function indexForCheckout(committedIndex, argv) {
|
|
@@ -0,0 +1,141 @@
|
|
|
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 { editionWorkspaceConfig } from './edition-config.mjs';
|
|
26
|
+
import { buildCapabilityDomainOwners, serializeJson } from './lib.mjs';
|
|
27
|
+
|
|
28
|
+
/** Why a develop candidate was refused. */
|
|
29
|
+
export const DevelopEditionInvariantReason = Object.freeze({
|
|
30
|
+
RESERVATION_TABLE_EMPTY: 'reservation-table-empty',
|
|
31
|
+
RESERVATION_TABLE_NOT_PLATFORM_TABLE: 'reservation-table-not-platform-table',
|
|
32
|
+
RESERVATION_TABLE_REFUSED_BY_KERNEL: 'reservation-table-refused-by-kernel',
|
|
33
|
+
CONFINED_BIOME_UNKNOWN: 'confined-biome-unknown',
|
|
34
|
+
CONFINED_BIOME_UNADMITTED: 'confined-biome-unadmitted',
|
|
35
|
+
CONFINEMENT_ADMISSION_STALE: 'confinement-admission-stale',
|
|
36
|
+
CONFINEMENT_CONSEQUENCE_MISSING: 'confinement-consequence-missing',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** The shortest `consequence` that can name what a confinement protects. */
|
|
40
|
+
const MIN_CONSEQUENCE_LENGTH = 40;
|
|
41
|
+
|
|
42
|
+
/** A develop candidate broke a plane invariant. `violations` carries every one, each with its reason. */
|
|
43
|
+
export class DevelopEditionInvariantError extends Error {
|
|
44
|
+
/** @param {string} id @param {{ reason: string, detail: string }[]} violations */
|
|
45
|
+
constructor(id, violations) {
|
|
46
|
+
super(
|
|
47
|
+
`develop edition "${id}" breaks ${violations.length} plane invariant(s):\n` +
|
|
48
|
+
violations.map(({ reason, detail }) => ` - [${reason}] ${detail}`).join('\n'),
|
|
49
|
+
);
|
|
50
|
+
this.name = 'DevelopEditionInvariantError';
|
|
51
|
+
this.distributionId = id;
|
|
52
|
+
this.violations = violations;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The reservation violations of `lock`, whose graph's full biome index is
|
|
58
|
+
* `biomeIndex`. `resolveFirstPartyCapabilityDomains` is the kernel's reader.
|
|
59
|
+
*/
|
|
60
|
+
export function reservationViolations({ lock, biomeIndex, resolveFirstPartyCapabilityDomains }) {
|
|
61
|
+
const R = DevelopEditionInvariantReason;
|
|
62
|
+
const table = lock.capabilityDomainOwners;
|
|
63
|
+
if (!Array.isArray(table) || table.length === 0) {
|
|
64
|
+
return [{
|
|
65
|
+
reason: R.RESERVATION_TABLE_EMPTY,
|
|
66
|
+
detail: 'capabilityDomainOwners is missing or empty: an empty reservation table makes NOTHING first-party (fail-open)',
|
|
67
|
+
}];
|
|
68
|
+
}
|
|
69
|
+
const violations = [];
|
|
70
|
+
try {
|
|
71
|
+
resolveFirstPartyCapabilityDomains(lock);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
violations.push({ reason: R.RESERVATION_TABLE_REFUSED_BY_KERNEL, detail: err.message });
|
|
74
|
+
}
|
|
75
|
+
if (serializeJson(table) !== serializeJson(buildCapabilityDomainOwners(biomeIndex))) {
|
|
76
|
+
violations.push({
|
|
77
|
+
reason: R.RESERVATION_TABLE_NOT_PLATFORM_TABLE,
|
|
78
|
+
detail:
|
|
79
|
+
'capabilityDomainOwners is not the table the frozen graph\'s full biome index builds. Ownership must not vary ' +
|
|
80
|
+
'by edition, or a trimmed edition leaves domains org-bindable',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return violations;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The confinement violations of edition `id` shipping `lock`, under the data
|
|
88
|
+
* root's `declarations`, with `knownBiomeIds` the frozen index's biome ids.
|
|
89
|
+
*/
|
|
90
|
+
export function confinementViolations({ id, lock, declarations, knownBiomeIds }) {
|
|
91
|
+
const R = DevelopEditionInvariantReason;
|
|
92
|
+
const shipped = new Set(lock.biomes.map((biome) => biome.id));
|
|
93
|
+
const violations = [];
|
|
94
|
+
for (const decl of declarations) {
|
|
95
|
+
if (typeof decl.consequence !== 'string' || decl.consequence.trim().length < MIN_CONSEQUENCE_LENGTH) {
|
|
96
|
+
violations.push({
|
|
97
|
+
reason: R.CONFINEMENT_CONSEQUENCE_MISSING,
|
|
98
|
+
detail: `confinement declaration "${decl.biomeId}" carries no substantive \`consequence\`; a refusal would name a rule without naming what it protects`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (!knownBiomeIds.has(decl.biomeId)) {
|
|
102
|
+
violations.push({
|
|
103
|
+
reason: R.CONFINED_BIOME_UNKNOWN,
|
|
104
|
+
detail:
|
|
105
|
+
`confinement declaration names biome "${decl.biomeId}", which the frozen biome index does not carry: it would ` +
|
|
106
|
+
'guard nothing. Repoint it to the renamed biome, or delete it if the biome is gone',
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const admitted = decl.admittedEditions.includes(id);
|
|
111
|
+
if (shipped.has(decl.biomeId) && !admitted) {
|
|
112
|
+
violations.push({
|
|
113
|
+
reason: R.CONFINED_BIOME_UNADMITTED,
|
|
114
|
+
detail:
|
|
115
|
+
`ships biome "${decl.biomeId}", which holds ${decl.holds}. That is a CONFINEMENT decision, not a packaging one: ` +
|
|
116
|
+
`${decl.consequence}. If intended, add "${id}" to its admittedEditions in edition-workspace.json in its own commit, naming who agreed`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (!shipped.has(decl.biomeId) && admitted) {
|
|
120
|
+
violations.push({
|
|
121
|
+
reason: R.CONFINEMENT_ADMISSION_STALE,
|
|
122
|
+
detail: `the admission "${decl.biomeId}" -> "${id}" suppresses NOTHING: the edition does not ship the biome. Delete it`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return violations;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Assert both invariants on develop candidate `id`. Throws
|
|
131
|
+
* {@link DevelopEditionInvariantError} naming every violation; returns the
|
|
132
|
+
* counts it graded otherwise.
|
|
133
|
+
*/
|
|
134
|
+
export function assertDevelopEditionInvariants({ id, lock, biomeIndex, schemas, declarations = editionWorkspaceConfig().confinedBiomes ?? [] }) {
|
|
135
|
+
const violations = [
|
|
136
|
+
...reservationViolations({ lock, biomeIndex, resolveFirstPartyCapabilityDomains: schemas.resolveFirstPartyCapabilityDomains }),
|
|
137
|
+
...confinementViolations({ id, lock, declarations, knownBiomeIds: new Set(biomeIndex.map((entry) => entry.id)) }),
|
|
138
|
+
];
|
|
139
|
+
if (violations.length > 0) {throw new DevelopEditionInvariantError(id, violations);}
|
|
140
|
+
return { reservedDomains: lock.capabilityDomainOwners.length, confinedBiomes: declarations.length };
|
|
141
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// develop-edition-resolve — resolve and seal one DEVELOP edition from a snapshot.
|
|
2
2
|
//
|
|
3
3
|
// The whole pipeline, in the order the release runs it, over the snapshot's
|
|
4
|
-
// injected inputs: distribution lock ->
|
|
4
|
+
// injected inputs: distribution lock -> plane invariants (reservation table,
|
|
5
|
+
// confinement; develop-edition-invariants.mjs) -> authorization base (composed, never
|
|
5
6
|
// written) -> images lock -> develop seal, plus the chart-set lock: the frozen
|
|
6
7
|
// deploy commit's develop chart set (develop-chart-set.mjs). Nothing here writes to the editions
|
|
7
8
|
// trees; the caller writes the artifact (edition-artifact.mjs).
|
|
@@ -10,6 +11,7 @@ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest }
|
|
|
10
11
|
import { biomeSourceTreeIds, readBiomeSourceFacts, sealLock } from './biome-content.mjs';
|
|
11
12
|
import { buildCatalogue } from './catalogue.mjs';
|
|
12
13
|
import { resolveDevelopChartSet } from './develop-chart-set.mjs';
|
|
14
|
+
import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
|
|
13
15
|
import { createDevelopImageProbe } from './develop-image-probe.mjs';
|
|
14
16
|
import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
|
|
15
17
|
import { EditionChannel } from './edition-constants.mjs';
|
|
@@ -30,6 +32,7 @@ export const DEVELOP_EDITION_STEPS = Object.freeze({
|
|
|
30
32
|
assertFrozenCheckouts,
|
|
31
33
|
buildSnapshotInputs,
|
|
32
34
|
resolveDistributionWithIndex,
|
|
35
|
+
assertDevelopEditionInvariants,
|
|
33
36
|
assertAdapterBundlesMatchHost,
|
|
34
37
|
composeAuthorizationBase,
|
|
35
38
|
resolveImages,
|
|
@@ -59,6 +62,9 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
59
62
|
// One line per phase, with the time since the resolve began, so the step is never silent for long.
|
|
60
63
|
const phase = (name) => progress(`[develop-edition] ${name} (+${Date.now() - startedAt}ms)`);
|
|
61
64
|
const { schemas, inputs, lock, index } = await resolveDevelopSnapshot({ id, pins, steps, phase });
|
|
65
|
+
// The plane invariants, against the graph this candidate froze (develop-edition-invariants.mjs).
|
|
66
|
+
const graded = steps.assertDevelopEditionInvariants({ id, lock, biomeIndex: inputs.biomeIndex, schemas });
|
|
67
|
+
phase(`reservation table asserted (${graded.reservedDomains} domains); confinement asserted (${graded.confinedBiomes} declaration(s))`);
|
|
62
68
|
await steps.assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest });
|
|
63
69
|
const authorizationLock = await steps.composeAuthorizationBase(id, lock, index);
|
|
64
70
|
phase('distribution lock resolved');
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
// freeze-graph --data-sha <sha> --kernel-sha <sha>
|
|
29
29
|
// --deploy-sha <sha> [--editions-root-url <https url>]
|
|
30
30
|
// [--overrides <json>] --out <file>
|
|
31
|
-
// resolve refs/heads/develop once per graph repository (
|
|
32
|
-
//
|
|
31
|
+
// resolve refs/heads/develop once per develop-graph repository (the data root's
|
|
32
|
+
// firstPartySources, at each declared URL) and the editions root; the three checked-out
|
|
33
33
|
// repositories are pinned at what the workflow checked out; write the pins. --kernel-sha is refused
|
|
34
34
|
// unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
|
|
35
35
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
@@ -76,7 +76,7 @@ import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
|
76
76
|
import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
|
|
77
77
|
import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
78
78
|
import { EditionChannel } from './edition-constants.mjs';
|
|
79
|
-
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir
|
|
79
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir } from './edition-workspace.mjs';
|
|
80
80
|
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
81
81
|
import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protocol-choice.mjs';
|
|
82
82
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
@@ -187,7 +187,6 @@ async function run(argv, env) {
|
|
|
187
187
|
case 'freeze-graph': {
|
|
188
188
|
assertKernelCommitSupported({ repoDir: kernelRepoDir(), commit: options['--kernel-sha'], label: `--kernel-sha ${options['--kernel-sha']}` });
|
|
189
189
|
const pins = freezeGraph({
|
|
190
|
-
runtimeSourceIndex: readStrictJsonFile(runtimeImageSourceIndexPath()),
|
|
191
190
|
dataSha: options['--data-sha'],
|
|
192
191
|
kernelSha: options['--kernel-sha'],
|
|
193
192
|
deploySha: options['--deploy-sha'],
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
17
|
|
|
18
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
19
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
18
20
|
import { EditionChannel } from './edition-channel.mjs';
|
|
19
21
|
import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
|
|
20
|
-
import { canonicalPublicHttpsRepositoryUrl } from './lib.mjs';
|
|
21
22
|
import {
|
|
22
23
|
dataPinKey,
|
|
23
24
|
EDITIONS_ROOT_PIN_KEY,
|
|
@@ -144,7 +145,6 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
147
|
* @param {object} request
|
|
147
|
-
* @param {object} request.runtimeSourceIndex the checkout's runtime-image-source-index.json (raw)
|
|
148
148
|
* @param {string} request.dataSha the commit the data root checkout is at
|
|
149
149
|
* @param {string} request.kernelSha the commit the kernel checkout is at
|
|
150
150
|
* @param {string} request.deploySha the commit the deploy checkout is at
|
|
@@ -152,7 +152,6 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
|
152
152
|
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
|
|
153
153
|
*/
|
|
154
154
|
export function freezeGraph({
|
|
155
|
-
runtimeSourceIndex,
|
|
156
155
|
dataSha,
|
|
157
156
|
kernelSha,
|
|
158
157
|
deploySha,
|
|
@@ -164,7 +163,9 @@ export function freezeGraph({
|
|
|
164
163
|
assertNoToolingOverride(overrides);
|
|
165
164
|
assertOverrides(overrides, GRAPH_PHASE);
|
|
166
165
|
const head = memoizedHeads(resolveHead);
|
|
167
|
-
|
|
166
|
+
// The develop graph is the data root's first-party repositories (develop-graph.mjs), never the release pins.
|
|
167
|
+
const members = developGraphMembers();
|
|
168
|
+
const graph = new Set(members.map((member) => member.repo));
|
|
168
169
|
for (const [key] of Object.entries(overrides)) {
|
|
169
170
|
if (snapshotPinNamespace(key) === SnapshotPinNamespace.Graph && !graph.has(key)) {
|
|
170
171
|
throw new Error(`override names "${key}", which is not a repository in the graph`);
|
|
@@ -176,7 +177,7 @@ export function freezeGraph({
|
|
|
176
177
|
{ key: deployPinKey(), sha: deploySha, flag: '--deploy-sha', what: 'the deploy checkout' },
|
|
177
178
|
];
|
|
178
179
|
for (const { key, sha, flag, what } of checkedOut) {assertCheckedOutPin({ key, sha, flag, what, overrides });}
|
|
179
|
-
const toResolve =
|
|
180
|
+
const toResolve = members.map((member) => ({ name: member.repo, url: member.repositoryUrl }));
|
|
180
181
|
if (editionsRootUrl !== undefined) {
|
|
181
182
|
toResolve.push({ name: EDITIONS_ROOT_PIN_KEY, url: canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url') });
|
|
182
183
|
}
|
|
@@ -186,8 +187,8 @@ export function freezeGraph({
|
|
|
186
187
|
throw new Error(`override names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
|
|
187
188
|
}
|
|
188
189
|
const pins = Object.fromEntries(checkedOut.map(({ key, sha }) => [key, sha]));
|
|
189
|
-
for (const
|
|
190
|
-
pins[
|
|
190
|
+
for (const member of members) {
|
|
191
|
+
pins[member.repo] = overrides[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
|
|
191
192
|
}
|
|
192
193
|
if (editionsRootUrl !== undefined) {
|
|
193
194
|
const url = canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// develop-graph — which repositories a DEVELOP candidate is built from.
|
|
2
|
+
//
|
|
3
|
+
// The develop graph is every first-party repository the data root declares
|
|
4
|
+
// (`firstPartySources` in edition-workspace.json), each at its declared
|
|
5
|
+
// `repositoryUrl`. It is never read from the release pins
|
|
6
|
+
// (`runtime-image-source-index.json#repositoryPins`): develop and release are
|
|
7
|
+
// separate lifecycles, and a repository is first-party on develop before any
|
|
8
|
+
// release pins it. freeze-graph, the snapshot checkout, the snapshot pins'
|
|
9
|
+
// validation and the snapshot's index all take their members from here.
|
|
10
|
+
|
|
11
|
+
import { firstPartySources } from './first-party-source-ids.mjs';
|
|
12
|
+
import { compareStrings } from './lib.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The develop graph: `{ repo, repositoryUrl }` per declared first-party
|
|
16
|
+
* repository, sorted by repo handle.
|
|
17
|
+
*
|
|
18
|
+
* @returns {readonly { repo: string, repositoryUrl: string }[]}
|
|
19
|
+
*/
|
|
20
|
+
export function developGraphMembers() {
|
|
21
|
+
return Object.freeze(
|
|
22
|
+
Object.entries(firstPartySources('the develop graph'))
|
|
23
|
+
.map(([repo, { repositoryUrl }]) => Object.freeze({ repo, repositoryUrl }))
|
|
24
|
+
.sort((a, b) => compareStrings(a.repo, b.repo)),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -81,7 +81,6 @@ export async function buildSnapshotInputs({ id, pins, schemas }) {
|
|
|
81
81
|
readStrictJsonFile(runtimeImageSourceIndexPath()),
|
|
82
82
|
);
|
|
83
83
|
validateSnapshotPins(pins, {
|
|
84
|
-
runtimeSourceIndex: committedIndex,
|
|
85
84
|
declaredSourceNames: composed.declaredSourceNames,
|
|
86
85
|
editionsRootExternal: editionsRoot() !== dataRoot(),
|
|
87
86
|
});
|