@xemahq/xema 0.41.0 → 0.42.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.
@@ -40,7 +40,10 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
40
40
  import { assertBuildLedgerComplete } from './build-ledger-completeness.mjs';
41
41
  import { BUILD_LEDGER_FILE, readBuildLedger } from './build-ledger.mjs';
42
42
  import { CROSS_REPO_BUILD_INPUTS_PATH, crossRepoInputKey, resolveCrossRepoVendoredInputs } from './cross-repo-build-inputs.mjs';
43
- import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
43
+ import {
44
+ resolveDispatchedRuntimeComponents,
45
+ UndeclaredSupplementalRuntimeComponentError,
46
+ } from './dispatched-runtime-components.mjs';
44
47
  import {
45
48
  assertCanonicalDistributionId,
46
49
  distributionOutputPath,
@@ -58,6 +61,7 @@ import {
58
61
  compareStrings,
59
62
  hashImageBuildInputs,
60
63
  imageInputSourceHashAlgorithm,
64
+ imageLockWriteSchemaVersion,
61
65
  loadKernelDistributionSchemas,
62
66
  parseResolvedDistributionLock,
63
67
  readGitJsonAtCommit,
@@ -681,6 +685,28 @@ export function resolveImageStructure(
681
685
  dispatchers,
682
686
  biomeIndex,
683
687
  )) {
688
+ if (dispatched.supplementalRuntimeComponentId !== undefined) {
689
+ // THE REFUSAL: a dispatched supplemental runtime component must be one the
690
+ // runtime image source index declares as a runtime-component subject.
691
+ // Every image lock is produced here, so this is the one place it is checked.
692
+ const declared = runtimeSourceIndex.supplementalSubjects.find(
693
+ (subject) =>
694
+ subject.kind === enums.RuntimeImageSubjectKind.RuntimeComponent &&
695
+ subject.id === dispatched.supplementalRuntimeComponentId,
696
+ );
697
+ if (declared === undefined) {
698
+ throw new UndeclaredSupplementalRuntimeComponentError(
699
+ dispatched.supplementalRuntimeComponentId,
700
+ dispatched.consumers,
701
+ `distribution "${lock.distributionId}"`,
702
+ );
703
+ }
704
+ addSupplementalSubject(
705
+ declared,
706
+ dispatched.consumers.map((biomeId) => ({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId })),
707
+ );
708
+ continue;
709
+ }
684
710
  const { target } = dispatched;
685
711
  addSubject(subjects, {
686
712
  kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
@@ -717,6 +743,16 @@ export function resolveImageStructure(
717
743
  const boundSubjects = new Set(
718
744
  runtimeSourceIndex.platformServiceBindings.map((binding) => bindingKey(binding)),
719
745
  );
746
+ // A biome's `supplemental:<id>` dispatch edge also carries the subject: the
747
+ // catalogue check reads the whole first-party index, not this roster, so a
748
+ // subject reached only by a dispatch is not "unreachable" in any edition.
749
+ const dispatchedSubjects = new Set(
750
+ biomeIndex
751
+ .filter((entry) => entry.source === undefined)
752
+ .flatMap((entry) => entry.dispatchesRuntimeComponents ?? [])
753
+ .filter((edge) => edge.supplementalRuntimeComponentId !== undefined)
754
+ .map((edge) => `${enums.RuntimeImageSubjectKind.RuntimeComponent}\0${edge.supplementalRuntimeComponentId}`),
755
+ );
720
756
  const launchers = new Map(
721
757
  supplementalSources
722
758
  .filter((source) => source.launchedBy !== undefined)
@@ -733,11 +769,11 @@ export function resolveImageStructure(
733
769
  for (const [key, supplemental] of supplementalSubjects) {
734
770
  const launcher = launchers.get(key);
735
771
  if (launcher === undefined) {
736
- if (!boundSubjects.has(key)) {
772
+ if (!boundSubjects.has(key) && !dispatchedSubjects.has(key)) {
737
773
  throw new Error(
738
- `supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy nor a ` +
739
- 'platform-service binding, so no edition could ever carry it — declare what launches it ' +
740
- '(runtimeImageSources.supplementalSources[].launchedBy) or bind it to a platform service',
774
+ `supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy, a ` +
775
+ 'platform-service binding nor a biome dispatching it, so no edition could ever carry it — declare what launches it ' +
776
+ '(runtimeImageSources.supplementalSources[].launchedBy), bind it to a platform service or dispatch it from a biome',
741
777
  );
742
778
  }
743
779
  continue;
@@ -935,6 +971,13 @@ export function editionSourceIds(lock) {
935
971
  * caller can tell "this image's inputs are unknown" from any other refusal:
936
972
  * the carried set is evidence, and with none there is no owner to derive.
937
973
  */
974
+ export class ComposedProcessWorkerError extends Error {
975
+ constructor(message) {
976
+ super(message);
977
+ this.name = 'ComposedProcessWorkerError';
978
+ }
979
+ }
980
+
938
981
  export class ComposedProcessInputsError extends Error {
939
982
  constructor(message) {
940
983
  super(message);
@@ -974,16 +1017,31 @@ function composedProcessCarriage(structure, enums, repo, sourceCommit, locate) {
974
1017
  const byBiome = new Map();
975
1018
  for (const subject of structure.subjects) {
976
1019
  if (subject.kind !== enums.RuntimeImageSubjectKind.Service || subject.repo !== repo) {continue;}
977
- const servicePath = subjectServicePath(subject, sourceCommit, locate);
1020
+ const { path: servicePath, componentKind } = subjectServiceSource(subject, sourceCommit, locate);
978
1021
  for (const owner of subject.owners) {
979
1022
  if (owner.kind !== enums.RuntimeImageOwnerKind.Biome) {continue;}
980
- const row = byBiome.get(owner.biomeId) ?? { biomeId: owner.biomeId, carried: [], uncarried: [] };
981
- (carried.has(servicePath) ? row.carried : row.uncarried).push(subject.id);
1023
+ const row = byBiome.get(owner.biomeId) ??
1024
+ { biomeId: owner.biomeId, carried: [], uncarried: [], carriedWorkers: [], packageDirs: {} };
1025
+ if (!carried.has(servicePath)) {
1026
+ row.uncarried.push(subject.id);
1027
+ } else if (componentKind === 'worker') {
1028
+ // A worker is never a member: it runs as its own Deployment.
1029
+ row.carriedWorkers.push(subject.id);
1030
+ } else {
1031
+ row.carried.push(subject.id);
1032
+ // The member's package directory IS the composed build's own input entry.
1033
+ row.packageDirs[subject.id] = servicePath;
1034
+ }
982
1035
  byBiome.set(owner.biomeId, row);
983
1036
  }
984
1037
  }
985
1038
  return [...byBiome.values()]
986
- .map((row) => ({ ...row, carried: row.carried.sort(compareStrings), uncarried: row.uncarried.sort(compareStrings) }))
1039
+ .map((row) => ({
1040
+ ...row,
1041
+ carried: row.carried.sort(compareStrings),
1042
+ uncarried: row.uncarried.sort(compareStrings),
1043
+ carriedWorkers: row.carriedWorkers.sort(compareStrings),
1044
+ }))
987
1045
  .sort((a, b) => compareStrings(a.biomeId, b.biomeId));
988
1046
  }
989
1047
 
@@ -1052,8 +1110,15 @@ export function readComposedProcessDeclarations(
1052
1110
  * resolve derive the same subject from the same declarations.
1053
1111
  *
1054
1112
  * Owners are the biomes the image CARRIES, derived from the composed build's
1055
- * inputs at the sealed commit (`declaration.carriage`): the biomes all of whose
1056
- * services are members, never every biome with a service in the repository.
1113
+ * inputs at the sealed commit (`declaration.carriage`): every biome at least one
1114
+ * of whose services is a member, never every biome with a service in the
1115
+ * repository. The members leave the structure and are sealed on the subject
1116
+ * as `members[{ service, packageDir }]` — the per-service membership truth: a
1117
+ * service runs from the composed digest if and only if it is a row (owners
1118
+ * never resolve a service). The edition carries the composed subject plus the
1119
+ * services that are no row (the specials, which keep their own subjects even
1120
+ * inside an owner biome), and nothing addresses a member's own image. A worker
1121
+ * is never a row; an own-image worker the composed build carries is refused.
1057
1122
  */
1058
1123
  export function withComposedProcessSubjects(
1059
1124
  structure,
@@ -1064,6 +1129,9 @@ export function withComposedProcessSubjects(
1064
1129
  ) {
1065
1130
  if (declarations.length === 0) {return structure;}
1066
1131
  const subjects = new Map(structure.subjects.map((subject) => [subjectKey(subject), subject]));
1132
+ // A biome's members run from ONE composed image: a biome owning two composed
1133
+ // subjects would leave its pods two images to choose between.
1134
+ const composedOwnerOf = new Map();
1067
1135
  for (const declaration of declarations) {
1068
1136
  const services = structure.subjects.filter(
1069
1137
  (subject) =>
@@ -1075,21 +1143,47 @@ export function withComposedProcessSubjects(
1075
1143
  'biomes the image carries — refusing to guess its owners',
1076
1144
  );
1077
1145
  }
1078
- // Owners are the biomes ALL of whose services the image carries. A biome
1079
- // with only some carried deploys dedicated, so it is no owner (and is
1080
- // named); a biome with none carried is simply not one.
1146
+ // Owners are the biomes ANY of whose services the image carries. A biome
1147
+ // with only some carried is an owner too; its services the image does not
1148
+ // carry are specials and keep their own subjects (named here). A biome with
1149
+ // none carried is simply not one.
1081
1150
  const biomeOwners = [];
1151
+ const members = new Set();
1152
+ const packageDirOf = new Map();
1082
1153
  for (const row of declaration.carriage) {
1154
+ // A worker is never a member. One the composed build carries must run
1155
+ // from a carried sibling's image (`workerImages`, declared in the
1156
+ // manifest), never be dropped into the composed process or fall back.
1157
+ for (const worker of row.carriedWorkers ?? []) {
1158
+ throw new ComposedProcessWorkerError(
1159
+ row.carried.length === 0
1160
+ ? `composed-process "${declaration.repo}" carries worker "${worker}" of biome "${row.biomeId}", ` +
1161
+ 'and that biome has no carried service whose image it could run — a worker is never a ' +
1162
+ 'composed member; carry a service of its biome or take the worker out of the composed inputs'
1163
+ : `composed-process "${declaration.repo}" carries worker "${worker}" of biome "${row.biomeId}" as ` +
1164
+ 'its own oci-image — a worker is never a composed member; declare it `artifact.kind: ' +
1165
+ `shared-oci-image\` of a carried service (${row.carried.join(', ')}) or take it out of the composed inputs`,
1166
+ );
1167
+ }
1083
1168
  if (row.carried.length === 0) {continue;}
1084
1169
  if (row.uncarried.length > 0) {
1085
1170
  onNotice(
1086
- `composed-process "${declaration.repo}": biome "${row.biomeId}" is only partly carried ` +
1171
+ `composed-process "${declaration.repo}": biome "${row.biomeId}" is partly carried ` +
1087
1172
  `(carried: ${row.carried.join(', ')}; not carried: ${row.uncarried.join(', ')}) — ` +
1088
- 'it is not an owner and deploys dedicated',
1173
+ 'its uncarried services keep their own subjects',
1174
+ );
1175
+ }
1176
+ const other = composedOwnerOf.get(row.biomeId);
1177
+ if (other !== undefined) {
1178
+ throw new Error(
1179
+ `biome "${row.biomeId}" is carried by the composed-process images of both "${other}" and ` +
1180
+ `"${declaration.repo}" — a biome is owned by exactly one composed subject`,
1089
1181
  );
1090
- continue;
1091
1182
  }
1183
+ composedOwnerOf.set(row.biomeId, declaration.repo);
1092
1184
  biomeOwners.push({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId: row.biomeId });
1185
+ for (const id of row.carried) {members.add(id);}
1186
+ for (const [id, packageDir] of Object.entries(row.packageDirs ?? {})) {packageDirOf.set(id, packageDir);}
1093
1187
  }
1094
1188
  if (biomeOwners.length === 0) {
1095
1189
  throw new Error(
@@ -1127,6 +1221,43 @@ export function withComposedProcessSubjects(
1127
1221
  `(${[...namespaces].join(', ')}) — a composed image of one build has one`,
1128
1222
  );
1129
1223
  }
1224
+ // A MEMBER reaches its pods through this one subject (every member runs as a
1225
+ // group-of-one of the composed image), so it carries no subject of its own:
1226
+ // no `<imageRef>:<contentHash>` of its own is probed, verified or sealed. A
1227
+ // member is a service of this build the composed build's inputs carry
1228
+ // (`declaration.carriage`), every owner of which is a biome owning the
1229
+ // composed image. Any other service (not carried, or owned by anything but
1230
+ // a biome, e.g. shared with a platform service) keeps its own.
1231
+ const composedOwners = new Set(biomeOwners.map((owner) => owner.biomeId));
1232
+ const memberRows = [];
1233
+ for (const subject of services) {
1234
+ if (
1235
+ members.has(subject.id) &&
1236
+ subject.owners.length > 0 &&
1237
+ subject.owners.every(
1238
+ (owner) => owner.kind === enums.RuntimeImageOwnerKind.Biome && composedOwners.has(owner.biomeId),
1239
+ )
1240
+ ) {
1241
+ subjects.delete(subjectKey(subject));
1242
+ const packageDir = packageDirOf.get(subject.id);
1243
+ if (packageDir === undefined) {
1244
+ throw new ComposedProcessInputsError(
1245
+ `composed-process "${declaration.repo}": member "${subject.id}" has no package directory in ` +
1246
+ 'the composed build inputs — its shared-image workers would have nowhere to run from',
1247
+ );
1248
+ }
1249
+ memberRows.push({ service: subject.id, packageDir });
1250
+ }
1251
+ }
1252
+ if (memberRows.length === 0) {
1253
+ throw new ComposedProcessInputsError(
1254
+ `composed-process "${declaration.repo}" has no member: every service its build carries ` +
1255
+ `(${[...members].sort(compareStrings).join(', ')}) is also owned by something other than a biome ` +
1256
+ 'it owns (e.g. a platform service), so each keeps its own subject — a composed image with no ' +
1257
+ 'member runs nothing; take it out of the edition or carry a biome-owned service',
1258
+ );
1259
+ }
1260
+ memberRows.sort((a, b) => compareStrings(a.service, b.service));
1130
1261
  const id = composedProcessSubjectId(declaration.sourceId);
1131
1262
  addSubject(subjects, {
1132
1263
  kind: enums.RuntimeImageSubjectKind.ComposedProcess,
@@ -1139,6 +1270,7 @@ export function withComposedProcessSubjects(
1139
1270
  sourceManifestPath: declaration.sourceManifestPath,
1140
1271
  requiredPlatforms,
1141
1272
  owners: biomeOwners,
1273
+ members: memberRows,
1142
1274
  });
1143
1275
  }
1144
1276
  return {
@@ -1240,11 +1372,21 @@ function sourceCommitMap(structure, locate = FIRST_PARTY_TREES) {
1240
1372
  * service directory to hash the SAME build-input closure the lock names.
1241
1373
  */
1242
1374
  export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_TREES) {
1375
+ return subjectServiceSource(subject, sourceCommit, locate).path;
1376
+ }
1377
+
1378
+ /**
1379
+ * The repository-relative source directory of a subject and the kind of the
1380
+ * manifest component it is built from (`worker` for a worker component,
1381
+ * `service` otherwise — a legacy `ships.apis[]` entry or a subject with its own
1382
+ * source manifest is not a worker).
1383
+ */
1384
+ function subjectServiceSource(subject, sourceCommit, locate = FIRST_PARTY_TREES) {
1243
1385
  const { dir: repoDirectory, base } = locate(subject.repo);
1244
1386
  const toRepoRelative = (absolute) =>
1245
1387
  relative(repoDirectory, absolute).split(sep).join('/') || '.';
1246
1388
  if (subject.sourceManifestPath) {
1247
- return toRepoRelative(join(base, subject.path));
1389
+ return { path: toRepoRelative(join(base, subject.path)), componentKind: 'service' };
1248
1390
  }
1249
1391
  const biomePath = toRepoRelative(join(base, subject.path));
1250
1392
  const manifestPath =
@@ -1282,6 +1424,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1282
1424
  );
1283
1425
  }
1284
1426
  let servicePath;
1427
+ let componentKind = 'service';
1285
1428
  if (v4ServiceComponents.length > 0) {
1286
1429
  // A SERVICE is identified by `protocol.serviceName`; a WORKER has no
1287
1430
  // service protocol and is identified by its component `key`. Matching only
@@ -1305,6 +1448,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1305
1448
  );
1306
1449
  }
1307
1450
  servicePath = component.artifact.path;
1451
+ componentKind = component.kind;
1308
1452
  } else {
1309
1453
  if (legacyApis.length === 0) {
1310
1454
  throw new Error(
@@ -1322,7 +1466,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1322
1466
  }
1323
1467
  servicePath = api.path;
1324
1468
  }
1325
- return toRepoRelative(join(base, subject.path, servicePath));
1469
+ return { path: toRepoRelative(join(base, subject.path, servicePath)), componentKind };
1326
1470
  }
1327
1471
 
1328
1472
  function assertSourceManifestParity(structure, commitByRepo, locate = FIRST_PARTY_TREES) {
@@ -1780,7 +1924,7 @@ export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {})
1780
1924
  const schemas = await loadKernelDistributionSchemas();
1781
1925
  const imageLock = validateImmutableImageLock(
1782
1926
  schemas.parseImageLock({
1783
- schemaVersion: 3,
1927
+ schemaVersion: imageLockWriteSchemaVersion(schemas, subjects, `distribution "${id}"`),
1784
1928
  sourceHashAlgorithm: schemas.ImageSourceHashAlgorithm.ImageInputV2,
1785
1929
  distributionId: id,
1786
1930
  contractAuthority,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -49,12 +49,12 @@
49
49
  "@nestjs/common": "^11.1.18",
50
50
  "@nestjs/core": "^11.1.18",
51
51
  "@nestjs/platform-express": "^11.1.28",
52
- "@xemahq/biome-sdk": "^18.1.0",
52
+ "@xemahq/biome-sdk": "^22.0.0",
53
53
  "@xemahq/biome-supply-chain": "^0.5.0",
54
54
  "@xemahq/distribution-source-hash": "^0.2.2",
55
55
  "@xemahq/dsl": "^0.24.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.83.0",
57
+ "@xemahq/contracts": "^0.94.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:322929ee2999d155d5d612f17bb786adef52f44c46cee82b9bc9b11ecc792758"
95
+ "sourceDigest": "sha256:ae545509930a5b6f1215c413f3e7121b15a29d5fd3e14ab373f0910ceaa9063d"
96
96
  }
97
97
  }