@xemahq/xema 0.41.1 → 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.
@@ -5,7 +5,9 @@
5
5
  * `operationRequirements`); the kernel's `admitOperationGrants` admits or
6
6
  * refuses every requirement, and a refusal is never a warning. The resolve
7
7
  * and the seal both call {@link assertOperationGrantsAdmitted}, so a lock with
8
- * a refused requirement is neither written nor sealed.
8
+ * a refused requirement is neither written nor sealed. The installed-operation
9
+ * ceiling (`requestsInstalledOperations`, carried as `installedOperationRequests`)
10
+ * and the edition's store-workload ceiling are admitted the same way.
9
11
  */
10
12
 
11
13
  /** Thrown when one or more declared operation requirements are not admitted. */
@@ -83,3 +85,65 @@ export function assertOrganizationSweepsAdmitted(schemas, lock) {
83
85
  }
84
86
  return grants;
85
87
  }
88
+
89
+ /** Thrown when one or more requested installed operations are not admitted into the ceiling. */
90
+ export class InstalledOperationCeilingAdmissionError extends Error {
91
+ /**
92
+ * @param {string} distributionId
93
+ * @param {ReadonlyArray<{code: string, biome: string, component: string, operation: string, message: string}>} refusals
94
+ */
95
+ constructor(distributionId, refusals) {
96
+ super(
97
+ `distribution "${distributionId}" declares installed operation requests that are not admitted:\n` +
98
+ refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
99
+ );
100
+ this.name = 'InstalledOperationCeilingAdmissionError';
101
+ this.distributionId = distributionId;
102
+ this.refusals = refusals;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Admit every installed-operation request of `lock` through the kernel
108
+ * (`admitInstalledOperationCeiling`), or throw
109
+ * {@link InstalledOperationCeilingAdmissionError} listing every refusal.
110
+ * Returns the admitted ceiling rows.
111
+ */
112
+ export function assertInstalledOperationCeilingAdmitted(schemas, lock) {
113
+ const { ceilings, refusals } = schemas.admitInstalledOperationCeiling(lock);
114
+ if (refusals.length > 0) {
115
+ throw new InstalledOperationCeilingAdmissionError(lock.distributionId, refusals);
116
+ }
117
+ return ceilings;
118
+ }
119
+
120
+ /** Thrown when one or more domains of the edition's store-workload ceiling are not admitted. */
121
+ export class StoreWorkloadCeilingAdmissionError extends Error {
122
+ /**
123
+ * @param {string} distributionId
124
+ * @param {ReadonlyArray<{code: string, domain: string, message: string}>} refusals
125
+ */
126
+ constructor(distributionId, refusals) {
127
+ super(
128
+ `distribution "${distributionId}" declares a store-workload ceiling that is not admitted:\n` +
129
+ refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
130
+ );
131
+ this.name = 'StoreWorkloadCeilingAdmissionError';
132
+ this.distributionId = distributionId;
133
+ this.refusals = refusals;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Admit the store-workload ceiling of `lock` through the kernel
139
+ * (`admitStoreWorkloadCeiling`), or throw
140
+ * {@link StoreWorkloadCeilingAdmissionError} listing every refusal. Returns the
141
+ * resolved ceiling.
142
+ */
143
+ export function assertStoreWorkloadCeilingAdmitted(schemas, lock) {
144
+ const { ceiling, refusals } = schemas.admitStoreWorkloadCeiling(lock);
145
+ if (refusals.length > 0) {
146
+ throw new StoreWorkloadCeilingAdmissionError(lock.distributionId, refusals);
147
+ }
148
+ return ceiling;
149
+ }
@@ -81,6 +81,18 @@ export function componentOptionalOperationRequirements(component, where) {
81
81
  return declaredPrivilegeList(component, where, 'optionalOperations', OperationIdSchema, 'operation id');
82
82
  }
83
83
 
84
+ /**
85
+ * The operations one workload component asks to exercise for the organization
86
+ * that installs its biome (`requestsInstalledOperations`), graded by the same
87
+ * operation-id grammar. Declared INTENT only; the lock admits the ceiling of
88
+ * the request (`admitInstalledOperationCeiling`), and an installed operation is
89
+ * never a deployment grant.
90
+ */
91
+ export function componentInstalledOperationRequests(component, where) {
92
+ const { OperationIdSchema } = loadKernelPrivilegeGrammars();
93
+ return declaredPrivilegeList(component, where, 'requestsInstalledOperations', OperationIdSchema, 'operation id');
94
+ }
95
+
84
96
  /**
85
97
  * The organization sweeps one workload component declares it may hold
86
98
  * (`requiresOrganizationSweeps`), each a member of the kernel's closed
@@ -96,7 +108,12 @@ export function componentOrganizationSweepRequirements(component, where) {
96
108
  * The privilege declarations a component may carry only if it is a workload
97
109
  * principal in the lock.
98
110
  */
99
- export const PRINCIPAL_PRIVILEGE_FIELDS = Object.freeze(['requiresOperations', 'optionalOperations', 'requiresOrganizationSweeps']);
111
+ export const PRINCIPAL_PRIVILEGE_FIELDS = Object.freeze([
112
+ 'requiresOperations',
113
+ 'optionalOperations',
114
+ 'requestsInstalledOperations',
115
+ 'requiresOrganizationSweeps',
116
+ ]);
100
117
 
101
118
  /**
102
119
  * Thrown when a component that has no workload principal in the lock declares
@@ -37,7 +37,7 @@ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
37
37
  import { EditionLockFile } from './edition-constants.mjs';
38
38
  import { editionsRoot } from './edition-workspace.mjs';
39
39
  import { adoptCatalogueKernel } from './kernel-package.mjs';
40
- import { compareStrings, serializeJson, validateImmutableImageLock } from './lib.mjs';
40
+ import { compareStrings, imageLockWriteSchemaVersion, serializeJson, validateImmutableImageLock } from './lib.mjs';
41
41
  import { pinnedSource, snapshotPinSource } from './pin-source.mjs';
42
42
  import { composeAuthorizationBase, composeManifest, resolveDistributionWithIndex, UnknownSelectionError } from './resolve-distribution.mjs';
43
43
  import { resolveImageRows } from './resolve-images.mjs';
@@ -238,13 +238,14 @@ export async function resolveCustomerImages({ id, catalogue, lock, composed, sch
238
238
  const seen = new Set(vendorSubjects.map(subjectKey));
239
239
  const clash = own.subjects.find((subject) => seen.has(subjectKey(subject)));
240
240
  if (clash !== undefined) {throw new Error(`the customer's own image ${clash.kind}:${clash.id} collides with a catalogue image of the same identity`);}
241
+ const mergedSubjects = [...vendorSubjects, ...own.subjects].sort(compareSubjects);
241
242
  const merged = validateImmutableImageLock(
242
243
  schemas.parseImageLock({
243
- schemaVersion: 3,
244
+ schemaVersion: imageLockWriteSchemaVersion(schemas, mergedSubjects, `distribution "${id}"`),
244
245
  sourceHashAlgorithm: catalogue.images.sourceHashAlgorithm,
245
246
  distributionId: id,
246
247
  contractAuthority: catalogue.contractAuthority,
247
- subjects: [...vendorSubjects, ...own.subjects].sort(compareSubjects),
248
+ subjects: mergedSubjects,
248
249
  platformServiceBindings: [...vendor.platformServiceBindings, ...own.platformServiceBindings].sort((a, b) => compareStrings(a.platformService, b.platformService)),
249
250
  webBiomes: [...vendor.webBiomes, ...own.webBiomes].sort((a, b) => compareStrings(a.biomeId, b.biomeId)),
250
251
  }),
@@ -95,8 +95,10 @@ import {
95
95
  validateServiceDependencyOwnerParity,
96
96
  } from './lib.mjs';
97
97
  import {
98
+ assertInstalledOperationCeilingAdmitted,
98
99
  assertOperationGrantsAdmitted,
99
100
  assertOrganizationSweepsAdmitted,
101
+ assertStoreWorkloadCeilingAdmitted,
100
102
  } from './operation-grant-admission.mjs';
101
103
  import { PUBLISHED_PIN_SOURCE, pinnedSource } from './pin-source.mjs';
102
104
  import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
@@ -307,6 +309,7 @@ export function composeManifest(manifest, pinSource = PUBLISHED_PIN_SOURCE) {
307
309
  // Visibility is a RESTRICTION; the caller (which holds the kernel enum)
308
310
  // reduces the declarations.
309
311
  declaredVisibilities: manifest.visibility === undefined ? [] : [manifest.visibility],
312
+ storeWorkloadCeiling: manifest.storeWorkloadCeiling,
310
313
  declaredSourceNames: new Set(sources.map((source) => source.name)),
311
314
  declaredSources: sources.map((source) => pinnedSource(source, pinSource)),
312
315
  owner: manifest.owner,
@@ -1159,6 +1162,7 @@ export function resolveDistributionFromInputs(
1159
1162
  trustPolicy,
1160
1163
  trustCeiling,
1161
1164
  declaredVisibilities,
1165
+ storeWorkloadCeiling,
1162
1166
  owner,
1163
1167
  leaf,
1164
1168
  // Absent on a selector-model composition — including every `composed` a
@@ -2047,6 +2051,13 @@ export function resolveDistributionFromInputs(
2047
2051
  Object.keys(e.optionalOperationRequirements).length > 0
2048
2052
  ? { optionalOperationRequirements: e.optionalOperationRequirements }
2049
2053
  : {}),
2054
+ // The operations each workload component asks to exercise for the
2055
+ // installing organization, forwarded verbatim; the lock admits their
2056
+ // ceiling (`admitInstalledOperationCeiling`).
2057
+ ...(e.installedOperationRequests !== undefined &&
2058
+ Object.keys(e.installedOperationRequests).length > 0
2059
+ ? { installedOperationRequests: e.installedOperationRequests }
2060
+ : {}),
2050
2061
  // The organization sweeps each workload component may hold, forwarded
2051
2062
  // verbatim; the seal admits them (`admitOrganizationSweeps`).
2052
2063
  ...(e.organizationSweepRequirements !== undefined &&
@@ -2202,6 +2213,9 @@ export function resolveDistributionFromInputs(
2202
2213
  // last for the same reason: an edition without it carries exactly the bytes it
2203
2214
  // always did. Not in `inputHash`; the kernel lock schema validates it.
2204
2215
  if (composed?.storePublisherTrust !== undefined) {lock.storePublisherTrust = composed.storePublisherTrust;}
2216
+ // The edition's store-workload ceiling — written ONLY when the edition
2217
+ // declares one (the kernel refuses an empty list), carried unchanged.
2218
+ if (storeWorkloadCeiling !== undefined) {lock.storeWorkloadCeiling = storeWorkloadCeiling;}
2205
2219
 
2206
2220
  // (7) Re-derive the ownership index through the KERNEL so the table this
2207
2221
  // resolver emits is validated by the exact code every consumer will read
@@ -2221,6 +2235,8 @@ export function resolveDistributionFromInputs(
2221
2235
  // never fails (the resolve and `edition check` both run through here).
2222
2236
  assertOperationGrantsAdmitted(schemas, parsed, { report: (line) => console.log(`[operations] ${line}`) });
2223
2237
  assertOrganizationSweepsAdmitted(schemas, parsed);
2238
+ assertInstalledOperationCeilingAdmitted(schemas, parsed);
2239
+ assertStoreWorkloadCeilingAdmitted(schemas, parsed);
2224
2240
  return validateImmutableDistributionLock(lock);
2225
2241
  }
2226
2242
 
@@ -61,6 +61,7 @@ import {
61
61
  compareStrings,
62
62
  hashImageBuildInputs,
63
63
  imageInputSourceHashAlgorithm,
64
+ imageLockWriteSchemaVersion,
64
65
  loadKernelDistributionSchemas,
65
66
  parseResolvedDistributionLock,
66
67
  readGitJsonAtCommit,
@@ -970,6 +971,13 @@ export function editionSourceIds(lock) {
970
971
  * caller can tell "this image's inputs are unknown" from any other refusal:
971
972
  * the carried set is evidence, and with none there is no owner to derive.
972
973
  */
974
+ export class ComposedProcessWorkerError extends Error {
975
+ constructor(message) {
976
+ super(message);
977
+ this.name = 'ComposedProcessWorkerError';
978
+ }
979
+ }
980
+
973
981
  export class ComposedProcessInputsError extends Error {
974
982
  constructor(message) {
975
983
  super(message);
@@ -1009,16 +1017,31 @@ function composedProcessCarriage(structure, enums, repo, sourceCommit, locate) {
1009
1017
  const byBiome = new Map();
1010
1018
  for (const subject of structure.subjects) {
1011
1019
  if (subject.kind !== enums.RuntimeImageSubjectKind.Service || subject.repo !== repo) {continue;}
1012
- const servicePath = subjectServicePath(subject, sourceCommit, locate);
1020
+ const { path: servicePath, componentKind } = subjectServiceSource(subject, sourceCommit, locate);
1013
1021
  for (const owner of subject.owners) {
1014
1022
  if (owner.kind !== enums.RuntimeImageOwnerKind.Biome) {continue;}
1015
- const row = byBiome.get(owner.biomeId) ?? { biomeId: owner.biomeId, carried: [], uncarried: [] };
1016
- (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
+ }
1017
1035
  byBiome.set(owner.biomeId, row);
1018
1036
  }
1019
1037
  }
1020
1038
  return [...byBiome.values()]
1021
- .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
+ }))
1022
1045
  .sort((a, b) => compareStrings(a.biomeId, b.biomeId));
1023
1046
  }
1024
1047
 
@@ -1087,8 +1110,15 @@ export function readComposedProcessDeclarations(
1087
1110
  * resolve derive the same subject from the same declarations.
1088
1111
  *
1089
1112
  * Owners are the biomes the image CARRIES, derived from the composed build's
1090
- * inputs at the sealed commit (`declaration.carriage`): the biomes all of whose
1091
- * 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.
1092
1122
  */
1093
1123
  export function withComposedProcessSubjects(
1094
1124
  structure,
@@ -1099,6 +1129,9 @@ export function withComposedProcessSubjects(
1099
1129
  ) {
1100
1130
  if (declarations.length === 0) {return structure;}
1101
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();
1102
1135
  for (const declaration of declarations) {
1103
1136
  const services = structure.subjects.filter(
1104
1137
  (subject) =>
@@ -1110,21 +1143,47 @@ export function withComposedProcessSubjects(
1110
1143
  'biomes the image carries — refusing to guess its owners',
1111
1144
  );
1112
1145
  }
1113
- // Owners are the biomes ALL of whose services the image carries. A biome
1114
- // with only some carried deploys dedicated, so it is no owner (and is
1115
- // 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.
1116
1150
  const biomeOwners = [];
1151
+ const members = new Set();
1152
+ const packageDirOf = new Map();
1117
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
+ }
1118
1168
  if (row.carried.length === 0) {continue;}
1119
1169
  if (row.uncarried.length > 0) {
1120
1170
  onNotice(
1121
- `composed-process "${declaration.repo}": biome "${row.biomeId}" is only partly carried ` +
1171
+ `composed-process "${declaration.repo}": biome "${row.biomeId}" is partly carried ` +
1122
1172
  `(carried: ${row.carried.join(', ')}; not carried: ${row.uncarried.join(', ')}) — ` +
1123
- 'it is not an owner and deploys dedicated',
1173
+ 'its uncarried services keep their own subjects',
1124
1174
  );
1125
- continue;
1126
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`,
1181
+ );
1182
+ }
1183
+ composedOwnerOf.set(row.biomeId, declaration.repo);
1127
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);}
1128
1187
  }
1129
1188
  if (biomeOwners.length === 0) {
1130
1189
  throw new Error(
@@ -1162,6 +1221,43 @@ export function withComposedProcessSubjects(
1162
1221
  `(${[...namespaces].join(', ')}) — a composed image of one build has one`,
1163
1222
  );
1164
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));
1165
1261
  const id = composedProcessSubjectId(declaration.sourceId);
1166
1262
  addSubject(subjects, {
1167
1263
  kind: enums.RuntimeImageSubjectKind.ComposedProcess,
@@ -1174,6 +1270,7 @@ export function withComposedProcessSubjects(
1174
1270
  sourceManifestPath: declaration.sourceManifestPath,
1175
1271
  requiredPlatforms,
1176
1272
  owners: biomeOwners,
1273
+ members: memberRows,
1177
1274
  });
1178
1275
  }
1179
1276
  return {
@@ -1275,11 +1372,21 @@ function sourceCommitMap(structure, locate = FIRST_PARTY_TREES) {
1275
1372
  * service directory to hash the SAME build-input closure the lock names.
1276
1373
  */
1277
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) {
1278
1385
  const { dir: repoDirectory, base } = locate(subject.repo);
1279
1386
  const toRepoRelative = (absolute) =>
1280
1387
  relative(repoDirectory, absolute).split(sep).join('/') || '.';
1281
1388
  if (subject.sourceManifestPath) {
1282
- return toRepoRelative(join(base, subject.path));
1389
+ return { path: toRepoRelative(join(base, subject.path)), componentKind: 'service' };
1283
1390
  }
1284
1391
  const biomePath = toRepoRelative(join(base, subject.path));
1285
1392
  const manifestPath =
@@ -1317,6 +1424,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1317
1424
  );
1318
1425
  }
1319
1426
  let servicePath;
1427
+ let componentKind = 'service';
1320
1428
  if (v4ServiceComponents.length > 0) {
1321
1429
  // A SERVICE is identified by `protocol.serviceName`; a WORKER has no
1322
1430
  // service protocol and is identified by its component `key`. Matching only
@@ -1340,6 +1448,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1340
1448
  );
1341
1449
  }
1342
1450
  servicePath = component.artifact.path;
1451
+ componentKind = component.kind;
1343
1452
  } else {
1344
1453
  if (legacyApis.length === 0) {
1345
1454
  throw new Error(
@@ -1357,7 +1466,7 @@ export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_T
1357
1466
  }
1358
1467
  servicePath = api.path;
1359
1468
  }
1360
- return toRepoRelative(join(base, subject.path, servicePath));
1469
+ return { path: toRepoRelative(join(base, subject.path, servicePath)), componentKind };
1361
1470
  }
1362
1471
 
1363
1472
  function assertSourceManifestParity(structure, commitByRepo, locate = FIRST_PARTY_TREES) {
@@ -1815,7 +1924,7 @@ export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {})
1815
1924
  const schemas = await loadKernelDistributionSchemas();
1816
1925
  const imageLock = validateImmutableImageLock(
1817
1926
  schemas.parseImageLock({
1818
- schemaVersion: 3,
1927
+ schemaVersion: imageLockWriteSchemaVersion(schemas, subjects, `distribution "${id}"`),
1819
1928
  sourceHashAlgorithm: schemas.ImageSourceHashAlgorithm.ImageInputV2,
1820
1929
  distributionId: id,
1821
1930
  contractAuthority,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.41.1",
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:7e8af916b8cd5c8de331d7e274a7022071e6b6ac0d9d54042c2e55461ad46bab"
95
+ "sourceDigest": "sha256:ae545509930a5b6f1215c413f3e7121b15a29d5fd3e14ab373f0910ceaa9063d"
96
96
  }
97
97
  }