@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.
@@ -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
- * committed index applied to that snapshot (the develop edition's frozen pins);
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 -> authorization base (composed, never
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 (and the editions
32
- // root) from this checkout's runtime-image-source-index.json; the three checked-out
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, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
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
- const graph = new Set(runtimeSourceIndex.repositoryPins.map((pin) => pin.repo));
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 = runtimeSourceIndex.repositoryPins.map((pin) => ({ name: pin.repo, url: pin.repositoryUrl }));
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 pin of runtimeSourceIndex.repositoryPins) {
190
- pins[pin.repo] = overrides[pin.repo] ?? head({ name: pin.repo, url: pin.repositoryUrl });
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
  });