@xemahq/xema 0.33.2 → 0.33.4

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,158 @@
1
+ /**
2
+ * The ONE reader of a consumer repository's cross-repo build-input record, and
3
+ * the resolver built on it. The source checkout (which commits to fetch) and the
4
+ * image resolve (which bytes to hash) both read the record here, at the
5
+ * consumer's pin, so the plan and the resolve cannot diverge.
6
+ */
7
+ import { sourcePath } from './edition-workspace.mjs';
8
+ import { readGitJsonAtCommit } from './git-json.mjs';
9
+
10
+ /**
11
+ * The producing repository's OWN committed record of what it vendored.
12
+ *
13
+ * `vendor-cross-repo-inputs.mjs` reads exactly this file, at the commit being
14
+ * built, to emit the `--vendored <dest>=<repoDir>@<commit>:<src>` arguments it
15
+ * hands `compute-image-tag.mjs`. It is therefore the ONLY source of the vendored
16
+ * commit that can agree with the tag CI pushes.
17
+ */
18
+ export const CROSS_REPO_BUILD_INPUTS_PATH =
19
+ 'tooling/release/cross-repo-build-inputs.json';
20
+
21
+ /** Identity of one vendored input — the tuple that survives a pin bump. */
22
+ export function crossRepoInputKey(input) {
23
+ return [input.sourceRepo, input.sourcePath, input.destinationPath].join('\u0000');
24
+ }
25
+
26
+ /** The `[sourceRepo, sourcePath, destinationPath]` a `crossRepoInputKey` frames. */
27
+ export function splitCrossRepoInputKey(key) {
28
+ return key.split('\u0000');
29
+ }
30
+
31
+ /**
32
+ * The consumer's record of what it vendored, read at ONE commit of the consumer
33
+ * (never the work tree): a map from `crossRepoInputKey` to the vendored
34
+ * `sourceCommit`. Unreadable is an error, not an empty record.
35
+ */
36
+ export function readCrossRepoBuildInputsRecord({
37
+ consumerRepo,
38
+ consumerRepoDir,
39
+ consumerCommit,
40
+ }) {
41
+ let record;
42
+ try {
43
+ record = readGitJsonAtCommit(
44
+ consumerRepoDir,
45
+ CROSS_REPO_BUILD_INPUTS_PATH,
46
+ consumerCommit,
47
+ );
48
+ } catch (cause) {
49
+ throw new Error(
50
+ `"${consumerRepo}" declares cross-repo build inputs but ` +
51
+ `${CROSS_REPO_BUILD_INPUTS_PATH} is unreadable at ${consumerCommit}. ` +
52
+ `That file is what its CI feeds to compute-image-tag.mjs; without it ` +
53
+ `no coordinate this tooling derives can match the tag CI pushes.`,
54
+ { cause },
55
+ );
56
+ }
57
+ const mirrored = new Map();
58
+ for (const input of record?.inputs ?? []) {
59
+ mirrored.set(crossRepoInputKey(input), input.sourceCommit);
60
+ }
61
+ return mirrored;
62
+ }
63
+
64
+ /**
65
+ * Resolve the vendored inputs of ONE consumer repository at ONE commit.
66
+ *
67
+ * WHY THE COMMIT COMES FROM THE CONSUMER, NOT FROM THE INDEX
68
+ *
69
+ * The index's `crossRepoBuildInputs` carry no `sourceCommit`; they name only
70
+ * WHICH content is vendored. Both this tooling and the parity gate used to
71
+ * supply the missing commit from `runtimeSourceIndex.repositoryPins`, and CI
72
+ * supplies it from the file above — so the two disagreed the moment a consumer
73
+ * repository's mirror lagged the index by one pin, which is its NORMAL resting
74
+ * state: the mirror is generated FROM the index and committed separately, so it
75
+ * is stale by construction between the index moving and the consumer
76
+ * regenerating.
77
+ *
78
+ * That is not a cosmetic disagreement. `crossRepo.sourceCommit` is framed into
79
+ * the component KEY of the image-input hash, so a pin that differs by one
80
+ * commit yields a different `contentHash` FOR BYTE-IDENTICAL VENDORED CONTENT —
81
+ * a coordinate no CI run has ever pushed or ever will. Measured 2026-09-07 on
82
+ * `origin/main`: five of the sixty-eight coordinates in the eight editions
83
+ * (`backlog-api`, `channel-gateway-api`, `control-plane-api`,
84
+ * `document-render-api` from repos/xema-community, and `biome-host-api` from
85
+ * repos/xema-base) 404'd in the registry for exactly this reason, while the tags
86
+ * their producing CI DID push were present and served on both platforms.
87
+ *
88
+ * Reading the index pin also imposed a requirement that cannot be satisfied.
89
+ * `repos/xema-base` vendors from `repos/xema-community` and vice versa, so
90
+ * "every mirror agrees with the index" is a mutual fixed point over a 2-cycle,
91
+ * and bumping either side invalidates the other. No such pair exists: scanning
92
+ * eighty commits of each on `origin/main` found ZERO mutually coherent pairs.
93
+ * Reading the consumer's own record removes the fixed point entirely — an
94
+ * image's identity depends only on the repository that builds it.
95
+ *
96
+ * It stays edition-INDEPENDENT, which is the property the previous design was
97
+ * protecting: the record is a property of the consumer's COMMIT, not of any
98
+ * edition's resolved commit map, so every edition shipping a subject still
99
+ * derives the same `contentHash` for it.
100
+ */
101
+ export function resolveCrossRepoVendoredInputs({
102
+ indexInputs,
103
+ consumerRepo,
104
+ consumerRepoDir,
105
+ consumerCommit,
106
+ }) {
107
+ const declared = indexInputs.filter(
108
+ (input) => input.consumerRepo === consumerRepo,
109
+ );
110
+ if (declared.length === 0) {return [];}
111
+ if (!consumerCommit) {
112
+ throw new Error(
113
+ `cross-repo build inputs are declared for "${consumerRepo}", but this ` +
114
+ `distribution resolves no commit for it — the vendored content cannot ` +
115
+ `be pinned, so the image cannot be addressed`,
116
+ );
117
+ }
118
+ const mirrored = readCrossRepoBuildInputsRecord({
119
+ consumerRepo,
120
+ consumerRepoDir,
121
+ consumerCommit,
122
+ });
123
+ // Set equality in BOTH directions. A commit that LAGS is normal and is now
124
+ // honoured; a differing input SET is a genuine declaration disagreement, and
125
+ // it moves the hash just as surely — so it must fail rather than resolve to a
126
+ // coordinate built from a different closure.
127
+ const declaredKeys = new Set(declared.map((input) => crossRepoInputKey(input)));
128
+ const resolved = declared.map((input) => {
129
+ const sourceCommit = mirrored.get(crossRepoInputKey(input));
130
+ if (!sourceCommit) {
131
+ throw new Error(
132
+ `the runtime image source index declares a cross-repo build input for ` +
133
+ `"${consumerRepo}" vendoring "${input.sourceRepo}:${input.sourcePath}" ` +
134
+ `to "${input.destinationPath}", but ${CROSS_REPO_BUILD_INPUTS_PATH} at ` +
135
+ `${consumerCommit} declares no such input — its CI does not vendor it, ` +
136
+ `so the coordinate this tooling would derive is one nothing builds`,
137
+ );
138
+ }
139
+ return {
140
+ consumerService: input.consumerService ?? null,
141
+ destinationPath: input.destinationPath,
142
+ sourceRepoDir: sourcePath(input.sourceRepo),
143
+ sourcePath: input.sourcePath,
144
+ sourceCommit,
145
+ };
146
+ });
147
+ for (const key of mirrored.keys()) {
148
+ if (declaredKeys.has(key)) {continue;}
149
+ const [sourceRepo, sourcePath, destinationPath] = splitCrossRepoInputKey(key);
150
+ throw new Error(
151
+ `${CROSS_REPO_BUILD_INPUTS_PATH} at ${consumerCommit} vendors ` +
152
+ `"${sourceRepo}:${sourcePath}" to "${destinationPath}" for ` +
153
+ `"${consumerRepo}", which the runtime image source index does not ` +
154
+ `declare — its CI bakes content this tooling does not hash`,
155
+ );
156
+ }
157
+ return resolved;
158
+ }
@@ -0,0 +1,61 @@
1
+ // develop-chart-set — the chart-set lock of a develop edition.
2
+ //
3
+ // A develop edition renders with the charts the publisher actually published:
4
+ // the committed release chart-set lock's entries (names, content versions and
5
+ // digests, and each index digest), unchanged. xema-deploy publishes charts per
6
+ // content version (`<n>-h<16 hex>`) for the commit its release set was closed
7
+ // at, so that commit is the only deploy commit whose charts exist in the
8
+ // registry. A develop edition frozen at that commit takes the release lock
9
+ // as it is, and asks the registry nothing about a chart's tag; a develop edition
10
+ // frozen at any other commit is refused, because no chart was published for it
11
+ // (a develop chart lane in xema-deploy is the owner's decision, not built).
12
+
13
+ import { basename } from 'node:path';
14
+
15
+ import { chartSetSourceCommits } from './chart-set-source-commit.mjs';
16
+ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
17
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
18
+ import { deployPinKey } from './pin-source.mjs';
19
+ import { CHART_SET_LOCK_FILE, chartRegistryClient, chartSetLockPath, readLock } from './resolve-chart-set.mjs';
20
+
21
+ /** The source commits of `lock`'s publishers, read from their index charts at the digests the lock pins. */
22
+ const registrySourceCommits = (lock) => chartSetSourceCommits({ client: chartRegistryClient(), lock });
23
+
24
+ /**
25
+ * The develop chart-set lock for the deploy commit `xemaDeploySha`: the committed
26
+ * release lock, when that lock's deploy publisher closed its set at this commit.
27
+ * `readSourceCommits(lock)` defaults to the chart registry's index charts and
28
+ * returns `{publishers: [{repo, sourceCommit}]}`.
29
+ *
30
+ * TEMPORARY BRIDGE (owner: edition, lead D41 10-13): only the release set's
31
+ * source commit is resolvable here. Exit: develop charts resolved by content
32
+ * version (edition T37: the shared developChartVersionFromDigest, published by
33
+ * xema-deploy publish-charts on develop pushes), which replaces this function
34
+ * before t348-i2 lands.
35
+ */
36
+ export async function resolveDevelopChartSet({ xemaDeploySha, releaseLock = readLock(), readSourceCommits = registrySourceCommits }) {
37
+ if (!COMMIT_PATTERN.test(xemaDeploySha ?? '')) {
38
+ throw new Error(`the deploy checkout SHA is not a 40-hex commit: ${JSON.stringify(xemaDeploySha)}`);
39
+ }
40
+ if (releaseLock === null) {
41
+ throw new Error(`${chartSetLockPath()} is missing or is not JSON: run 'xema edition chart-set resolve' first`);
42
+ }
43
+ assertRosteredChartSetLock(releaseLock, CHART_SET_LOCK_FILE);
44
+ const deployRepo = basename(deployPinKey());
45
+ const deployPublishers = releaseLock.publishers.filter((publisher) => basename(publisher.repo) === deployRepo);
46
+ if (deployPublishers.length !== 1) {
47
+ throw new Error(
48
+ `${deployPublishers.length} chart publishers of ${CHART_SET_LOCK_FILE} have repository ${deployRepo} (the deploy pin role); exactly one must, ` +
49
+ 'since its source commit is the one commit whose charts were published',
50
+ );
51
+ }
52
+ const { sourceCommit } = (await readSourceCommits(releaseLock)).publishers.find((publisher) => publisher.index === deployPublishers[0].index) ?? {};
53
+ if (sourceCommit !== xemaDeploySha) {
54
+ throw new Error(
55
+ `the develop deploy commit ${xemaDeploySha} is not the commit ${CHART_SET_LOCK_FILE} was published from (${sourceCommit}): ` +
56
+ 'develop charts are only published for the release set\'s source commit, so no chart exists for this commit. ' +
57
+ 'A develop chart lane in the deploy repository is required to develop at another commit (owner decision, not built)',
58
+ );
59
+ }
60
+ return releaseLock;
61
+ }
@@ -2,26 +2,26 @@
2
2
  //
