@xemahq/xema 0.52.0 → 0.54.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.
Files changed (50) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +2 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/auth.d.ts +9 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +66 -0
  8. package/dist/commands/auth.js.map +1 -0
  9. package/dist/commands/edition.d.ts.map +1 -1
  10. package/dist/commands/edition.js +1 -0
  11. package/dist/commands/edition.js.map +1 -1
  12. package/dist/edition/verify-image-evidence.bundle.mjs +1013 -870
  13. package/dist/edition/verify-image-evidence.bundle.provenance.json +501 -495
  14. package/dist/runtime/edition/verb-map.d.ts +2 -1
  15. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  16. package/dist/runtime/edition/verb-map.js +2 -0
  17. package/dist/runtime/edition/verb-map.js.map +1 -1
  18. package/edition/README.md +54 -2
  19. package/edition/adapter-bundled-versions.mjs +2 -1
  20. package/edition/authorization-base.mjs +9 -8
  21. package/edition/biome-content.mjs +23 -29
  22. package/edition/candidate-edition.mjs +692 -0
  23. package/edition/checkout-indexed-source-repositories.mjs +6 -8
  24. package/edition/collected-refusals.mjs +59 -0
  25. package/edition/contract-authority.mjs +122 -0
  26. package/edition/cosign-certificate-constraints.mjs +18 -8
  27. package/edition/customer-edition-verbs.mjs +30 -8
  28. package/edition/develop-chart-set.mjs +7 -6
  29. package/edition/develop-edition-invariants.mjs +2 -1
  30. package/edition/develop-edition-resolve.mjs +109 -31
  31. package/edition/develop-edition.mjs +49 -8
  32. package/edition/develop-engine-image.mjs +95 -0
  33. package/edition/develop-image-probe.mjs +2 -1
  34. package/edition/develop-resolve-report.mjs +159 -0
  35. package/edition/develop-snapshot-inputs.mjs +4 -3
  36. package/edition/develop-verify-image-record.mjs +23 -4
  37. package/edition/edition-artifact.mjs +17 -7
  38. package/edition/edition-protocol.mjs +8 -2
  39. package/edition/evidence-input-mode.mjs +4 -22
  40. package/edition/images-from-record.mjs +362 -0
  41. package/edition/lib.mjs +1 -19
  42. package/edition/locked-bundle-signer.mjs +15 -3
  43. package/edition/operation-grant-admission.mjs +51 -9
  44. package/edition/operation-requirements.mjs +11 -0
  45. package/edition/platform-service-class.mjs +4 -2
  46. package/edition/resolve-distribution.mjs +95 -82
  47. package/edition/resolve-images.mjs +12 -5
  48. package/edition/shallow-checkout.mjs +18 -0
  49. package/edition/workspace-sections.mjs +23 -3
  50. package/package.json +7 -3
@@ -74,6 +74,7 @@ import {
74
74
  sealEditionAuthorizationBase,
75
75
  writeAuthorizationLock,
76
76
  } from './authorization-base.mjs';
77
+ import { RefusalError, captureRefusal, isRefusal, throwCollectedRefusals } from './collected-refusals.mjs';
77
78
  import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
78
79
  import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
79
80
  import { workspaceSection } from './edition-config.mjs';
@@ -95,16 +96,8 @@ import {
95
96
  validateImmutableDistributionLock,
96
97
  validateServiceDependencyOwnerParity,
97
98
  } from './lib.mjs';
