@timeax/digital-service-engine 0.2.1 → 0.2.2

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.
@@ -26,6 +26,7 @@ __export(core_exports, {
26
26
  createFallbackEditor: () => createFallbackEditor,
27
27
  createNodeIndex: () => createNodeIndex,
28
28
  filterServicesForVisibleGroup: () => filterServicesForVisibleGroup,
29
+ getAssignedServiceIds: () => getAssignedServiceIds,
29
30
  getEligibleFallbacks: () => getEligibleFallbacks,
30
31
  getFallbackRegistrationInfo: () => getFallbackRegistrationInfo,
31
32
  normalise: () => normalise,
@@ -1231,6 +1232,9 @@ function toFiniteNumber(v) {
1231
1232
  const n = Number(v);
1232
1233
  return Number.isFinite(n) ? n : NaN;
1233
1234
  }
1235
+ function isValidServiceIdRef(value) {
1236
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
1237
+ }
1234
1238
  function constraintFitOk(svcMap, candidate, constraints) {
1235
1239
  const cap = getServiceCapability(svcMap, candidate);
1236
1240
  if (!cap) return false;
@@ -1239,18 +1243,32 @@ function constraintFitOk(svcMap, candidate, constraints) {
1239
1243
  return !(constraints.cancel === true && !cap.cancel);
1240
1244
  }
1241
1245
  function getServiceCapability(svcMap, candidate) {
1242
- if (candidate === void 0 || candidate === null) return void 0;
1243
- const direct = svcMap[candidate];
1244
- if (direct) return direct;
1245
- const byString = svcMap[String(candidate)];
1246
- if (byString) return byString;
1247
- if (typeof candidate === "string") {
1248
- const maybeNumber = Number(candidate);
1249
- if (Number.isFinite(maybeNumber)) {
1250
- return svcMap[maybeNumber];
1251
- }
1246
+ var _a;
1247
+ return (_a = getServiceCapabilityEntry(svcMap, candidate)) == null ? void 0 : _a.capability;
1248
+ }
1249
+ function getServiceCapabilityCanonicalRef(svcMap, candidate) {
1250
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1251
+ if (!entry) return void 0;
1252
+ return getCanonicalServiceRef(entry.key, entry.capability);
1253
+ }
1254
+ function getServiceCapabilityAliases(svcMap, candidate) {
1255
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1256
+ if (!entry) return [];
1257
+ return collectServiceRefAliases(entry.key, entry.capability);
1258
+ }
1259
+ function isSameServiceCapabilityRef(svcMap, left, right) {
1260
+ if (!isValidServiceIdRef(left) || !isValidServiceIdRef(right)) return false;
1261
+ const leftAliases = new Set(
1262
+ getServiceCapabilityAliases(svcMap, left).map((value) => String(value))
1263
+ );
1264
+ if (!leftAliases.size) {
1265
+ leftAliases.add(String(left));
1252
1266
  }
1253
- return void 0;
1267
+ const rightAliases = getServiceCapabilityAliases(svcMap, right);
1268
+ if (!rightAliases.length) {
1269
+ return leftAliases.has(String(right));
1270
+ }
1271
+ return rightAliases.some((value) => leftAliases.has(String(value)));
1254
1272
  }
1255
1273
  function normalizeRatePolicy(policy) {
1256
1274
  var _a;
@@ -1286,6 +1304,74 @@ function rateOk(svcMap, candidate, primary, policy) {
1286
1304
  if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
1287
1305
  return passesRatePolicy(policy.ratePolicy, pRate, cRate);
1288
1306
  }
1307
+ function getServiceCapabilityEntry(svcMap, candidate) {
1308
+ if (candidate === void 0 || candidate === null) return void 0;
1309
+ const direct = svcMap[candidate];
1310
+ if (direct) {
1311
+ return { key: String(candidate), capability: direct };
1312
+ }
1313
+ const byString = svcMap[String(candidate)];
1314
+ if (byString) {
1315
+ return { key: String(candidate), capability: byString };
1316
+ }
1317
+ if (typeof candidate === "string") {
1318
+ const maybeNumber = Number(candidate);
1319
+ if (Number.isFinite(maybeNumber)) {
1320
+ const byNumber = svcMap[maybeNumber];
1321
+ if (byNumber) {
1322
+ return { key: String(maybeNumber), capability: byNumber };
1323
+ }
1324
+ }
1325
+ }
1326
+ const target = String(candidate);
1327
+ for (const [key, capability] of Object.entries(svcMap != null ? svcMap : {})) {
1328
+ if (collectServiceRefAliases(key, capability).some(
1329
+ (alias) => String(alias) === target
1330
+ )) {
1331
+ return { key, capability };
1332
+ }
1333
+ }
1334
+ return void 0;
1335
+ }
1336
+ function collectServiceRefAliases(key, capability) {
1337
+ const out = [];
1338
+ const seen = /* @__PURE__ */ new Set();
1339
+ const push = (value) => {
1340
+ if (!isValidServiceIdRef(value)) return;
1341
+ const normalized = normalizeServiceRef(value);
1342
+ if (!normalized) return;
1343
+ const aliasKey = String(normalized);
1344
+ if (seen.has(aliasKey)) return;
1345
+ seen.add(aliasKey);
1346
+ out.push(normalized);
1347
+ };
1348
+ push(getCanonicalServiceRef(key, capability));
1349
+ push(capability.service);
1350
+ push(capability.key);
1351
+ push(capability.id);
1352
+ return out;
1353
+ }
1354
+ function getCanonicalServiceRef(key, capability) {
1355
+ const explicitRefs = [capability.service, capability.key, capability.id];
1356
+ for (const ref of explicitRefs) {
1357
+ if (!isValidServiceIdRef(ref)) continue;
1358
+ if (String(ref) === key) {
1359
+ return ref;
1360
+ }
1361
+ }
1362
+ return normalizeServiceRef(key);
1363
+ }
1364
+ function normalizeServiceRef(value) {
1365
+ if (!isValidServiceIdRef(value)) return void 0;
1366
+ if (typeof value === "number") return value;
1367
+ const trimmed = value.trim();
1368
+ if (!trimmed) return void 0;
1369
+ const asNumber = Number(trimmed);
1370
+ if (Number.isFinite(asNumber) && String(asNumber) === trimmed) {
1371
+ return asNumber;
1372
+ }
1373
+ return trimmed;
1374
+ }
1289
1375
 
1290
1376
  // src/core/validate/steps/rates.ts
1291
1377
  function validateRates(v) {
@@ -2738,27 +2824,42 @@ var DEFAULT_SETTINGS = {
2738
2824
  mode: "strict"
2739
2825
  };
2740
2826
  function resolveServiceFallback(params) {
2741
- var _a, _b, _c, _d, _e;
2827
+ var _a, _b;
2742
2828
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2743
2829
  const { primary, nodeId, tagId, services } = params;
2744
- const fb = (_b = params.fallbacks) != null ? _b : {};
2745
- const tried = [];
2746
- const lists = [];
2747
- if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
2748
- if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
2830
+ const fallbackLists = listRegisteredFallbackCandidates(
2831
+ (_b = params.fallbacks) != null ? _b : {},
2832
+ primary,
2833
+ nodeId,
2834
+ services
2835
+ );
2836
+ const tried = /* @__PURE__ */ new Set();
2749
2837
  const primaryRate = rateOf(services, primary);
2750
- for (const list of lists) {
2751
- for (const cand of list) {
2752
- if (tried.includes(cand)) continue;
2753
- tried.push(cand);
2754
- const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
2755
- if (!candCap) continue;
2756
- if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
2838
+ for (const list of fallbackLists) {
2839
+ for (const candidate of list) {
2840
+ const candidateIdentity = getComparableServiceRefKey(
2841
+ services,
2842
+ candidate
2843
+ );
2844
+ if (tried.has(candidateIdentity)) continue;
2845
+ tried.add(candidateIdentity);
2846
+ const capability = getCap(services, candidate);
2847
+ if (!capability) continue;
2848
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2849
+ continue;
2850
+ }
2851
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
2852
+ continue;
2853
+ }
2757
2854
  if (s.requireConstraintFit && tagId) {
2758
- const ok = satisfiesTagConstraints(tagId, params, candCap);
2759
- if (!ok) continue;
2855
+ const fitsConstraints = satisfiesTagConstraints(
2856
+ tagId,
2857
+ params,
2858
+ capability
2859
+ );
2860
+ if (!fitsConstraints) continue;
2760
2861
  }
2761
- return cand;
2862
+ return candidate;
2762
2863
  }
2763
2864
  }
2764
2865
  return null;
@@ -2768,7 +2869,7 @@ function collectFailedFallbacks(props, services, settings) {
2768
2869
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
2769
2870
  const out = [];
2770
2871
  const fb = (_a = props.fallbacks) != null ? _a : {};
2771
- const primaryRate = (p) => rateOf(services, p);
2872
+ const primaryRate = (primary) => rateOf(services, primary);
2772
2873
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
2773
2874
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2774
2875
  if (!primary) {
@@ -2781,34 +2882,34 @@ function collectFailedFallbacks(props, services, settings) {
2781
2882
  });
2782
2883
  continue;
2783
2884
  }
2784
- for (const cand of list) {
2785
- const cap = getCap(services, cand);
2786
- if (!cap) {
2885
+ for (const candidate of list) {
2886
+ const capability = getCap(services, candidate);
2887
+ if (!capability) {
2787
2888
  out.push({
2788
2889
  scope: "node",
2789
2890
  nodeId,
2790
2891
  primary,
2791
- candidate: cand,
2892
+ candidate,
2792
2893
  reason: "unknown_service"
2793
2894
  });
2794
2895
  continue;
2795
2896
  }
2796
- if (String(cand) === String(primary)) {
2897
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2797
2898
  out.push({
2798
2899
  scope: "node",
2799
2900
  nodeId,
2800
2901
  primary,
2801
- candidate: cand,
2902
+ candidate,
2802
2903
  reason: "cycle"
2803
2904
  });
2804
2905
  continue;
2805
2906
  }
2806
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2907
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2807
2908
  out.push({
2808
2909
  scope: "node",
2809
2910
  nodeId,
2810
2911
  primary,
2811
- candidate: cand,
2912
+ candidate,
2812
2913
  reason: "rate_violation"
2813
2914
  });
2814
2915
  continue;
@@ -2818,58 +2919,55 @@ function collectFailedFallbacks(props, services, settings) {
2818
2919
  scope: "node",
2819
2920
  nodeId,
2820
2921
  primary,
2821
- candidate: cand,
2922
+ candidate,
2822
2923
  reason: "no_tag_context"
2823
2924
  });
2824
2925
  continue;
2825
2926
  }
2826
- let anyPass = false;
2827
- let anyFail = false;
2828
2927
  for (const tagId of tagContexts) {
2829
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
2830
- if (ok) anyPass = true;
2831
- else {
2832
- anyFail = true;
2833
- out.push({
2834
- scope: "node",
2835
- nodeId,
2836
- primary,
2837
- candidate: cand,
2838
- tagContext: tagId,
2839
- reason: "constraint_mismatch"
2840
- });
2841
- }
2928
+ const fitsConstraints = s.requireConstraintFit ? satisfiesTagConstraints(
2929
+ tagId,
2930
+ { services, props },
2931
+ capability
2932
+ ) : true;
2933
+ if (fitsConstraints) continue;
2934
+ out.push({
2935
+ scope: "node",
2936
+ nodeId,
2937
+ primary,
2938
+ candidate,
2939
+ tagContext: tagId,
2940
+ reason: "constraint_mismatch"
2941
+ });
2842
2942
  }
2843
- void anyPass;
2844
- void anyFail;
2845
2943
  }
2846
2944
  }
2847
2945
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
2848
- for (const cand of list) {
2849
- const cap = getCap(services, cand);
2850
- if (!cap) {
2946
+ for (const candidate of list) {
2947
+ const capability = getCap(services, candidate);
2948
+ if (!capability) {
2851
2949
  out.push({
2852
2950
  scope: "global",
2853
2951
  primary,
2854
- candidate: cand,
2952
+ candidate,
2855
2953
  reason: "unknown_service"
2856
2954
  });
2857
2955
  continue;
2858
2956
  }
2859
- if (String(cand) === String(primary)) {
2957
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2860
2958
  out.push({
2861
2959
  scope: "global",
2862
2960
  primary,
2863
- candidate: cand,
2961
+ candidate,
2864
2962
  reason: "cycle"
2865
2963
  });
2866
2964
  continue;
2867
2965
  }
2868
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2966
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2869
2967
  out.push({
2870
2968
  scope: "global",
2871
2969
  primary,
2872
- candidate: cand,
2970
+ candidate,
2873
2971
  reason: "rate_violation"
2874
2972
  });
2875
2973
  }
@@ -2878,52 +2976,61 @@ function collectFailedFallbacks(props, services, settings) {
2878
2976
  return out;
2879
2977
  }
2880
2978
  function rateOf(map, id) {
2881
- var _a;
2979
+ var _a, _b;
2882
2980
  if (id === void 0 || id === null) return void 0;
2883
- const c = getCap(map, id);
2884
- return (_a = c == null ? void 0 : c.rate) != null ? _a : void 0;
2981
+ return (_b = (_a = getCap(map, id)) == null ? void 0 : _a.rate) != null ? _b : void 0;
2885
2982
  }
2886
- function passesRate(policy, primaryRate, candRate) {
2887
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
2983
+ function passesRate(policy, primaryRate, candidateRate) {
2984
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
2888
2985
  return false;
2889
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
2986
+ }
2987
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
2890
2988
  return false;
2891
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
2989
+ }
2990
+ return passesRatePolicy(
2991
+ normalizeRatePolicy(policy),
2992
+ primaryRate,
2993
+ candidateRate
2994
+ );
2892
2995
  }
2893
2996
  function getCap(map, id) {
2894
2997
  return getServiceCapability(map, id);
2895
2998
  }
2896
- function isCapFlagEnabled(cap, flagId) {
2999
+ function isCapFlagEnabled(capability, flagId) {
2897
3000
  var _a, _b;
2898
- const fromFlags = (_b = (_a = cap.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
3001
+ const fromFlags = (_b = (_a = capability.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
2899
3002
  if (fromFlags === true) return true;
2900
3003
  if (fromFlags === false) return false;
2901
- const legacy = cap[flagId];
3004
+ const legacy = capability[flagId];
2902
3005
  return legacy === true;
2903
3006
  }
2904
- function satisfiesTagConstraints(tagId, ctx, cap) {
2905
- const tag = ctx.props.filters.find((t) => t.id === tagId);
2906
- const eff = tag == null ? void 0 : tag.constraints;
2907
- if (!eff) return true;
2908
- for (const [key, value] of Object.entries(eff)) {
2909
- if (value === true && !isCapFlagEnabled(cap, key)) {
3007
+ function satisfiesTagConstraints(tagId, ctx, capability) {
3008
+ const tag = ctx.props.filters.find((item) => item.id === tagId);
3009
+ const effectiveConstraints2 = tag == null ? void 0 : tag.constraints;
3010
+ if (!effectiveConstraints2) return true;
3011
+ for (const [key, value] of Object.entries(effectiveConstraints2)) {
3012
+ if (value === true && !isCapFlagEnabled(capability, key)) {
2910
3013
  return false;
2911
3014
  }
2912
3015
  }
2913
3016
  return true;
2914
3017
  }
2915
3018
  function primaryForNode(props, nodeId) {
2916
- const tag = props.filters.find((t) => t.id === nodeId);
3019
+ const tag = props.filters.find((item) => item.id === nodeId);
2917
3020
  if (tag) {
2918
3021
  return { primary: tag.service_id, tagContexts: [tag.id] };
2919
3022
  }
2920
3023
  const field = props.fields.find(
2921
- (f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId)
3024
+ (item) => Array.isArray(item.options) && item.options.some((option2) => option2.id === nodeId)
2922
3025
  );
2923
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
2924
- const opt = field.options.find((o) => o.id === nodeId);
2925
- const contexts = bindIdsToArray(field.bind_id);
2926
- return { primary: opt.service_id, tagContexts: contexts };
3026
+ if (!field) {
3027
+ return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3028
+ }
3029
+ const option = field.options.find((item) => item.id === nodeId);
3030
+ return {
3031
+ primary: option.service_id,
3032
+ tagContexts: bindIdsToArray(field.bind_id)
3033
+ };
2927
3034
  }
2928
3035
  function bindIdsToArray(bind) {
2929
3036
  if (!bind) return [];
@@ -2933,43 +3040,54 @@ function getEligibleFallbacks(params) {
2933
3040
  var _a, _b, _c, _d, _e, _f;
2934
3041
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2935
3042
  const { primary, nodeId, tagId, services } = params;
2936
- const fb = (_b = params.fallbacks) != null ? _b : {};
2937
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
2938
- excludes.add(String(primary));
2939
- const unique = (_d = params.unique) != null ? _d : true;
2940
- const lists = [];
2941
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
2942
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
2943
- if (!lists.length) return [];
3043
+ const excludes = /* @__PURE__ */ new Set();
3044
+ for (const ref of (_b = params.exclude) != null ? _b : []) {
3045
+ addComparableServiceRef(excludes, services, ref);
3046
+ }
3047
+ addComparableServiceRef(excludes, services, primary);
3048
+ const source = (_c = params.source) != null ? _c : "registered";
3049
+ const candidateLists = source === "all_services" ? [listServicePoolCandidates(services)] : listRegisteredFallbackCandidates(
3050
+ (_d = params.fallbacks) != null ? _d : {},
3051
+ primary,
3052
+ nodeId,
3053
+ services
3054
+ );
3055
+ if (!candidateLists.length) return [];
2944
3056
  const primaryRate = rateOf(services, primary);
2945
3057
  const seen = /* @__PURE__ */ new Set();
2946
3058
  const eligible = [];
2947
- for (const list of lists) {
2948
- for (const cand of list) {
2949
- const key = String(cand);
2950
- if (excludes.has(key)) continue;
2951
- if (unique && seen.has(key)) continue;
2952
- seen.add(key);
2953
- const cap = getCap(services, cand);
2954
- if (!cap) continue;
2955
- if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
3059
+ for (const list of candidateLists) {
3060
+ for (const candidate of list) {
3061
+ if (hasComparableServiceRef(excludes, services, candidate)) continue;
3062
+ const capability = getCap(services, candidate);
3063
+ if (!capability) continue;
3064
+ const candidateId = (_e = getServiceCapabilityCanonicalRef(services, candidate)) != null ? _e : candidate;
3065
+ const candidateIdentity = getComparableServiceRefKey(
3066
+ services,
3067
+ candidateId
3068
+ );
3069
+ if (((_f = params.unique) != null ? _f : true) && seen.has(candidateIdentity)) continue;
3070
+ seen.add(candidateIdentity);
3071
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
3072
+ continue;
3073
+ }
2956
3074
  if (s.requireConstraintFit && tagId) {
2957
- const ok = satisfiesTagConstraints(
3075
+ const fitsConstraints = satisfiesTagConstraints(
2958
3076
  tagId,
2959
3077
  { props: params.props, services },
2960
- cap
3078
+ capability
2961
3079
  );
2962
- if (!ok) continue;
3080
+ if (!fitsConstraints) continue;
2963
3081
  }
2964
- eligible.push(cand);
3082
+ eligible.push(candidateId);
2965
3083
  }
2966
3084
  }
2967
3085
  if (s.selectionStrategy === "cheapest") {
2968
- eligible.sort((a, b) => {
3086
+ eligible.sort((left, right) => {
2969
3087
  var _a2, _b2;
2970
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
2971
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
2972
- return ra - rb;
3088
+ const leftRate = (_a2 = rateOf(services, left)) != null ? _a2 : Infinity;
3089
+ const rightRate = (_b2 = rateOf(services, right)) != null ? _b2 : Infinity;
3090
+ return leftRate - rightRate;
2973
3091
  });
2974
3092
  }
2975
3093
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -2977,10 +3095,104 @@ function getEligibleFallbacks(params) {
2977
3095
  }
2978
3096
  return eligible;
2979
3097
  }
3098
+ function getAssignedServiceIds(params) {
3099
+ var _a, _b, _c, _d, _e;
3100
+ const seen = /* @__PURE__ */ new Set();
3101
+ const out = [];
3102
+ const push = (value) => {
3103
+ if (!isValidServiceIdRef(value)) return;
3104
+ const key = String(value);
3105
+ if (seen.has(key)) return;
3106
+ seen.add(key);
3107
+ out.push(value);
3108
+ };
3109
+ const props = params.props;
3110
+ if (props) {
3111
+ for (const tag of (_a = props.filters) != null ? _a : []) {
3112
+ push(tag.service_id);
3113
+ }
3114
+ for (const field of (_b = props.fields) != null ? _b : []) {
3115
+ const fieldService = field.service_id;
3116
+ if (field.button === true) {
3117
+ push(fieldService);
3118
+ }
3119
+ for (const option of (_c = field.options) != null ? _c : []) {
3120
+ if (option.pricing_role === "utility") continue;
3121
+ push(option.service_id);
3122
+ }
3123
+ }
3124
+ }
3125
+ const snapshot = params.snapshot;
3126
+ if (snapshot) {
3127
+ for (const serviceId of (_d = snapshot.services) != null ? _d : []) {
3128
+ push(serviceId);
3129
+ }
3130
+ for (const list of Object.values((_e = snapshot.serviceMap) != null ? _e : {})) {
3131
+ for (const serviceId of list != null ? list : []) {
3132
+ push(serviceId);
3133
+ }
3134
+ }
3135
+ }
3136
+ return out;
3137
+ }
2980
3138
  function getFallbackRegistrationInfo(props, nodeId) {
2981
3139
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2982
3140
  return { primary, tagContexts };
2983
3141
  }
3142
+ function listRegisteredFallbackCandidates(fallbacks, primary, nodeId, services) {
3143
+ var _a, _b;
3144
+ const lists = [];
3145
+ if (nodeId && ((_a = fallbacks.nodes) == null ? void 0 : _a[nodeId])) {
3146
+ lists.push(fallbacks.nodes[nodeId]);
3147
+ }
3148
+ for (const [registeredPrimary, list] of Object.entries((_b = fallbacks.global) != null ? _b : {})) {
3149
+ if (!isMatchingServiceRef(services, registeredPrimary, primary)) continue;
3150
+ lists.push(list);
3151
+ }
3152
+ return lists;
3153
+ }
3154
+ function listServicePoolCandidates(services) {
3155
+ const seen = /* @__PURE__ */ new Set();
3156
+ const out = [];
3157
+ for (const [key, capability] of Object.entries(services != null ? services : {})) {
3158
+ const candidate = getServicePoolCandidateId(key, capability);
3159
+ if (!isValidServiceIdRef(candidate)) continue;
3160
+ const identity = getComparableServiceRefKey(services, candidate);
3161
+ if (seen.has(identity)) continue;
3162
+ seen.add(identity);
3163
+ out.push(candidate);
3164
+ }
3165
+ return out;
3166
+ }
3167
+ function getServicePoolCandidateId(key, capability) {
3168
+ var _a;
3169
+ return (_a = getServiceCapabilityCanonicalRef({ [key]: capability }, key)) != null ? _a : key;
3170
+ }
3171
+ function addComparableServiceRef(target, services, value) {
3172
+ for (const ref of getComparableServiceRefs(services, value)) {
3173
+ target.add(ref);
3174
+ }
3175
+ }
3176
+ function hasComparableServiceRef(target, services, value) {
3177
+ return getComparableServiceRefs(services, value).some((ref) => target.has(ref));
3178
+ }
3179
+ function getComparableServiceRefKey(services, value) {
3180
+ if (!isValidServiceIdRef(value)) return "";
3181
+ const canonical = getServiceCapabilityCanonicalRef(services, value);
3182
+ return String(canonical != null ? canonical : value);
3183
+ }
3184
+ function getComparableServiceRefs(services, value) {
3185
+ if (!isValidServiceIdRef(value)) return [];
3186
+ const aliases = getServiceCapabilityAliases(services, value);
3187
+ if (!aliases.length) {
3188
+ return [String(value)];
3189
+ }
3190
+ return aliases.map((ref) => String(ref));
3191
+ }
3192
+ function isMatchingServiceRef(services, left, right) {
3193
+ if (!services) return String(left) === String(right);
3194
+ return isSameServiceCapabilityRef(services, left, right);
3195
+ }
2984
3196
 
2985
3197
  // src/core/tag-relations.ts
2986
3198
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -4519,6 +4731,7 @@ function createFallbackEditor(options = {}) {
4519
4731
  const original = cloneFallbacks(options.fallbacks);
4520
4732
  let current = cloneFallbacks(options.fallbacks);
4521
4733
  const props = options.props;
4734
+ const snapshot = options.snapshot;
4522
4735
  const services = (_a = options.services) != null ? _a : {};
4523
4736
  const settings = (_b = options.settings) != null ? _b : {};
4524
4737
  function state() {
@@ -4613,7 +4826,11 @@ function createFallbackEditor(options = {}) {
4613
4826
  const allowed2 = [];
4614
4827
  for (const candidate of normalized) {
4615
4828
  const reasons = [];
4616
- if (String(candidate) === String(context.primary)) {
4829
+ if (isSameServiceCapabilityRef(
4830
+ services,
4831
+ candidate,
4832
+ context.primary
4833
+ )) {
4617
4834
  reasons.push("self_reference");
4618
4835
  }
4619
4836
  if (reasons.length) {
@@ -4701,7 +4918,19 @@ function createFallbackEditor(options = {}) {
4701
4918
  return writeScope(context, []);
4702
4919
  }
4703
4920
  function eligible(context, opt) {
4921
+ var _a2, _b2;
4704
4922
  if (!props) return [];
4923
+ const source = (_a2 = opt == null ? void 0 : opt.source) != null ? _a2 : "all_services";
4924
+ const exclude = normalizeCandidateList(
4925
+ [
4926
+ ...(_b2 = opt == null ? void 0 : opt.exclude) != null ? _b2 : [],
4927
+ ...source === "all_services" ? [
4928
+ ...getAssignedServiceIds({ props, snapshot }),
4929
+ ...getScope(context)
4930
+ ] : []
4931
+ ],
4932
+ true
4933
+ );
4705
4934
  if (context.scope === "global") {
4706
4935
  return getEligibleFallbacks({
4707
4936
  primary: context.primary,
@@ -4709,9 +4938,10 @@ function createFallbackEditor(options = {}) {
4709
4938
  fallbacks: current,
4710
4939
  settings,
4711
4940
  props,
4712
- exclude: opt == null ? void 0 : opt.exclude,
4941
+ exclude,
4713
4942
  unique: opt == null ? void 0 : opt.unique,
4714
- limit: opt == null ? void 0 : opt.limit
4943
+ limit: opt == null ? void 0 : opt.limit,
4944
+ source
4715
4945
  });
4716
4946
  }
4717
4947
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -4719,14 +4949,19 @@ function createFallbackEditor(options = {}) {
4719
4949
  return getEligibleFallbacks({
4720
4950
  primary: info.primary,
4721
4951
  nodeId: context.nodeId,
4722
- tagId: info.tagContexts[0],
4952
+ tagId: resolveNodeTagContext({
4953
+ nodeId: context.nodeId,
4954
+ snapshot,
4955
+ fallbackTagContexts: info.tagContexts
4956
+ }),
4723
4957
  services,
4724
4958
  fallbacks: current,
4725
4959
  settings,
4726
4960
  props,
4727
- exclude: opt == null ? void 0 : opt.exclude,
4961
+ exclude,
4728
4962
  unique: opt == null ? void 0 : opt.unique,
4729
- limit: opt == null ? void 0 : opt.limit
4963
+ limit: opt == null ? void 0 : opt.limit,
4964
+ source
4730
4965
  });
4731
4966
  }
4732
4967
  function writeScope(context, nextList) {
@@ -4784,14 +5019,14 @@ function sameFallbacks(a, b) {
4784
5019
  function normalizeCandidateList(input, preserveOrder) {
4785
5020
  const out = [];
4786
5021
  for (const item of input != null ? input : []) {
4787
- if (!isValidServiceIdRef(item)) continue;
5022
+ if (!isValidServiceIdRef2(item)) continue;
4788
5023
  const exists = out.some((x) => String(x) === String(item));
4789
5024
  if (exists) continue;
4790
5025
  out.push(item);
4791
5026
  }
4792
5027
  return preserveOrder ? out : out;
4793
5028
  }
4794
- function isValidServiceIdRef(value) {
5029
+ function isValidServiceIdRef2(value) {
4795
5030
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
4796
5031
  }
4797
5032
  function clamp(n, min, max) {
@@ -4800,7 +5035,7 @@ function clamp(n, min, max) {
4800
5035
  function getNodeRegistrationInfo(props, nodeId) {
4801
5036
  const tag = props.filters.find((t) => t.id === nodeId);
4802
5037
  if (tag) {
4803
- if (!isValidServiceIdRef(tag.service_id)) {
5038
+ if (!isValidServiceIdRef2(tag.service_id)) {
4804
5039
  return { ok: false, reasons: ["no_primary"] };
4805
5040
  }
4806
5041
  return {
@@ -4813,7 +5048,7 @@ function getNodeRegistrationInfo(props, nodeId) {
4813
5048
  if (!hit) {
4814
5049
  return { ok: false, reasons: ["node_not_found"] };
4815
5050
  }
4816
- if (!isValidServiceIdRef(hit.option.service_id)) {
5051
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
4817
5052
  return { ok: false, reasons: ["no_primary"] };
4818
5053
  }
4819
5054
  return {
@@ -4835,6 +5070,15 @@ function bindIdsToArray2(v) {
4835
5070
  if (Array.isArray(v)) return v.filter(Boolean);
4836
5071
  return v ? [v] : [];
4837
5072
  }
5073
+ function resolveNodeTagContext(params) {
5074
+ var _a, _b, _c;
5075
+ const nodeContexts = (_c = (_b = (_a = params.snapshot) == null ? void 0 : _a.meta) == null ? void 0 : _b.context) == null ? void 0 : _c.nodeContexts;
5076
+ if (nodeContexts && Object.prototype.hasOwnProperty.call(nodeContexts, params.nodeId)) {
5077
+ const tagId = nodeContexts[params.nodeId];
5078
+ return typeof tagId === "string" && tagId.trim().length > 0 ? tagId : void 0;
5079
+ }
5080
+ return params.fallbackTagContexts[0];
5081
+ }
4838
5082
  function mapDiagReason(reason) {
4839
5083
  switch (String(reason)) {
4840
5084
  case "unknown_service":
@@ -4861,6 +5105,7 @@ function mapDiagReason(reason) {
4861
5105
  createFallbackEditor,
4862
5106
  createNodeIndex,
4863
5107
  filterServicesForVisibleGroup,
5108
+ getAssignedServiceIds,
4864
5109
  getEligibleFallbacks,
4865
5110
  getFallbackRegistrationInfo,
4866
5111
  normalise,