3
3
  // The whole pipeline, in the order the release runs it, over the snapshot's
4
4
  // injected inputs: distribution lock -> authorization base (composed, never
5
- // written) -> images lock -> develop seal, plus the chart-set lock: the frozen
6
- // xema-deploy charts' registry manifest digests, in the release roster shape. Nothing here writes to the editions
5
+ // written) -> images lock -> develop seal, plus the chart-set lock: the release
6
+ // lock's published charts (develop-chart-set.mjs). Nothing here writes to the editions
7
7
  // trees; the caller writes the artifact (edition-artifact.mjs).
8
8
 
9
9
  import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
10
10
  import { biomeSourceTreeIds, sealLock } from './biome-content.mjs';
11
11
  import { buildCatalogue } from './catalogue.mjs';
12
+ import { resolveDevelopChartSet } from './develop-chart-set.mjs';
12
13
  import { sealDevelopImages } from './develop-seal.mjs';
13
14
  import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
14
15
  import { EditionChannel } from './edition-constants.mjs';
15
- import { deployRoot, kernelPackageDir } from './edition-workspace.mjs';
16
+ import { kernelPackageDir } from './edition-workspace.mjs';
16
17
  import { kernelPackageOfAuthority } from './kernel-package.mjs';
17
18
  import { loadKernelDistributionSchemas } from './lib.mjs';