98
- import { BundleSignerPolicyAbsentError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
99
- import {
100
- assertInstalledOperationCeilingAdmitted,
101
- assertOperationGrantsAdmitted,
102
- assertExactAudiencesAdmitted,
103
- assertOrganizationSweepsAdmitted,
104
- assertEventSubscriptionsAdmitted,
105
- assertOrgContextRelaysAdmitted,
106
- assertStoreWorkloadCeilingAdmitted,
107
- } from './operation-grant-admission.mjs';
99
+ import { BundleSignerPolicyAbsentError, BundleSignerRefusedError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
100
+ import { admitDistributionLock } from './operation-grant-admission.mjs';
108
101
  import { PUBLISHED_PIN_SOURCE, pinnedSource } from './pin-source.mjs';
109
102
  import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
110
103
  import { closureBiomeFacts } from './positive-closure.mjs';
@@ -151,7 +144,7 @@ export function lockedServiceProcessFacts(entry) {
151
144
  const facts = entry.serviceProcessFacts ?? {};
152
145
  const named = Object.keys(facts).sort();
153
146
  if (serializeJson(named) !== serializeJson([...services].sort())) {
154
- throw new Error(
147
+ throw new RefusalError(
155
148
  `biome "${entry.id}" carries process facts for [${named.join(', ')}] but ships ` +
156
149
  `services [${services.join(', ')}] — every shipped service has exactly one ` +
157
150
  'row; regenerate the biome index.',
@@ -181,14 +174,14 @@ function versionSatisfies(actual, range) {
181
174
  };
182
175
  const a = parse(actual);
183
176
  if (!a) {
184
- throw new Error(`cannot parse actual version "${actual}"`);
177
+ throw new RefusalError(`cannot parse actual version "${actual}"`);
185
178
  }
186
179
  const trimmed = range.trim();
187
180
  const op = trimmed[0] === '^' || trimmed[0] === '~' ? trimmed[0] : '=';
188
181
  const bare = op === '=' ? trimmed : trimmed.slice(1);
189
182
  const r = parse(bare);
190
183
  if (!r) {
191
- throw new Error(
184
+ throw new RefusalError(
192
185
  `cannot parse version range "${range}" — only exact / ^ / ~ forms are supported by this bounded checker`,
193
186
  );
194
187
  }
@@ -222,7 +215,7 @@ function versionSatisfies(actual, range) {
222
215
  const REFUSED_EXTENDS_FIELD = 'extends';
223
216
 
224
217
  /** An edition definition that breaks the edition model (a refused field); a caller tells it from an I/O or schema error by class. */
225
- export class EditionModelError extends Error {
218
+ export class EditionModelError extends RefusalError {
226
219
  constructor(message) {
227
220
  super(message);
228
221
  this.name = 'EditionModelError';
@@ -240,7 +233,7 @@ const REFUSED_PLATFORM_SERVICES_FIELD = 'platformServices';
240
233
  * A selection or declared dependency the index does not hold. Against a
241
234
  * catalogue it is the catalogue's ceiling: the caller names the catalogue.
242
235
  */
243
- export class UnknownSelectionError extends Error {
236
+ export class UnknownSelectionError extends RefusalError {
244
237
  constructor(message) {
245
238
  super(message);
246
239
  this.name = 'UnknownSelectionError';
@@ -254,7 +247,7 @@ export class UnknownSelectionError extends Error {
254
247
  export function loadEditionManifest(parseDistribution, id) {
255
248
  const path = join(editionsRoot(), id, 'xema-distribution.json');
256
249
  if (!existsSync(path)) {
257
- throw new Error(`distribution "${id}" not found: ${path} does not exist`);
250
+ throw new RefusalError(`distribution "${id}" not found: ${path} does not exist`);
258
251
  }
259
252
  return parseEditionManifest(parseDistribution, id, readStrictJsonFile(path), path);
260
253
  }
@@ -271,7 +264,7 @@ export function parseEditionManifest(parseDistribution, id, raw, source) {
271
264
  );
272
265
  }
273
266
  if (raw !== null && typeof raw === 'object' && Object.hasOwn(raw, REFUSED_PLATFORM_SERVICES_FIELD)) {
274
- throw new Error(
267
+ throw new RefusalError(
275
268
  `edition "${id}": "${REFUSED_PLATFORM_SERVICES_FIELD}" is not part of the edition definition; ` +
276
269
  'the platform substrate is drawn from platform-service-index.json by installation class ' +
277
270
  "(core class, the classes the edition's include names, and services a shipped biome's API requires)",
@@ -280,7 +273,7 @@ export function parseEditionManifest(parseDistribution, id, raw, source) {
280
273
  // `sourcePins` reaches the kernel unchanged: its Distribution schema validates it.
281
274
  const manifest = parseDistribution(raw);
282
275
  if (manifest.id !== id) {
283
- throw new Error(`distribution at ${source} declares id "${manifest.id}", but it was addressed as "${id}"`);
276
+ throw new RefusalError(`distribution at ${source} declares id "${manifest.id}", but it was addressed as "${id}"`);
284
277
  }
285
278
  return manifest;
286
279
  }
@@ -355,7 +348,7 @@ function webBiomeProducerRepo() {
355
348
  const hosts = workspaceSection('runtimeImageSources', 'knowing which repository produces web biomes')
356
349
  .supplementalSources.filter((source) => source.kind === RuntimeSubjectKind.Host);
357
350
  if (hosts.length !== 1) {
358
- throw new Error(
351
+ throw new RefusalError(
359
352
  `edition-workspace.json runtimeImageSources must declare exactly one "${RuntimeSubjectKind.Host}" source ` +
360
353
  `(the image web biomes are bundled into); it declares ${hosts.length}`,
361
354
  );
@@ -449,7 +442,7 @@ export function isUniversal(entry) {
449
442
  */
450
443
  export function assertIndexDeclaresInstallationClasses(id, index) {
451
444
  if (!index.some((entry) => entry.installationClass !== undefined)) {
452
- throw new Error(
445
+ throw new RefusalError(
453
446
  `distribution "${id}": NO entry in the biome index carries an installationClass, so ` +
454
447
  `the index cannot say which components are universal (installationClass core) ` +
455
448
  `and every edition would resolve without its floor. The tier is never read in its ` +
@@ -460,14 +453,14 @@ export function assertIndexDeclaresInstallationClasses(id, index) {
460
453
  const universal = universalInstallationClass();
461
454
  for (const entry of [...index].sort(compareIndexEntries)) {
462
455
  if (entry.tier === 'kernel' && entry.installationClass !== universal) {
463
- throw new Error(
456
+ throw new RefusalError(
464
457
  `distribution "${id}": kernel-tier biome "${entry.id}" (${entry.repo}) declares ` +
465
458
  `installationClass ${entry.installationClass === undefined ? '(none)' : `"${entry.installationClass}"`}; ` +
466
459
  `every kernel-tier biome must declare "${universal}" — fix the manifest and regenerate the index.`,
467
460
  );
468
461
  }
469
462
  if (entry.installationClass === universal && entry.audience === 'operator') {
470
- throw new Error(
463
+ throw new RefusalError(
471
464
  `distribution "${id}": operator biome "${entry.id}" (${entry.repo}) declares installationClass ` +
472
465
  `"${universal}" — an operator biome ships only where an edition names it, so it cannot be universal.`,
473
466
  );
@@ -581,7 +574,7 @@ export function mergeExternalSourceEntries(
581
574
  // customer's private biome shipping inside `oss`.
582
575
  const unstamped = externalEntries.filter((entry) => entry.source === undefined);
583
576
  if (unstamped.length > 0) {
584
- throw new Error(
577
+ throw new RefusalError(
585
578
  `refusing to merge ${unstamped.length} UNSTAMPED external entr(ies) ` +
586
579
  `(${unstamped.map((entry) => entry.id).join(', ')}) into the biome index: an entry ` +
587
580
  'with no source stamp is platform-default and admissible to every edition.',
@@ -635,7 +628,7 @@ function isThirdPartyOciSourceEntry(entry, sourceRecords) {
635
628
  function requireSourceRecord(entry, sourceRecords) {
636
629
  const record = sourceRecords.get(entry.source);
637
630
  if (record === undefined) {
638
- throw new Error(
631
+ throw new RefusalError(
639
632
  `biome "${entry.id}" is stamped with source "${entry.source}", but no pinned source ` +
640
633
  'record was produced for that name. A stamped entry with no record cannot be traced ' +
641
634
  'to a revision, and emitting it unstamped would make it read as first-party.',
@@ -677,7 +670,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
677
670
  }
678
671
  if (existing.form === form) {
679
672
  if (existing.policy !== policy) {
680
- throw new Error(
673
+ throw new RefusalError(
681
674
  `conflicting installPolicy for biome "${entryId}": two ${form}-form selectors ` +
682
675
  `stamp "${existing.policy}" and "${policy}" — resolve the manifest ambiguity`,
683
676
  );
@@ -718,7 +711,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
718
711
  entrySourceAdmissible(e, declaredSourceNames),
719
712
  );
720
713
  if (matches.length === 0) {
721
- throw new Error(
714
+ throw new RefusalError(
722
715
  `include { installationClass: "${sel.installationClass}" } matches no biome in ` +
723
716
  `the index. An edition that asks for a class nothing declares is asking for ` +
724
717
  `something that cannot be built; name the biomes explicitly or drop the selector.`,
@@ -743,7 +736,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
743
736
  );
744
737
  if (inadmissible.length > 0 && inadmissible.length === idMatches.length) {
745
738
  const names = [...new Set(inadmissible.map((e) => e.source))].sort();
746
- throw new Error(
739
+ throw new RefusalError(
747
740
  `include { biome: "${sel.biome}" } names a biome drawn from source(s) ` +
748
741
  `${names.map((name) => `"${name}"`).join(', ')}, which this edition does not ` +
749
742
  `declare in sources[]. A biome is reachable only from a source its own ` +
@@ -757,7 +750,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
757
750
  // naming an operator biome is how you force it in, while naming a web
758
751
  // biome with no producer asks for something no build can make.
759
752
  if (idMatches.every((e) => !isDeliverable(e, webProducerRepos))) {
760
- throw new Error(
753
+ throw new RefusalError(
761
754
  `include { biome: "${sel.biome}" } names a target:web biome authored in ` +
762
755
  `"${idMatches[0].repo}". Only web biomes under ` +
763
756
  `"${(webProducerRepos ?? [webBiomeProducerRepo()]).join(', ')}" are bundled into the host image, so nothing ` +
@@ -772,20 +765,20 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
772
765
  try {
773
766
  return versionSatisfies(e.version, sel.version);
774
767
  } catch (err) {
775
- throw new Error(
768
+ throw new RefusalError(
776
769
  `version check for biome "${sel.biome}" failed: ${err.message}`,
777
770
  );
778
771
  }
779
772
  })
780
773
  : idMatches;
781
774
  if (sel.version && matches.length === 0) {
782
- throw new Error(
775
+ throw new RefusalError(
783
776
  `include { biome: "${sel.biome}", version: "${sel.version}" } matched ids but no index version satisfied the range ` +
784
777
  `(available: ${idMatches.map((m) => m.version).join(', ')})`,
785
778
  );
786
779
  }
787
780
  } else {
788
- throw new Error(`unrecognized selector shape: ${JSON.stringify(sel)}`);
781
+ throw new RefusalError(`unrecognized selector shape: ${JSON.stringify(sel)}`);
789
782
  }
790
783
  for (const e of matches) {
791
784
  const key = entryKey(e);
@@ -877,7 +870,7 @@ function selectByPositiveClosure({
877
870
  : { kind };
878
871
  if ('platformService' in root) {
879
872
  if (!platformSourcesByName.has(root.platformService)) {
880
- throw new Error(
873
+ throw new RefusalError(
881
874
  `distribution "${id}": ${cls} root names platform service ` +
882
875
  `"${root.platformService}", which platform-service-index.json does not describe`,
883
876
  );
@@ -957,7 +950,7 @@ function selectByPositiveClosure({
957
950
  });
958
951
  } catch (error) {
959
952
  if (error instanceof BiomeClosureError) {
960
- throw new Error(`distribution "${id}": ${error.message}`, {
953
+ throw new RefusalError(`distribution "${id}": ${error.message}`, {
961
954
  cause: error,
962
955
  });
963
956
  }
@@ -985,7 +978,7 @@ function selectByPositiveClosure({
985
978
  for (const entry of [...selected.values()].sort(compareIndexEntries)) {
986
979
  for (const required of entry.requires ?? []) {
987
980
  if (!shippedIds.has(required)) {
988
- throw new Error(
981
+ throw new RefusalError(
989
982
  `distribution "${id}": biome "${entry.id}" declares that it requires biome ` +
990
983
  `"${required}", which positive closure did not include — its dependency ` +
991
984
  'contract carries no required edge to it. Either the contract is missing ' +
@@ -999,7 +992,7 @@ function selectByPositiveClosure({
999
992
  ? shippedPlatformServices.has(edge.requiredService)
1000
993
  : shippedIds.has(edge.owner.biomeId);
1001
994
  if (!satisfied) {
1002
- throw new Error(
995
+ throw new RefusalError(
1003
996
  `distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" declares ` +
1004
997
  `that it requires service "${edge.requiredService}" (owned by ` +
1005
998
  `${edge.owner.kind === 'platform' ? 'platform service' : `biome "${edge.owner.biomeId}"`}), ` +
@@ -1050,7 +1043,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
1050
1043
  for (const entry of selectedEntries) {
1051
1044
  for (const provision of entry.providesResources ?? []) {
1052
1045
  if (!(Array.isArray(entry.services) && entry.services.includes(provision.service))) {
1053
- throw new Error(
1046
+ throw new RefusalError(
1054
1047
  `distribution "${distributionId}": biome "${entry.id}" declares that ` +
1055
1048
  `"${provision.service}" provides ${provision.kind}, but it does not ` +
1056
1049
  `ship that service`,
@@ -1070,7 +1063,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
1070
1063
  `${requirement.kind} "${requirement.logicalName}" (${requirement.scope}-scoped, ` +
1071
1064
  `${requirement.inclusion})`;
1072
1065
  if (providers.length > 1) {
1073
- throw new Error(
1066
+ throw new RefusalError(
1074
1067
  `distribution "${distributionId}": ${subject}, and ${providers.length} ` +
1075
1068
  `shipped services provide that kind: ` +
1076
1069
  `${providers.map((p) => `${p.biomeId}/${p.serviceName}`).join(', ')}. ` +
@@ -1080,7 +1073,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
1080
1073
  }
1081
1074
  if (providers.length === 0) {
1082
1075
  if (requirement.inclusion === 'required') {
1083
- throw new Error(
1076
+ throw new RefusalError(
1084
1077
  `distribution "${distributionId}": ${subject}, and no shipped service ` +
1085
1078
  `provides ${requirement.kind}. Select the biome that provides it, ` +
1086
1079
  `or exclude the consumer.`,
@@ -1096,6 +1089,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
1096
1089
  logicalName: requirement.logicalName,
1097
1090
  scope: requirement.scope,
1098
1091
  inclusion: requirement.inclusion,
1092
+ ...(requirement.sharing === undefined ? {} : { sharing: requirement.sharing }),
1099
1093
  provider: providers[0],
1100
1094
  });
1101
1095
  }
@@ -1132,7 +1126,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
1132
1126
  for (const entry of selectedEntries) {
1133
1127
  for (const provision of entry.nexusProvides ?? []) {
1134
1128
  if (!(Array.isArray(entry.services) && entry.services.includes(provision.component))) {
1135
- throw new Error(
1129
+ throw new RefusalError(
1136
1130
  `distribution "${distributionId}": biome "${entry.id}" declares that ` +
1137
1131
  `component "${provision.component}" provides nexus service ` +
1138
1132
  `${provision.service}@${provision.version}, but it does not ship that ` +
@@ -1149,7 +1143,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
1149
1143
  })),
1150
1144
  );
1151
1145
  if (!result.ok) {
1152
- throw new Error(
1146
+ throw new RefusalError(
1153
1147
  `distribution "${distributionId}": ${result.refusals.length} Nexus ` +
1154
1148
  `requirement(s) cannot be bound:\n` +
1155
1149
  result.refusals
@@ -1160,7 +1154,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
1160
1154
  return result.bindings;
1161
1155
  }
1162
1156
 
1163
- export function resolveDistributionFromInputs(
1157
+ export function resolveUnadmittedDistributionFromInputs(
1164
1158
  id,
1165
1159
  index,
1166
1160
  schemas,
@@ -1214,7 +1208,7 @@ export function resolveDistributionFromInputs(
1214
1208
  }
1215
1209
  for (const [biomeId, entries] of entriesById) {
1216
1210
  if (entries.length > 1) {
1217
- throw new Error(
1211
+ throw new RefusalError(
1218
1212
  `distribution "${id}": biome id "${biomeId}" resolves to multiple compound identities: ` +
1219
1213
  entries
1220
1214
  .map((entry) => `${entry.target}@${entry.repo}/${entry.path}`)
@@ -1227,7 +1221,7 @@ export function resolveDistributionFromInputs(
1227
1221
  const platformSourcesByName = new Map();
1228
1222
  for (const source of platformServiceIndex.services) {
1229
1223
  if (platformSourcesByName.has(source.name)) {
1230
- throw new Error(
1224
+ throw new RefusalError(
1231
1225
  `distribution "${id}": platform service "${source.name}" has ambiguous source identities in platform-service-index.json`,
1232
1226
  );
1233
1227
  }
@@ -1298,7 +1292,7 @@ export function resolveDistributionFromInputs(
1298
1292
  const isBiome = entriesById.has(excludedId);
1299
1293
  const platformService = platformServices.get(excludedId);
1300
1294
  if (isBiome && platformService) {
1301
- throw new Error(
1295
+ throw new RefusalError(
1302
1296
  `distribution "${id}" excludes "${excludedId}", which is BOTH a biome id and a ` +
1303
1297
  `platform service name — the shared exclude namespace cannot choose safely`,
1304
1298
  );
@@ -1307,7 +1301,7 @@ export function resolveDistributionFromInputs(
1307
1301
  // Mirrors the universal-biome rule below: a core-class service is in
1308
1302
  // every installation, so it can never be opted out of.
1309
1303
  if (isUniversal(platformService)) {
1310
- throw new Error(
1304
+ throw new RefusalError(
1311
1305
  `distribution "${id}" tries to exclude universal (installationClass core) platform ` +
1312
1306
  `service "${excludedId}" — a universal service is in every edition and cannot be excluded`,
1313
1307
  );
@@ -1316,7 +1310,7 @@ export function resolveDistributionFromInputs(
1316
1310
  continue;
1317
1311
  }
1318
1312
  if (!isBiome) {
1319
- throw new Error(
1313
+ throw new RefusalError(
1320
1314
  `distribution "${id}" excludes unknown biome or platform service "${excludedId}"`,
1321
1315
  );
1322
1316
  }
@@ -1368,7 +1362,7 @@ export function resolveDistributionFromInputs(
1368
1362
  // (3) Apply exclude — but a universal biome cannot be excluded.
1369
1363
  for (const ex of excludedBiomes) {
1370
1364
  if (universalEntries.some((e) => e.id === ex)) {
1371
- throw new Error(
1365
+ throw new RefusalError(
1372
1366
  `distribution "${id}" tries to exclude universal (installationClass core) biome "${ex}" — ` +
1373
1367
  `a universal biome is in every edition and cannot be excluded`,
1374
1368
  );
@@ -1405,7 +1399,7 @@ export function resolveDistributionFromInputs(
1405
1399
  const requireBiomeOwner = (consumer, edge) => {
1406
1400
  const owner = edge.owner;
1407
1401
  if (owner.target !== 'server') {
1408
- throw new Error(
1402
+ throw new RefusalError(
1409
1403
  `distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires service ` +
1410
1404
  `"${edge.requiredService}" from cross-target ${owner.target} biome "${owner.biomeId}"`,
1411
1405
  );
@@ -1421,7 +1415,7 @@ export function resolveDistributionFromInputs(
1421
1415
  candidate.services.includes(edge.requiredService),
1422
1416
  );
1423
1417
  if (matches.length !== 1) {
1424
- throw new Error(
1418
+ throw new RefusalError(
1425
1419
  `distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires service ` +
1426
1420
  `"${edge.requiredService}", but its exact biome owner ${owner.biomeId} ` +
1427
1421
  `(${owner.target}@${owner.repo}/${owner.path}, ${owner.version}, ${owner.sourceManifestHash}) ` +
@@ -1443,7 +1437,7 @@ export function resolveDistributionFromInputs(
1443
1437
  const requirePlatformOwner = (consumer, edge) => {
1444
1438
  const source = edge.owner.source;
1445
1439
  if (source.name !== edge.requiredService) {
1446
- throw new Error(
1440
+ throw new RefusalError(
1447
1441
  `distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" service edge ` +
1448
1442
  `"${edge.requiredService}" carries mismatched platform owner "${source.name}"`,
1449
1443
  );
@@ -1456,7 +1450,7 @@ export function resolveDistributionFromInputs(
1456
1450
  );
1457
1451
  }
1458
1452
  if (serializeJson(canonical) !== serializeJson(source)) {
1459
- throw new Error(
1453
+ throw new RefusalError(
1460
1454
  `distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires platform service ` +
1461
1455
  `"${edge.requiredService}", but its exact committed source identity is absent or mismatched`,
1462
1456
  );
@@ -1475,7 +1469,7 @@ export function resolveDistributionFromInputs(
1475
1469
  }
1476
1470
  const declared = shippedPlatformServices.get(edge.requiredService);
1477
1471
  if (!declared) {
1478
- throw new Error(
1472
+ throw new RefusalError(
1479
1473
  `distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires platform service ` +
1480
1474
  `"${edge.requiredService}", but the distribution explicitly excludes it`,
1481
1475
  );
@@ -1529,7 +1523,7 @@ export function resolveDistributionFromInputs(
1529
1523
  pullWebBiomesOf(entry);
1530
1524
  for (const req of [...entry.requires].sort()) {
1531
1525
  if (excludedBiomes.has(req)) {
1532
- throw new Error(
1526
+ throw new RefusalError(
1533
1527
  `distribution "${id}": biome "${entry.id}" (${entry.repo}) requires "${req}", ` +
1534
1528
  `but the distribution explicitly excludes it`,
1535
1529
  );
@@ -1548,7 +1542,7 @@ export function resolveDistributionFromInputs(
1548
1542
  if (
1549
1543
  dependencies.some((dependency) => dependency.audience === 'operator')
1550
1544
  ) {
1551
- throw new Error(
1545
+ throw new RefusalError(
1552
1546
  `distribution "${id}": biome "${entry.id}" (${entry.repo}) requires operator biome "${req}", ` +
1553
1547
  `which must be selected explicitly by id`,
1554
1548
  );
@@ -1568,7 +1562,7 @@ export function resolveDistributionFromInputs(
1568
1562
  );
1569
1563
  if (foreignDeps.length > 0 && foreignDeps.length === dependencies.length) {
1570
1564
  const names = [...new Set(foreignDeps.map((d) => d.source))].sort();
1571
- throw new Error(
1565
+ throw new RefusalError(
1572
1566
  `distribution "${id}": biome "${entry.id}" (${entry.repo}) requires "${req}", ` +
1573
1567
  `which is drawn from source(s) ${names.map((name) => `"${name}"`).join(', ')} ` +
1574
1568
  `that this edition does not declare in sources[]. Refusing rather than ` +
@@ -1592,14 +1586,14 @@ export function resolveDistributionFromInputs(
1592
1586
  if (entry.dependencyContract !== undefined) {
1593
1587
  for (const target of schemas.contractRequiredServiceTargets(entry.dependencyContract)) {
1594
1588
  if (excludedPlatformServices.has(target)) {
1595
- throw new Error(
1589
+ throw new RefusalError(
1596
1590
  `distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
1597
1591
  `"${target}", but the distribution explicitly excludes it`,
1598
1592
  );
1599
1593
  }
1600
1594
  for (const owner of serverOwnersOfService.get(target) ?? []) {
1601
1595
  if (excludedBiomes.has(owner.id)) {
1602
- throw new Error(
1596
+ throw new RefusalError(
1603
1597
  `distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
1604
1598
  `"${target}" owned by explicitly excluded biome "${owner.id}"`,
1605
1599
  );
@@ -1609,7 +1603,7 @@ export function resolveDistributionFromInputs(
1609
1603
  }
1610
1604
  for (const edge of entry.serviceDependencies ?? []) {
1611
1605
  if (!entry.services.includes(edge.consumerService)) {
1612
- throw new Error(
1606
+ throw new RefusalError(
1613
1607
  `distribution "${id}": biome "${entry.id}" service dependency names unknown consumer API ` +
1614
1608
  `"${edge.consumerService}"`,
1615
1609
  );
@@ -1620,7 +1614,7 @@ export function resolveDistributionFromInputs(
1620
1614
  }
1621
1615
  const dependency = requireBiomeOwner(entry, edge);
1622
1616
  if (excludedBiomes.has(dependency.id)) {
1623
- throw new Error(
1617
+ throw new RefusalError(
1624
1618
  `distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" requires service ` +
1625
1619
  `"${edge.requiredService}" owned by explicitly excluded biome "${dependency.id}"`,
1626
1620
  );
@@ -1629,7 +1623,7 @@ export function resolveDistributionFromInputs(
1629
1623
  dependency.audience === 'operator' &&
1630
1624
  !includedIds.has(dependency.id)
1631
1625
  ) {
1632
- throw new Error(
1626
+ throw new RefusalError(
1633
1627
  `distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" requires service ` +
1634
1628
  `"${edge.requiredService}" owned by operator biome "${dependency.id}", which must be selected explicitly`,
1635
1629
  );
@@ -1659,7 +1653,7 @@ export function resolveDistributionFromInputs(
1659
1653
  });
1660
1654
  if (provider.id === entry.id) {continue;}
1661
1655
  if (excludedBiomes.has(provider.id)) {
1662
- throw new Error(
1656
+ throw new RefusalError(
1663
1657
  `distribution "${id}": biome "${entry.id}" service "${requirement.consumerService}" requires ` +
1664
1658
  `${requirement.kind} "${requirement.logicalName}" provided by explicitly excluded biome "${provider.id}"`,
1665
1659
  );
@@ -1720,7 +1714,7 @@ export function resolveDistributionFromInputs(
1720
1714
  }
1721
1715
  }
1722
1716
  const refuseWidening = (consumer, via, pulledClass) => {
1723
- throw new Error(
1717
+ throw new RefusalError(
1724
1718
  `distribution "${id}": biome "${consumer.id}" ${via}, which declares ` +
1725
1719
  `installationClass "${pulledClass}" — "${consumer.id}" is in this edition only ` +
1726
1720
  `through the class selector(s) [${[...admittedClasses].sort().join(', ')}], and what a ` +
@@ -1763,7 +1757,7 @@ export function resolveDistributionFromInputs(
1763
1757
  const allow = new Set(trustPolicy.allow);
1764
1758
  for (const e of selected.values()) {
1765
1759
  if (!allow.has(e.origin)) {
1766
- throw new Error(
1760
+ throw new RefusalError(
1767
1761
  `distribution "${id}": biome "${e.id}" (${e.repo}) has origin "${e.origin}", not in trustPolicy.allow [${[...allow].join(', ')}]`,
1768
1762
  );
1769
1763
  }
@@ -1796,6 +1790,7 @@ export function resolveDistributionFromInputs(
1796
1790
 
1797
1791
  // (5c') Bundle signers — every OCI bundle row (a biome's, a host module's) states the signer the data root's
1798
1792
  // trust policy selects for it. See `locked-bundle-signer.mjs`.
1793
+ const bundleSignerRefusals = [];
1799
1794
  const bundleSignerOf = (e, bundleSource, componentKey) => {
1800
1795
  const subject = { biomeId: e.id, ...(componentKey === undefined ? {} : { componentKey }), origin: e.origin, repo: e.repo, bundleSource };
1801
1796
  if (composed?.bundleSigners === undefined) {
@@ -1803,7 +1798,8 @@ export function resolveDistributionFromInputs(
1803
1798
  `${componentKey === undefined ? `biome "${e.id}"` : `host module "${componentKey}" of biome "${e.id}"`} is an OCI bundle, but this resolve was given no bundle signer selection`,
1804
1799
  );
1805
1800
  }
1806
- return composed.bundleSigners.signerFor(subject);
1801
+ // Every row is selected before any is refused, so one run names them all; the lock is not built from a refused row.
1802
+ return captureRefusal(bundleSignerRefusals, () => composed.bundleSigners.signerFor(subject), isRefusal);
1807
1803
  };
1808
1804
  // A row drawn from a GIT source carries no OCI bundle (biome-level; an OCI host-module bundle is refused below), hence no bundleSigner: it is built
1809
1805
  // from the pinned source, and the manifest's `bundleSource` names a release artifact not built from this commit.
@@ -2310,6 +2306,9 @@ export function resolveDistributionFromInputs(
2310
2306
  // one (the kernel refuses an empty list), carried unchanged.
2311
2307
  if (storeEventCeiling !== undefined) {lock.storeEventCeiling = storeEventCeiling;}
2312
2308
 
2309
+ // (6b) Every bundle signer refusal found while building the rows, together: none, and the lock is whole.
2310
+ throwCollectedRefusals(bundleSignerRefusals, BundleSignerRefusedError);
2311
+
2313
2312
  // (7) Re-derive the ownership index through the KERNEL so the table this
2314
2313
  // resolver emits is validated by the exact code every consumer will read
2315
2314
  // it with (`capability-registry-api`, `xema-capability-router`). Catches
@@ -2321,21 +2320,24 @@ export function resolveDistributionFromInputs(
2321
2320
  schemas.resolveFirstPartyCapabilityDomains(lock);
2322
2321
 
2323
2322
  // (8) Validate against the BUILT kernel schema — fail-fast.
2324
- const parsed = schemas.parseUnsealedDistributionLock(lock);
2325
- // (9) Admit every declared service-to-service operation requirement; a
2326
- // refusal throws, so a lock with one is never returned to be written.
2327
- // An optional operation left ungranted is reported, one line each, and
2328
- // never fails (the resolve and `edition check` both run through here).
2329
- assertOperationGrantsAdmitted(schemas, parsed, { report: (line) => console.log(`[operations] ${line}`) });
2330
- assertOrganizationSweepsAdmitted(schemas, parsed);
2331
- assertExactAudiencesAdmitted(schemas, parsed);
2332
- assertOrgContextRelaysAdmitted(schemas, parsed);
2333
- assertEventSubscriptionsAdmitted(schemas, parsed);
2334
- assertInstalledOperationCeilingAdmitted(schemas, parsed);
2335
- assertStoreWorkloadCeilingAdmitted(schemas, parsed);
2323
+ schemas.parseUnsealedDistributionLock(lock);
2336
2324
  return validateImmutableDistributionLock(lock);
2337
2325
  }
2338
2326
 
2327
+ /**
2328
+ * Admit every declared service-to-service operation requirement of a composed lock; a refusal throws, so a lock with
2329
+ * one is never returned to be written. An optional operation left ungranted is reported, one line each, and never
2330
+ * fails (the resolve and `edition check` both run through here). Admission only reads the lock.
2331
+ */
2332
+ export const admitResolvedLock = (schemas, lock) => admitDistributionLock(schemas, lock, { report: (line) => console.log(`[operations] ${line}`) });
2333
+
2334
+ /** The lock composed over the inputs, admitted: what every release caller gets. */
2335
+ export function resolveDistributionFromInputs(id, index, schemas, composed, resolvedAt, platformServiceIndex) {
2336
+ const lock = resolveUnadmittedDistributionFromInputs(id, index, schemas, composed, resolvedAt, platformServiceIndex);
2337
+ admitResolvedLock(schemas, lock);
2338
+ return lock;
2339
+ }
2340
+
2339
2341
  /**
2340
2342
  * Produce a lock object for a distribution id (used by both the CLI and
2341
2343
  * dist-check). `resolvedAt` is freshly stamped each call; callers that diff
@@ -2359,7 +2361,18 @@ export async function resolveDistribution(id) {
2359
2361
  * `materialize` is the walk's one network seam, injectable for the contract
2360
2362
  * suite exactly as it is on `walkDeclaredSources`.
2361
2363
  */
2362
- export async function resolveDistributionWithIndex(id, { materialize, inputs, readManifest } = {}) {
2364
+ export async function resolveDistributionWithIndex(id, options = {}) {
2365
+ const { lock, index, schemas } = await prepareDistributionWithIndex(id, options);
2366
+ admitResolvedLock(schemas, lock);
2367
+ return { lock, index };
2368
+ }
2369
+
2370
+ /**
2371
+ * `resolveDistributionWithIndex` up to, and not including, admission: the composed lock, its index and the kernel
2372
+ * schemas it was composed with. A develop resolve admits it itself, beside its other checks, so every refusal is
2373
+ * reported in one run; the lock bytes are the ones the admitting variant returns.
2374
+ */
2375
+ export async function prepareDistributionWithIndex(id, { materialize, inputs, readManifest } = {}) {
2363
2376
  const schemas = await loadKernelDistributionSchemas();
2364
2377
  // The manifest is composed FIRST, because it is what declares the sources and
2365
2378
  // what refuses a malformed definition before any other work. The walk then
@@ -2403,7 +2416,7 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
2403
2416
  inputs !== undefined && Object.hasOwn(inputs, 'storePublisherTrust')
2404
2417
  ? inputs.storePublisherTrust
2405
2418
  : readStorePublisherTrust();
2406
- const lock = resolveDistributionFromInputs(
2419
+ const lock = resolveUnadmittedDistributionFromInputs(
2407
2420
  id,
2408
2421
  index,
2409
2422
  schemas,
@@ -2416,7 +2429,7 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
2416
2429
  new Date().toISOString(),
2417
2430
  platformServiceIndex,
2418
2431
  );
2419
- return { lock, index };
2432
+ return { lock, index, schemas };
2420
2433
  }
2421
2434
 
2422
2435
  /**
@@ -2434,7 +2447,7 @@ export function assertReleaseGitSourcesDeclareNoBundle(lock, index) {
2434
2447
  (bundleSource) => bundleSource?.kind === 'oci',
2435
2448
  );
2436
2449
  if (declared !== undefined) {
2437
- throw new Error(
2450
+ throw new RefusalError(
2438
2451
  `biome "${biome.id}" is drawn from git source "${biome.source.name}" and declares an OCI bundleSource (${declared.ociRef}); ` +
2439
2452
  'a release does not carry it: declare the biome as an `oci` source with a third-party source pin instead',
2440
2453
  );
@@ -2462,7 +2475,7 @@ export async function writeResolvedDistribution(id, { log = true, materialize, f
2462
2475
  // edition; resolve-chart-set.mjs keeps every copy on the pin thereafter.
2463
2476
  const pinPath = fleetChartSetLockPath();
2464
2477
  if (!existsSync(pinPath)) {
2465
- throw new Error(`${pinPath} is missing — run resolve-chart-set.mjs; an edition is not resolved without its chart set`);
2478
+ throw new RefusalError(`${pinPath} is missing — run resolve-chart-set.mjs; an edition is not resolved without its chart set`);
2466
2479
  }
2467
2480
  const pinText = readFileSync(pinPath, 'utf8');
2468
2481
  writeEditionChartSet(join(editionsRoot(), id), pinText);
@@ -2513,7 +2526,7 @@ export async function sealAuthorizationBase(id, lock, index, { log = true } = {}
2513
2526
  */
2514
2527
  export async function composeAuthorizationBase(id, lock, index) {
2515
2528
  if (!Array.isArray(index)) {
2516
- throw new Error(
2529
+ throw new RefusalError(
2517
2530
  `${id}: sealAuthorizationBase needs the index the lock was resolved from ` +
2518
2531
  '(resolveDistributionWithIndex). The committed platform index alone does not hold a source-walked biome.',
2519
2532
  );