18
19
  import { deployPinKey } from './pin-source.mjs';
19
- import { resolveDevelopChartSet } from './resolve-chart-set.mjs';
20
20
  import { composeAuthorizationBase, resolveDistributionWithIndex } from './resolve-distribution.mjs';
21
21
  import { externalSourceTrees, resolveImages, sourceTreeLocator } from './resolve-images.mjs';
22
22
  import { createPinnedSourceAuthority } from './source-authority.mjs';
23
23
 
24
- export async function resolveDevelopEdition({ id, pins, env = process.env, sealOptions = {}, chartDigestLookup, fetchKernelTarball, fetchManifest = fetchRegistryManifest }) {
24
+ export async function resolveDevelopEdition({ id, pins, env = process.env, sealOptions = {}, readSourceCommits, fetchKernelTarball, fetchManifest = fetchRegistryManifest }) {
25
25
  const schemas = await loadKernelDistributionSchemas();
26
26
  assertFrozenCheckouts(pins);
27
27
  const inputs = await buildSnapshotInputs({ id, pins, schemas });
@@ -41,9 +41,8 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
41
41
  vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
42
42
  });
43
43
  const chartSetLock = await resolveDevelopChartSet({
44
- xemaDeployDir: deployRoot(),
45
44
  xemaDeploySha: pins[deployPinKey()],
46
- lookup: chartDigestLookup,
45
+ ...(readSourceCommits === undefined ? {} : { readSourceCommits }),
47
46
  });
48
47
  const sourceTreeIds = await biomeSourceTreeIds({
49
48
  schemas,
@@ -37,7 +37,6 @@ import {
37
37
  loadKernelAuthorizationComposer,
38
38
  } from './authorization-base.mjs';
39
39
  import { verifyBiomeContentHashes } from './biome-content.mjs';
40
- import { EditionChannel } from './edition-channel.mjs';
41
40
  import {
42
41
  EDITION_CHART_SET_FILE,
43
42
  EditionChartSetCheck,
@@ -951,7 +950,7 @@ async function main() {
951
950
  let chartSetPinText = null;
952
951
  try {
953
952
  chartSetPinText = readFileSync(fleetChartSetLockPath(), 'utf8');
954
- assertRosteredChartSetLock(JSON.parse(chartSetPinText), EDITION_CHART_SET_FILE, EditionChannel.Release);
953
+ assertRosteredChartSetLock(JSON.parse(chartSetPinText), EDITION_CHART_SET_FILE);
955
954
  } catch (err) {
956
955
  console.error(`[dist:check] chart-set pin: ${err.message}. Run \`${commandFor('resolve-chart-set.mjs')}\`.`);
957
956
  failed = true;
@@ -276,7 +276,7 @@ export function writeEditionArtifact({
276
276
  throw new Error(`edition "${distributionId}" has no ${file}: an edition carries all four locks`);
277
277
  }
278
278
  }
279
- assertRosteredChartSetLock(chartSetLock, EDITION_CHART_SET_FILE, channel);
279
+ assertRosteredChartSetLock(chartSetLock, EDITION_CHART_SET_FILE);
280
280
  const descriptor = editionDescriptor({ distributionId, channel, pins, workflow, evidenceTrustPolicy, engineImage });
281
281
  mkdirSync(outDir, { recursive: true });
282
282
  for (const [file, value] of [[EDITION_DESCRIPTOR_FILE, descriptor], ...byFile]) {
@@ -26,7 +26,6 @@
26
26
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
27
27
  import { join } from 'node:path';
28
28
 
29
- import { EditionChannel } from './edition-channel.mjs';
30
29
  import { dataRoot } from './edition-workspace.mjs';
31
30
 
32
31
  export const EDITION_CHART_SET_FILE = 'chart-set.lock.json';
@@ -39,38 +38,12 @@ export const EditionChartSetCheck = Object.freeze({
39
38
  STALE: 'stale',
40
39
  });
41
40
 
42
- /**
43
- * A published chart's version coordinate, per channel: `-h<16 hex>` (content
44
- * hash) in a release chart set, `-dev.<12 hex>` (the frozen xema-deploy SHA) in a
45
- * develop one. One lock SHAPE serves both; the coordinate is what the producer
46
- * of each channel writes, so each producer validates its own channel's (this is
47
- * a check of what is being written, not a branch of any reader).
48
- */
49
- const CHART_VERSION_COORDINATE = Object.freeze({
50
- [EditionChannel.Release]: /-h[0-9a-f]{16}$/,
51
- [EditionChannel.Develop]: /-dev\.[0-9a-f]{12}$/,
52
- });
41
+ /** A published chart's version coordinate: `-h<16 hex>`, the content hash its publisher mints. A develop chart set takes the release set's charts, so it carries the same coordinate. */
42
+ const CHART_VERSION_COORDINATE = /-h[0-9a-f]{16}$/;
53
43
 
54
44
  /** The chart-set lock schema every reader accepts; a lock of any other schema is re-resolved, never migrated. */
55
45
  export const CHART_SET_LOCK_SCHEMA_VERSION = 4;
56
46
 
57
- /** The version prefix of a develop snapshot chart set (`0.0.0-dev.<sha12>`): xema-deploy's frozen-SHA charts, which have no index chart. */
58
- export const DEVELOP_CHART_VERSION_PREFIX = '0.0.0-dev.';
59
-
60
- /** The length of the lowercase-hex xema-deploy SHA a develop snapshot version carries. */
61
- export const DEVELOP_CHART_SHA_LENGTH = 12;
62
-
63
- /** The full develop snapshot version grammar: `0.0.0-dev.<12 lowercase hex>`, anchored. */
64
- export const DEVELOP_CHART_VERSION_PATTERN = new RegExp(
65
- `^${DEVELOP_CHART_VERSION_PREFIX.replaceAll('.', '\\.')}[0-9a-f]{${DEVELOP_CHART_SHA_LENGTH}}$`,
66
- );
67
-
68
- /** Does `version` start with the develop prefix, whether or not it is the full develop grammar? */
69
- export const hasDevelopChartVersionPrefix = (version) => typeof version === 'string' && version.startsWith(DEVELOP_CHART_VERSION_PREFIX);
70
-
71
- /** Is `version` a develop snapshot version? The one predicate for "this publisher has no index chart". */
72
- export const isDevelopChartVersion = (version) => typeof version === 'string' && DEVELOP_CHART_VERSION_PATTERN.test(version);
73
-
74
47
  /** An OCI manifest digest: `sha256:` and 64 lowercase hex. */
75
48
  export const OCI_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
76
49
 
@@ -79,15 +52,10 @@ export const RESOLVE_CHART_SET_INSTRUCTION = 're-resolve the chart set with `xem
79
52
 
80
53
  /**
81
54
  * A chart-set lock (schemaVersion 4) that pins every member by version and OCI
82
- * manifest digest, with `channel`'s version coordinate, and pins each publisher's
83
- * index chart by `indexDigest` (null exactly for a develop snapshot version,
84
- * which has no index chart; the release channel refuses null).
55
+ * manifest digest, with the `-h<16 hex>` content coordinate, and pins each
56
+ * publisher's index chart by `indexDigest` (always a sha256 digest).
85
57
  */
86
- export function assertRosteredChartSetLock(lock, label, channel) {
87
- const coordinate = CHART_VERSION_COORDINATE[channel];
88
- if (coordinate === undefined) {
89
- throw new Error(`${label}: cannot validate a chart-set lock for channel ${JSON.stringify(channel)}; expected ${Object.values(EditionChannel).join(' | ')}`);
90
- }
58
+ export function assertRosteredChartSetLock(lock, label) {
91
59
  const fail = (why) => {
92
60
  throw new Error(`${label} is not a rostered chart-set lock: ${why}`);
93
61
  };
@@ -101,30 +69,20 @@ export function assertRosteredChartSetLock(lock, label, channel) {
101
69
  }
102
70
  for (const [name, member] of Object.entries(pin.charts)) {
103
71
  if (!member || typeof member !== 'object' || Array.isArray(member)) {fail(`${pin.index}: ${name} must be {version, digest}`);}
104
- if (!coordinate.test(String(member.version))) {fail(`${pin.index}: ${name} has no ${channel} content coordinate (${member.version}); a ${channel} chart set carries ${coordinate}`);}
72
+ if (!CHART_VERSION_COORDINATE.test(String(member.version))) {fail(`${pin.index}: ${name} has no content coordinate (${member.version}); a chart set carries ${CHART_VERSION_COORDINATE}`);}
105
73
  if (!OCI_DIGEST_PATTERN.test(String(member.digest))) {fail(`${pin.index}: ${name} has no sha256 manifest digest`);}
106
74
  }
107
- assertIndexDigest(pin, position, channel, fail);
75
+ assertIndexDigest(pin, position, fail);
108
76
  }
109
77
  return lock;
110
78
  }
111
79
 
112
- /** The `indexDigest` rule: present, a sha256 digest unless the version is a develop snapshot (then null), and never null in a release lock. */
113
- function assertIndexDigest(pin, position, channel, fail) {
80
+ /** The `indexDigest` rule: the key is present and is a sha256 digest. */
81
+ function assertIndexDigest(pin, position, fail) {
114
82
  const where = `publisher ${position} (${pin?.index ?? '?'})`;
115
83
  if (!pin || typeof pin !== 'object' || !Object.hasOwn(pin, 'indexDigest')) {
116
- fail(`${where} carries no indexDigest key (required: "sha256:<64 hex>" for a release index, null for a develop snapshot version)`);
117
- }
118
- if (hasDevelopChartVersionPrefix(pin.version) && !isDevelopChartVersion(pin.version)) {
119
- fail(`${where} has malformed develop chart version ${JSON.stringify(pin.version)}; a develop version is exactly ${DEVELOP_CHART_VERSION_PREFIX}<${DEVELOP_CHART_SHA_LENGTH} lowercase hex>`);
120
- }
121
- const develop = isDevelopChartVersion(pin.version);
122
- if (pin.indexDigest === null) {
123
- if (!develop) {fail(`${where} has indexDigest null with version ${JSON.stringify(pin.version)}; null is only for a develop snapshot version (${DEVELOP_CHART_VERSION_PREFIX}<sha12>)`);}
124
- if (channel === EditionChannel.Release) {fail(`${where} has indexDigest null, which a release chart set refuses; a release index chart is pinned by digest`);}
125
- return;
84
+ fail(`${where} carries no indexDigest key (required: "sha256:<64 hex>")`);
126
85
  }
127
- if (develop) {fail(`${where} has an indexDigest with develop snapshot version ${pin.version}; a develop snapshot has no index chart, so indexDigest must be null`);}
128
86
  if (!OCI_DIGEST_PATTERN.test(String(pin.indexDigest))) {fail(`${where} has a malformed indexDigest (${JSON.stringify(pin.indexDigest)}); expected "sha256:<64 lowercase hex>"`);}
129
87
  }
130
88
 
@@ -134,7 +92,7 @@ function assertIndexDigest(pin, position, channel, fail) {
134
92
  * coordinates is written.
135
93
  */
136
94
  export function writeEditionChartSet(editionDir, pinText) {
137
- assertRosteredChartSetLock(JSON.parse(pinText), EDITION_CHART_SET_FILE, EditionChannel.Release);
95
+ assertRosteredChartSetLock(JSON.parse(pinText), EDITION_CHART_SET_FILE);
138
96
  writeFileSync(join(editionDir, EDITION_CHART_SET_FILE), pinText);
139
97
  }
140
98
 
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+
4
+ import { parseStrictJson } from './strict-json.mjs';
5
+
6
+ const requireFromHere = createRequire(import.meta.url);
7
+
8
+ let loadedSourceHash;
9
+
10
+ /** Git-object primitives of `@xemahq/distribution-source-hash`; no kernel is consulted. */
11
+ function gitObjectReader() {
12
+ loadedSourceHash ??= requireFromHere('@xemahq/distribution-source-hash');
13
+ return loadedSourceHash;
14
+ }
15
+
16
+ /**
17
+ * Read + parse a JSON file out of an exact Git commit (never the work tree).
18
+ *
19
+ * This is the seam every authored source file crosses on its way into a lock
20
+ * or an index — `xema-biome.json`, a platform service's `package.json`,
21
+ * `cross-repo-build-inputs.json`, the committed generated biome index — so it
22
+ * parses STRICTLY: a duplicated object key is refused, naming the commit, the
23
+ * path and the JSON pointer of the object. `JSON.parse` would keep the last
24
+ * occurrence, which is how a `resources` declaration was dropped from a sealed
25
+ * lock with every reader agreeing (see `strict-json.mjs`).
26
+ *
27
+ * The commit check, the buffer ceiling and the exact-UTF-8 decode are the
28
+ * source-hash package's own (loaded WITHOUT the kernel check hashing needs, so
29
+ * the source checkout, which runs before any kernel is built, can read a record
30
+ * too); only the parse differs from its
31
+ * `readGitJsonAtCommit`, which is lenient and is therefore not delegated to.
32
+ */
33
+ export function readGitJsonAtCommit(repoDir, repoRelativePath, commit) {
34
+ const sourceHash = gitObjectReader();
35
+ sourceHash.assertGitCommitExists(repoDir, commit);
36
+ let raw;
37
+ try {
38
+ raw = execFileSync(
39
+ 'git',
40
+ ['-C', repoDir, 'show', `${commit}:${repoRelativePath}`],
41
+ {
42
+ encoding: 'buffer',
43
+ maxBuffer: sourceHash.GIT_OBJECT_MAX_BUFFER_BYTES,
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ },
46
+ );
47
+ } catch {
48
+ throw new Error(
49
+ `cannot read "${repoRelativePath}" from Git commit "${commit}"`,
50
+ );
51
+ }
52
+ const context = `Git object "${commit}:${repoRelativePath}"`;
53
+ return parseStrictJson(sourceHash.decodeExactUtf8(raw, context), context);
54
+ }
package/edition/lib.mjs CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  sourcePath,
31
31
  } from './edition-workspace.mjs';
32
32
  import { SHA256_HEX_PATTERN } from './evidence-grammars.mjs';
33
+ import { readGitJsonAtCommit } from './git-json.mjs';
33
34
  import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
34
35
  import {
35
36
  assertPrivilegesHavePrincipals,
@@ -42,7 +43,7 @@ import {
42
43
  PUBLISHED_SOURCE_REF,
43
44
  publishedSourceCommit,
44
45
  } from './source-authority.mjs';
45
- import { parseStrictJson, readStrictJsonFile } from './strict-json.mjs';
46
+ import { readStrictJsonFile } from './strict-json.mjs';
46
47
  import { compareStrings } from './string-order.mjs';
47
48
 
48
49
  export {
@@ -51,6 +52,7 @@ export {
51
52
  DistributionSourceMode,
52
53
  PUBLISHED_SOURCE_REF,
53
54
  publishedSourceCommit,
55
+ readGitJsonAtCommit,
54
56
  };
55
57
 
56
58
 
@@ -3063,167 +3065,3 @@ export function hashImageBuildInputs(request, algorithm) {
3063
3065
  return distributionSourceHash().hashImageBuildInputs(request, algorithm);
3064
3066
  }
3065
3067
 
3066
- /**
3067
- * Read + parse a JSON file out of an exact Git commit (never the work tree).
3068
- *
3069
- * This is the seam every authored source file crosses on its way into a lock
3070
- * or an index — `xema-biome.json`, a platform service's `package.json`,
3071
- * `cross-repo-build-inputs.json`, the committed generated biome index — so it
3072
- * parses STRICTLY: a duplicated object key is refused, naming the commit, the
3073
- * path and the JSON pointer of the object. `JSON.parse` would keep the last
3074
- * occurrence, which is how a `resources` declaration was dropped from a sealed
3075
- * lock with every reader agreeing (see `strict-json.mjs`).
3076
- *
3077
- * The commit check, the buffer ceiling and the exact-UTF-8 decode are the
3078
- * source-hash package's own; only the parse differs from its
3079
- * `readGitJsonAtCommit`, which is lenient and is therefore not delegated to.
3080
- */
3081
- export function readGitJsonAtCommit(repoDir, repoRelativePath, commit) {
3082
- const sourceHash = distributionSourceHash();
3083
- sourceHash.assertGitCommitExists(repoDir, commit);
3084
- let raw;
3085
- try {
3086
- raw = execFileSync(
3087
- 'git',
3088
- ['-C', repoDir, 'show', `${commit}:${repoRelativePath}`],
3089
- {
3090
- encoding: 'buffer',
3091
- maxBuffer: sourceHash.GIT_OBJECT_MAX_BUFFER_BYTES,
3092
- stdio: ['ignore', 'pipe', 'pipe'],
3093
- },
3094
- );
3095
- } catch {
3096
- throw new Error(
3097
- `cannot read "${repoRelativePath}" from Git commit "${commit}"`,
3098
- );
3099
- }
3100
- const context = `Git object "${commit}:${repoRelativePath}"`;
3101
- return parseStrictJson(sourceHash.decodeExactUtf8(raw, context), context);
3102
- }
3103
-
3104
- /**
3105
- * The producing repository's OWN committed record of what it vendored.
3106
- *
3107
- * `vendor-cross-repo-inputs.mjs` reads exactly this file, at the commit being
3108
- * built, to emit the `--vendored <dest>=<repoDir>@<commit>:<src>` arguments it
3109
- * hands `compute-image-tag.mjs`. It is therefore the ONLY source of the vendored
3110
- * commit that can agree with the tag CI pushes.
3111
- */
3112
- export const CROSS_REPO_BUILD_INPUTS_PATH =
3113
- 'tooling/release/cross-repo-build-inputs.json';
3114
-
3115
- /** Identity of one vendored input — the tuple that survives a pin bump. */
3116
- export function crossRepoInputKey(input) {
3117
- return [input.sourceRepo, input.sourcePath, input.destinationPath].join('\u0000');
3118
- }
3119
-
3120
- /**
3121
- * Resolve the vendored inputs of ONE consumer repository at ONE commit.
3122
- *
3123
- * WHY THE COMMIT COMES FROM THE CONSUMER, NOT FROM THE INDEX
3124
- *
3125
- * The index's `crossRepoBuildInputs` carry no `sourceCommit`; they name only
3126
- * WHICH content is vendored. Both this tooling and the parity gate used to
3127
- * supply the missing commit from `runtimeSourceIndex.repositoryPins`, and CI
3128
- * supplies it from the file above — so the two disagreed the moment a consumer
3129
- * repository's mirror lagged the index by one pin, which is its NORMAL resting
3130
- * state: the mirror is generated FROM the index and committed separately, so it
3131
- * is stale by construction between the index moving and the consumer
3132
- * regenerating.
3133
- *
3134
- * That is not a cosmetic disagreement. `crossRepo.sourceCommit` is framed into
3135
- * the component KEY of the image-input hash, so a pin that differs by one
3136
- * commit yields a different `contentHash` FOR BYTE-IDENTICAL VENDORED CONTENT —
3137
- * a coordinate no CI run has ever pushed or ever will. Measured 2026-09-07 on
3138
- * `origin/main`: five of the sixty-eight coordinates in the eight editions
3139
- * (`backlog-api`, `channel-gateway-api`, `control-plane-api`,
3140
- * `document-render-api` from repos/xema-community, and `biome-host-api` from
3141
- * repos/xema-base) 404'd in the registry for exactly this reason, while the tags
3142
- * their producing CI DID push were present and served on both platforms.
3143
- *
3144
- * Reading the index pin also imposed a requirement that cannot be satisfied.
3145
- * `repos/xema-base` vendors from `repos/xema-community` and vice versa, so
3146
- * "every mirror agrees with the index" is a mutual fixed point over a 2-cycle,
3147
- * and bumping either side invalidates the other. No such pair exists: scanning
3148
- * eighty commits of each on `origin/main` found ZERO mutually coherent pairs.
3149
- * Reading the consumer's own record removes the fixed point entirely — an
3150
- * image's identity depends only on the repository that builds it.
3151
- *
3152
- * It stays edition-INDEPENDENT, which is the property the previous design was
3153
- * protecting: the record is a property of the consumer's COMMIT, not of any
3154
- * edition's resolved commit map, so every edition shipping a subject still
3155
- * derives the same `contentHash` for it.
3156
- */
3157
- export function resolveCrossRepoVendoredInputs({
3158
- indexInputs,
3159
- consumerRepo,
3160
- consumerRepoDir,
3161
- consumerCommit,
3162
- }) {
3163
- const declared = indexInputs.filter(
3164
- (input) => input.consumerRepo === consumerRepo,
3165
- );
3166
- if (declared.length === 0) {return [];}
3167
- if (!consumerCommit) {
3168
- throw new Error(
3169
- `cross-repo build inputs are declared for "${consumerRepo}", but this ` +
3170
- `distribution resolves no commit for it — the vendored content cannot ` +
3171
- `be pinned, so the image cannot be addressed`,
3172
- );
3173
- }
3174
- let record;
3175
- try {
3176
- record = readGitJsonAtCommit(
3177
- consumerRepoDir,
3178
- CROSS_REPO_BUILD_INPUTS_PATH,
3179
- consumerCommit,
3180
- );
3181
- } catch (cause) {
3182
- throw new Error(
3183
- `"${consumerRepo}" declares cross-repo build inputs but ` +
3184
- `${CROSS_REPO_BUILD_INPUTS_PATH} is unreadable at ${consumerCommit}. ` +
3185
- `That file is what its CI feeds to compute-image-tag.mjs; without it ` +
3186
- `no coordinate this tooling derives can match the tag CI pushes.`,
3187
- { cause },
3188
- );
3189
- }
3190
- const mirrored = new Map();
3191
- for (const input of record?.inputs ?? []) {
3192
- mirrored.set(crossRepoInputKey(input), input.sourceCommit);
3193
- }
3194
- // Set equality in BOTH directions. A commit that LAGS is normal and is now
3195
- // honoured; a differing input SET is a genuine declaration disagreement, and
3196
- // it moves the hash just as surely — so it must fail rather than resolve to a
3197
- // coordinate built from a different closure.
3198
- const declaredKeys = new Set(declared.map((input) => crossRepoInputKey(input)));
3199
- const resolved = declared.map((input) => {
3200
- const sourceCommit = mirrored.get(crossRepoInputKey(input));
3201
- if (!sourceCommit) {
3202
- throw new Error(
3203
- `the runtime image source index declares a cross-repo build input for ` +
3204
- `"${consumerRepo}" vendoring "${input.sourceRepo}:${input.sourcePath}" ` +
3205
- `to "${input.destinationPath}", but ${CROSS_REPO_BUILD_INPUTS_PATH} at ` +
3206
- `${consumerCommit} declares no such input — its CI does not vendor it, ` +
3207
- `so the coordinate this tooling would derive is one nothing builds`,
3208
- );
3209
- }
3210
- return {
3211
- consumerService: input.consumerService ?? null,
3212
- destinationPath: input.destinationPath,
3213
- sourceRepoDir: sourcePath(input.sourceRepo),
3214
- sourcePath: input.sourcePath,
3215
- sourceCommit,
3216
- };
3217
- });
3218
- for (const key of mirrored.keys()) {
3219
- if (declaredKeys.has(key)) {continue;}
3220
- const [sourceRepo, sourcePath, destinationPath] = key.split('\0');
3221
- throw new Error(
3222
- `${CROSS_REPO_BUILD_INPUTS_PATH} at ${consumerCommit} vendors ` +
3223
- `"${sourceRepo}:${sourcePath}" to "${destinationPath}" for ` +
3224
- `"${consumerRepo}", which the runtime image source index does not ` +
3225
- `declare — its CI bakes content this tooling does not hash`,
3226
- );
3227
- }
3228
- return resolved;
3229
- }