@timeax/digital-service-engine 0.2.1 → 0.2.3

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.
@@ -1192,6 +1192,9 @@ function toFiniteNumber(v) {
1192
1192
  const n = Number(v);
1193
1193
  return Number.isFinite(n) ? n : NaN;
1194
1194
  }
1195
+ function isValidServiceIdRef(value) {
1196
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
1197
+ }
1195
1198
  function constraintFitOk(svcMap, candidate, constraints) {
1196
1199
  const cap = getServiceCapability(svcMap, candidate);
1197
1200
  if (!cap) return false;
@@ -1200,18 +1203,32 @@ function constraintFitOk(svcMap, candidate, constraints) {
1200
1203
  return !(constraints.cancel === true && !cap.cancel);
1201
1204
  }
1202
1205
  function getServiceCapability(svcMap, candidate) {
1203
- if (candidate === void 0 || candidate === null) return void 0;
1204
- const direct = svcMap[candidate];
1205
- if (direct) return direct;
1206
- const byString = svcMap[String(candidate)];
1207
- if (byString) return byString;
1208
- if (typeof candidate === "string") {
1209
- const maybeNumber = Number(candidate);
1210
- if (Number.isFinite(maybeNumber)) {
1211
- return svcMap[maybeNumber];
1212
- }
1206
+ var _a;
1207
+ return (_a = getServiceCapabilityEntry(svcMap, candidate)) == null ? void 0 : _a.capability;
1208
+ }
1209
+ function getServiceCapabilityCanonicalRef(svcMap, candidate) {
1210
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1211
+ if (!entry) return void 0;
1212
+ return getCanonicalServiceRef(entry.key, entry.capability);
1213
+ }
1214
+ function getServiceCapabilityAliases(svcMap, candidate) {
1215
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1216
+ if (!entry) return [];
1217
+ return collectServiceRefAliases(entry.key, entry.capability);
1218
+ }
1219
+ function isSameServiceCapabilityRef(svcMap, left, right) {
1220
+ if (!isValidServiceIdRef(left) || !isValidServiceIdRef(right)) return false;
1221
+ const leftAliases = new Set(
1222
+ getServiceCapabilityAliases(svcMap, left).map((value) => String(value))
1223
+ );
1224
+ if (!leftAliases.size) {
1225
+ leftAliases.add(String(left));
1213
1226
  }
1214
- return void 0;
1227
+ const rightAliases = getServiceCapabilityAliases(svcMap, right);
1228
+ if (!rightAliases.length) {
1229
+ return leftAliases.has(String(right));
1230
+ }
1231
+ return rightAliases.some((value) => leftAliases.has(String(value)));
1215
1232
  }
1216
1233
  function normalizeRatePolicy(policy) {
1217
1234
  var _a;
@@ -1247,6 +1264,74 @@ function rateOk(svcMap, candidate, primary, policy) {
1247
1264
  if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
1248
1265
  return passesRatePolicy(policy.ratePolicy, pRate, cRate);
1249
1266
  }
1267
+ function getServiceCapabilityEntry(svcMap, candidate) {
1268
+ if (candidate === void 0 || candidate === null) return void 0;
1269
+ const direct = svcMap[candidate];
1270
+ if (direct) {
1271
+ return { key: String(candidate), capability: direct };
1272
+ }
1273
+ const byString = svcMap[String(candidate)];
1274
+ if (byString) {
1275
+ return { key: String(candidate), capability: byString };
1276
+ }
1277
+ if (typeof candidate === "string") {
1278
+ const maybeNumber = Number(candidate);
1279
+ if (Number.isFinite(maybeNumber)) {
1280
+ const byNumber = svcMap[maybeNumber];
1281
+ if (byNumber) {
1282
+ return { key: String(maybeNumber), capability: byNumber };
1283
+ }
1284
+ }
1285
+ }
1286
+ const target = String(candidate);
1287
+ for (const [key, capability] of Object.entries(svcMap != null ? svcMap : {})) {
1288
+ if (collectServiceRefAliases(key, capability).some(
1289
+ (alias) => String(alias) === target
1290
+ )) {
1291
+ return { key, capability };
1292
+ }
1293
+ }
1294
+ return void 0;
1295
+ }
1296
+ function collectServiceRefAliases(key, capability) {
1297
+ const out = [];
1298
+ const seen = /* @__PURE__ */ new Set();
1299
+ const push = (value) => {
1300
+ if (!isValidServiceIdRef(value)) return;
1301
+ const normalized = normalizeServiceRef(value);
1302
+ if (!normalized) return;
1303
+ const aliasKey = String(normalized);
1304
+ if (seen.has(aliasKey)) return;
1305
+ seen.add(aliasKey);
1306
+ out.push(normalized);
1307
+ };
1308
+ push(getCanonicalServiceRef(key, capability));
1309
+ push(capability.service);
1310
+ push(capability.key);
1311
+ push(capability.id);
1312
+ return out;
1313
+ }
1314
+ function getCanonicalServiceRef(key, capability) {
1315
+ const explicitRefs = [capability.service, capability.key, capability.id];
1316
+ for (const ref of explicitRefs) {
1317
+ if (!isValidServiceIdRef(ref)) continue;
1318
+ if (String(ref) === key) {
1319
+ return ref;
1320
+ }
1321
+ }
1322
+ return normalizeServiceRef(key);
1323
+ }
1324
+ function normalizeServiceRef(value) {
1325
+ if (!isValidServiceIdRef(value)) return void 0;
1326
+ if (typeof value === "number") return value;
1327
+ const trimmed = value.trim();
1328
+ if (!trimmed) return void 0;
1329
+ const asNumber = Number(trimmed);
1330
+ if (Number.isFinite(asNumber) && String(asNumber) === trimmed) {
1331
+ return asNumber;
1332
+ }
1333
+ return trimmed;
1334
+ }
1250
1335
 
1251
1336
  // src/core/validate/steps/rates.ts
1252
1337
  function validateRates(v) {
@@ -2699,27 +2784,42 @@ var DEFAULT_SETTINGS = {
2699
2784
  mode: "strict"
2700
2785
  };
2701
2786
  function resolveServiceFallback(params) {
2702
- var _a, _b, _c, _d, _e;
2787
+ var _a, _b;
2703
2788
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2704
2789
  const { primary, nodeId, tagId, services } = params;
2705
- const fb = (_b = params.fallbacks) != null ? _b : {};
2706
- const tried = [];
2707
- const lists = [];
2708
- if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
2709
- if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
2790
+ const fallbackLists = listRegisteredFallbackCandidates(
2791
+ (_b = params.fallbacks) != null ? _b : {},
2792
+ primary,
2793
+ nodeId,
2794
+ services
2795
+ );
2796
+ const tried = /* @__PURE__ */ new Set();
2710
2797
  const primaryRate = rateOf(services, primary);
2711
- for (const list of lists) {
2712
- for (const cand of list) {
2713
- if (tried.includes(cand)) continue;
2714
- tried.push(cand);
2715
- const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
2716
- if (!candCap) continue;
2717
- if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
2798
+ for (const list of fallbackLists) {
2799
+ for (const candidate of list) {
2800
+ const candidateIdentity = getComparableServiceRefKey(
2801
+ services,
2802
+ candidate
2803
+ );
2804
+ if (tried.has(candidateIdentity)) continue;
2805
+ tried.add(candidateIdentity);
2806
+ const capability = getCap(services, candidate);
2807
+ if (!capability) continue;
2808
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2809
+ continue;
2810
+ }
2811
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
2812
+ continue;
2813
+ }
2718
2814
  if (s.requireConstraintFit && tagId) {
2719
- const ok = satisfiesTagConstraints(tagId, params, candCap);
2720
- if (!ok) continue;
2815
+ const fitsConstraints = satisfiesTagConstraints(
2816
+ tagId,
2817
+ params,
2818
+ capability
2819
+ );
2820
+ if (!fitsConstraints) continue;
2721
2821
  }
2722
- return cand;
2822
+ return candidate;
2723
2823
  }
2724
2824
  }
2725
2825
  return null;
@@ -2729,7 +2829,7 @@ function collectFailedFallbacks(props, services, settings) {
2729
2829
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
2730
2830
  const out = [];
2731
2831
  const fb = (_a = props.fallbacks) != null ? _a : {};
2732
- const primaryRate = (p) => rateOf(services, p);
2832
+ const primaryRate = (primary) => rateOf(services, primary);
2733
2833
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
2734
2834
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2735
2835
  if (!primary) {
@@ -2742,34 +2842,34 @@ function collectFailedFallbacks(props, services, settings) {
2742
2842
  });
2743
2843
  continue;
2744
2844
  }
2745
- for (const cand of list) {
2746
- const cap = getCap(services, cand);
2747
- if (!cap) {
2845
+ for (const candidate of list) {
2846
+ const capability = getCap(services, candidate);
2847
+ if (!capability) {
2748
2848
  out.push({
2749
2849
  scope: "node",
2750
2850
  nodeId,
2751
2851
  primary,
2752
- candidate: cand,
2852
+ candidate,
2753
2853
  reason: "unknown_service"
2754
2854
  });
2755
2855
  continue;
2756
2856
  }
2757
- if (String(cand) === String(primary)) {
2857
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2758
2858
  out.push({
2759
2859
  scope: "node",
2760
2860
  nodeId,
2761
2861
  primary,
2762
- candidate: cand,
2862
+ candidate,
2763
2863
  reason: "cycle"
2764
2864
  });
2765
2865
  continue;
2766
2866
  }
2767
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2867
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2768
2868
  out.push({
2769
2869
  scope: "node",
2770
2870
  nodeId,
2771
2871
  primary,
2772
- candidate: cand,
2872
+ candidate,
2773
2873
  reason: "rate_violation"
2774
2874
  });
2775
2875
  continue;
@@ -2779,58 +2879,55 @@ function collectFailedFallbacks(props, services, settings) {
2779
2879
  scope: "node",
2780
2880
  nodeId,
2781
2881
  primary,
2782
- candidate: cand,
2882
+ candidate,
2783
2883
  reason: "no_tag_context"
2784
2884
  });
2785
2885
  continue;
2786
2886
  }
2787
- let anyPass = false;
2788
- let anyFail = false;
2789
2887
  for (const tagId of tagContexts) {
2790
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
2791
- if (ok) anyPass = true;
2792
- else {
2793
- anyFail = true;
2794
- out.push({
2795
- scope: "node",
2796
- nodeId,
2797
- primary,
2798
- candidate: cand,
2799
- tagContext: tagId,
2800
- reason: "constraint_mismatch"
2801
- });
2802
- }
2888
+ const fitsConstraints = s.requireConstraintFit ? satisfiesTagConstraints(
2889
+ tagId,
2890
+ { services, props },
2891
+ capability
2892
+ ) : true;
2893
+ if (fitsConstraints) continue;
2894
+ out.push({
2895
+ scope: "node",
2896
+ nodeId,
2897
+ primary,
2898
+ candidate,
2899
+ tagContext: tagId,
2900
+ reason: "constraint_mismatch"
2901
+ });
2803
2902
  }
2804
- void anyPass;
2805
- void anyFail;
2806
2903
  }
2807
2904
  }
2808
2905
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
2809
- for (const cand of list) {
2810
- const cap = getCap(services, cand);
2811
- if (!cap) {
2906
+ for (const candidate of list) {
2907
+ const capability = getCap(services, candidate);
2908
+ if (!capability) {
2812
2909
  out.push({
2813
2910
  scope: "global",
2814
2911
  primary,
2815
- candidate: cand,
2912
+ candidate,
2816
2913
  reason: "unknown_service"
2817
2914
  });
2818
2915
  continue;
2819
2916
  }
2820
- if (String(cand) === String(primary)) {
2917
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2821
2918
  out.push({
2822
2919
  scope: "global",
2823
2920
  primary,
2824
- candidate: cand,
2921
+ candidate,
2825
2922
  reason: "cycle"
2826
2923
  });
2827
2924
  continue;
2828
2925
  }
2829
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2926
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2830
2927
  out.push({
2831
2928
  scope: "global",
2832
2929
  primary,
2833
- candidate: cand,
2930
+ candidate,
2834
2931
  reason: "rate_violation"
2835
2932
  });
2836
2933
  }
@@ -2839,52 +2936,61 @@ function collectFailedFallbacks(props, services, settings) {
2839
2936
  return out;
2840
2937
  }
2841
2938
  function rateOf(map, id) {
2842
- var _a;
2939
+ var _a, _b;
2843
2940
  if (id === void 0 || id === null) return void 0;
2844
- const c = getCap(map, id);
2845
- return (_a = c == null ? void 0 : c.rate) != null ? _a : void 0;
2941
+ return (_b = (_a = getCap(map, id)) == null ? void 0 : _a.rate) != null ? _b : void 0;
2846
2942
  }
2847
- function passesRate(policy, primaryRate, candRate) {
2848
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
2943
+ function passesRate(policy, primaryRate, candidateRate) {
2944
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
2849
2945
  return false;
2850
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
2946
+ }
2947
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
2851
2948
  return false;
2852
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
2949
+ }
2950
+ return passesRatePolicy(
2951
+ normalizeRatePolicy(policy),
2952
+ primaryRate,
2953
+ candidateRate
2954
+ );
2853
2955
  }
2854
2956
  function getCap(map, id) {
2855
2957
  return getServiceCapability(map, id);
2856
2958
  }
2857
- function isCapFlagEnabled(cap, flagId) {
2959
+ function isCapFlagEnabled(capability, flagId) {
2858
2960
  var _a, _b;
2859
- const fromFlags = (_b = (_a = cap.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
2961
+ const fromFlags = (_b = (_a = capability.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
2860
2962
  if (fromFlags === true) return true;
2861
2963
  if (fromFlags === false) return false;
2862
- const legacy = cap[flagId];
2964
+ const legacy = capability[flagId];
2863
2965
  return legacy === true;
2864
2966
  }
2865
- function satisfiesTagConstraints(tagId, ctx, cap) {
2866
- const tag = ctx.props.filters.find((t) => t.id === tagId);
2867
- const eff = tag == null ? void 0 : tag.constraints;
2868
- if (!eff) return true;
2869
- for (const [key, value] of Object.entries(eff)) {
2870
- if (value === true && !isCapFlagEnabled(cap, key)) {
2967
+ function satisfiesTagConstraints(tagId, ctx, capability) {
2968
+ const tag = ctx.props.filters.find((item) => item.id === tagId);
2969
+ const effectiveConstraints2 = tag == null ? void 0 : tag.constraints;
2970
+ if (!effectiveConstraints2) return true;
2971
+ for (const [key, value] of Object.entries(effectiveConstraints2)) {
2972
+ if (value === true && !isCapFlagEnabled(capability, key)) {
2871
2973
  return false;
2872
2974
  }
2873
2975
  }
2874
2976
  return true;
2875
2977
  }
2876
2978
  function primaryForNode(props, nodeId) {
2877
- const tag = props.filters.find((t) => t.id === nodeId);
2979
+ const tag = props.filters.find((item) => item.id === nodeId);
2878
2980
  if (tag) {
2879
2981
  return { primary: tag.service_id, tagContexts: [tag.id] };
2880
2982
  }
2881
2983
  const field = props.fields.find(
2882
- (f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId)
2984
+ (item) => Array.isArray(item.options) && item.options.some((option2) => option2.id === nodeId)
2883
2985
  );
2884
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
2885
- const opt = field.options.find((o) => o.id === nodeId);
2886
- const contexts = bindIdsToArray(field.bind_id);
2887
- return { primary: opt.service_id, tagContexts: contexts };
2986
+ if (!field) {
2987
+ return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
2988
+ }
2989
+ const option = field.options.find((item) => item.id === nodeId);
2990
+ return {
2991
+ primary: option.service_id,
2992
+ tagContexts: bindIdsToArray(field.bind_id)
2993
+ };
2888
2994
  }
2889
2995
  function bindIdsToArray(bind) {
2890
2996
  if (!bind) return [];
@@ -2894,43 +3000,54 @@ function getEligibleFallbacks(params) {
2894
3000
  var _a, _b, _c, _d, _e, _f;
2895
3001
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2896
3002
  const { primary, nodeId, tagId, services } = params;
2897
- const fb = (_b = params.fallbacks) != null ? _b : {};
2898
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
2899
- excludes.add(String(primary));
2900
- const unique = (_d = params.unique) != null ? _d : true;
2901
- const lists = [];
2902
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
2903
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
2904
- if (!lists.length) return [];
3003
+ const excludes = /* @__PURE__ */ new Set();
3004
+ for (const ref of (_b = params.exclude) != null ? _b : []) {
3005
+ addComparableServiceRef(excludes, services, ref);
3006
+ }
3007
+ addComparableServiceRef(excludes, services, primary);
3008
+ const source = (_c = params.source) != null ? _c : "registered";
3009
+ const candidateLists = source === "all_services" ? [listServicePoolCandidates(services)] : listRegisteredFallbackCandidates(
3010
+ (_d = params.fallbacks) != null ? _d : {},
3011
+ primary,
3012
+ nodeId,
3013
+ services
3014
+ );
3015
+ if (!candidateLists.length) return [];
2905
3016
  const primaryRate = rateOf(services, primary);
2906
3017
  const seen = /* @__PURE__ */ new Set();
2907
3018
  const eligible = [];
2908
- for (const list of lists) {
2909
- for (const cand of list) {
2910
- const key = String(cand);
2911
- if (excludes.has(key)) continue;
2912
- if (unique && seen.has(key)) continue;
2913
- seen.add(key);
2914
- const cap = getCap(services, cand);
2915
- if (!cap) continue;
2916
- if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
3019
+ for (const list of candidateLists) {
3020
+ for (const candidate of list) {
3021
+ if (hasComparableServiceRef(excludes, services, candidate)) continue;
3022
+ const capability = getCap(services, candidate);
3023
+ if (!capability) continue;
3024
+ const candidateId = (_e = getServiceCapabilityCanonicalRef(services, candidate)) != null ? _e : candidate;
3025
+ const candidateIdentity = getComparableServiceRefKey(
3026
+ services,
3027
+ candidateId
3028
+ );
3029
+ if (((_f = params.unique) != null ? _f : true) && seen.has(candidateIdentity)) continue;
3030
+ seen.add(candidateIdentity);
3031
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
3032
+ continue;
3033
+ }
2917
3034
  if (s.requireConstraintFit && tagId) {
2918
- const ok = satisfiesTagConstraints(
3035
+ const fitsConstraints = satisfiesTagConstraints(
2919
3036
  tagId,
2920
3037
  { props: params.props, services },
2921
- cap
3038
+ capability
2922
3039
  );
2923
- if (!ok) continue;
3040
+ if (!fitsConstraints) continue;
2924
3041
  }
2925
- eligible.push(cand);
3042
+ eligible.push(candidateId);
2926
3043
  }
2927
3044
  }
2928
3045
  if (s.selectionStrategy === "cheapest") {
2929
- eligible.sort((a, b) => {
3046
+ eligible.sort((left, right) => {
2930
3047
  var _a2, _b2;
2931
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
2932
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
2933
- return ra - rb;
3048
+ const leftRate = (_a2 = rateOf(services, left)) != null ? _a2 : Infinity;
3049
+ const rightRate = (_b2 = rateOf(services, right)) != null ? _b2 : Infinity;
3050
+ return leftRate - rightRate;
2934
3051
  });
2935
3052
  }
2936
3053
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -2938,10 +3055,104 @@ function getEligibleFallbacks(params) {
2938
3055
  }
2939
3056
  return eligible;
2940
3057
  }
3058
+ function getAssignedServiceIds(params) {
3059
+ var _a, _b, _c, _d, _e;
3060
+ const seen = /* @__PURE__ */ new Set();
3061
+ const out = [];
3062
+ const push = (value) => {
3063
+ if (!isValidServiceIdRef(value)) return;
3064
+ const key = String(value);
3065
+ if (seen.has(key)) return;
3066
+ seen.add(key);
3067
+ out.push(value);
3068
+ };
3069
+ const props = params.props;
3070
+ if (props) {
3071
+ for (const tag of (_a = props.filters) != null ? _a : []) {
3072
+ push(tag.service_id);
3073
+ }
3074
+ for (const field of (_b = props.fields) != null ? _b : []) {
3075
+ const fieldService = field.service_id;
3076
+ if (field.button === true) {
3077
+ push(fieldService);
3078
+ }
3079
+ for (const option of (_c = field.options) != null ? _c : []) {
3080
+ if (option.pricing_role === "utility") continue;
3081
+ push(option.service_id);
3082
+ }
3083
+ }
3084
+ }
3085
+ const snapshot = params.snapshot;
3086
+ if (snapshot) {
3087
+ for (const serviceId of (_d = snapshot.services) != null ? _d : []) {
3088
+ push(serviceId);
3089
+ }
3090
+ for (const list of Object.values((_e = snapshot.serviceMap) != null ? _e : {})) {
3091
+ for (const serviceId of list != null ? list : []) {
3092
+ push(serviceId);
3093
+ }
3094
+ }
3095
+ }
3096
+ return out;
3097
+ }
2941
3098
  function getFallbackRegistrationInfo(props, nodeId) {
2942
3099
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2943
3100
  return { primary, tagContexts };
2944
3101
  }
3102
+ function listRegisteredFallbackCandidates(fallbacks, primary, nodeId, services) {
3103
+ var _a, _b;
3104
+ const lists = [];
3105
+ if (nodeId && ((_a = fallbacks.nodes) == null ? void 0 : _a[nodeId])) {
3106
+ lists.push(fallbacks.nodes[nodeId]);
3107
+ }
3108
+ for (const [registeredPrimary, list] of Object.entries((_b = fallbacks.global) != null ? _b : {})) {
3109
+ if (!isMatchingServiceRef(services, registeredPrimary, primary)) continue;
3110
+ lists.push(list);
3111
+ }
3112
+ return lists;
3113
+ }
3114
+ function listServicePoolCandidates(services) {
3115
+ const seen = /* @__PURE__ */ new Set();
3116
+ const out = [];
3117
+ for (const [key, capability] of Object.entries(services != null ? services : {})) {
3118
+ const candidate = getServicePoolCandidateId(key, capability);
3119
+ if (!isValidServiceIdRef(candidate)) continue;
3120
+ const identity = getComparableServiceRefKey(services, candidate);
3121
+ if (seen.has(identity)) continue;
3122
+ seen.add(identity);
3123
+ out.push(candidate);
3124
+ }
3125
+ return out;
3126
+ }
3127
+ function getServicePoolCandidateId(key, capability) {
3128
+ var _a;
3129
+ return (_a = getServiceCapabilityCanonicalRef({ [key]: capability }, key)) != null ? _a : key;
3130
+ }
3131
+ function addComparableServiceRef(target, services, value) {
3132
+ for (const ref of getComparableServiceRefs(services, value)) {
3133
+ target.add(ref);
3134
+ }
3135
+ }
3136
+ function hasComparableServiceRef(target, services, value) {
3137
+ return getComparableServiceRefs(services, value).some((ref) => target.has(ref));
3138
+ }
3139
+ function getComparableServiceRefKey(services, value) {
3140
+ if (!isValidServiceIdRef(value)) return "";
3141
+ const canonical = getServiceCapabilityCanonicalRef(services, value);
3142
+ return String(canonical != null ? canonical : value);
3143
+ }
3144
+ function getComparableServiceRefs(services, value) {
3145
+ if (!isValidServiceIdRef(value)) return [];
3146
+ const aliases = getServiceCapabilityAliases(services, value);
3147
+ if (!aliases.length) {
3148
+ return [String(value)];
3149
+ }
3150
+ return aliases.map((ref) => String(ref));
3151
+ }
3152
+ function isMatchingServiceRef(services, left, right) {
3153
+ if (!services) return String(left) === String(right);
3154
+ return isSameServiceCapabilityRef(services, left, right);
3155
+ }
2945
3156
 
2946
3157
  // src/core/tag-relations.ts
2947
3158
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -4480,6 +4691,7 @@ function createFallbackEditor(options = {}) {
4480
4691
  const original = cloneFallbacks(options.fallbacks);
4481
4692
  let current = cloneFallbacks(options.fallbacks);
4482
4693
  const props = options.props;
4694
+ const snapshot = options.snapshot;
4483
4695
  const services = (_a = options.services) != null ? _a : {};
4484
4696
  const settings = (_b = options.settings) != null ? _b : {};
4485
4697
  function state() {
@@ -4574,7 +4786,11 @@ function createFallbackEditor(options = {}) {
4574
4786
  const allowed2 = [];
4575
4787
  for (const candidate of normalized) {
4576
4788
  const reasons = [];
4577
- if (String(candidate) === String(context.primary)) {
4789
+ if (isSameServiceCapabilityRef(
4790
+ services,
4791
+ candidate,
4792
+ context.primary
4793
+ )) {
4578
4794
  reasons.push("self_reference");
4579
4795
  }
4580
4796
  if (reasons.length) {
@@ -4662,7 +4878,19 @@ function createFallbackEditor(options = {}) {
4662
4878
  return writeScope(context, []);
4663
4879
  }
4664
4880
  function eligible(context, opt) {
4881
+ var _a2, _b2;
4665
4882
  if (!props) return [];
4883
+ const source = (_a2 = opt == null ? void 0 : opt.source) != null ? _a2 : "all_services";
4884
+ const exclude = normalizeCandidateList(
4885
+ [
4886
+ ...(_b2 = opt == null ? void 0 : opt.exclude) != null ? _b2 : [],
4887
+ ...source === "all_services" ? [
4888
+ ...getAssignedServiceIds({ props, snapshot }),
4889
+ ...getScope(context)
4890
+ ] : []
4891
+ ],
4892
+ true
4893
+ );
4666
4894
  if (context.scope === "global") {
4667
4895
  return getEligibleFallbacks({
4668
4896
  primary: context.primary,
@@ -4670,9 +4898,10 @@ function createFallbackEditor(options = {}) {
4670
4898
  fallbacks: current,
4671
4899
  settings,
4672
4900
  props,
4673
- exclude: opt == null ? void 0 : opt.exclude,
4901
+ exclude,
4674
4902
  unique: opt == null ? void 0 : opt.unique,
4675
- limit: opt == null ? void 0 : opt.limit
4903
+ limit: opt == null ? void 0 : opt.limit,
4904
+ source
4676
4905
  });
4677
4906
  }
4678
4907
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -4680,14 +4909,19 @@ function createFallbackEditor(options = {}) {
4680
4909
  return getEligibleFallbacks({
4681
4910
  primary: info.primary,
4682
4911
  nodeId: context.nodeId,
4683
- tagId: info.tagContexts[0],
4912
+ tagId: resolveNodeTagContext({
4913
+ nodeId: context.nodeId,
4914
+ snapshot,
4915
+ fallbackTagContexts: info.tagContexts
4916
+ }),
4684
4917
  services,
4685
4918
  fallbacks: current,
4686
4919
  settings,
4687
4920
  props,
4688
- exclude: opt == null ? void 0 : opt.exclude,
4921
+ exclude,
4689
4922
  unique: opt == null ? void 0 : opt.unique,
4690
- limit: opt == null ? void 0 : opt.limit
4923
+ limit: opt == null ? void 0 : opt.limit,
4924
+ source
4691
4925
  });
4692
4926
  }
4693
4927
  function writeScope(context, nextList) {
@@ -4745,14 +4979,14 @@ function sameFallbacks(a, b) {
4745
4979
  function normalizeCandidateList(input, preserveOrder) {
4746
4980
  const out = [];
4747
4981
  for (const item of input != null ? input : []) {
4748
- if (!isValidServiceIdRef(item)) continue;
4982
+ if (!isValidServiceIdRef2(item)) continue;
4749
4983
  const exists = out.some((x) => String(x) === String(item));
4750
4984
  if (exists) continue;
4751
4985
  out.push(item);
4752
4986
  }
4753
4987
  return preserveOrder ? out : out;
4754
4988
  }
4755
- function isValidServiceIdRef(value) {
4989
+ function isValidServiceIdRef2(value) {
4756
4990
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
4757
4991
  }
4758
4992
  function clamp(n, min, max) {
@@ -4761,7 +4995,7 @@ function clamp(n, min, max) {
4761
4995
  function getNodeRegistrationInfo(props, nodeId) {
4762
4996
  const tag = props.filters.find((t) => t.id === nodeId);
4763
4997
  if (tag) {
4764
- if (!isValidServiceIdRef(tag.service_id)) {
4998
+ if (!isValidServiceIdRef2(tag.service_id)) {
4765
4999
  return { ok: false, reasons: ["no_primary"] };
4766
5000
  }
4767
5001
  return {
@@ -4774,7 +5008,7 @@ function getNodeRegistrationInfo(props, nodeId) {
4774
5008
  if (!hit) {
4775
5009
  return { ok: false, reasons: ["node_not_found"] };
4776
5010
  }
4777
- if (!isValidServiceIdRef(hit.option.service_id)) {
5011
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
4778
5012
  return { ok: false, reasons: ["no_primary"] };
4779
5013
  }
4780
5014
  return {
@@ -4796,6 +5030,15 @@ function bindIdsToArray2(v) {
4796
5030
  if (Array.isArray(v)) return v.filter(Boolean);
4797
5031
  return v ? [v] : [];
4798
5032
  }
5033
+ function resolveNodeTagContext(params) {
5034
+ var _a, _b, _c;
5035
+ const nodeContexts = (_c = (_b = (_a = params.snapshot) == null ? void 0 : _a.meta) == null ? void 0 : _b.context) == null ? void 0 : _c.nodeContexts;
5036
+ if (nodeContexts && Object.prototype.hasOwnProperty.call(nodeContexts, params.nodeId)) {
5037
+ const tagId = nodeContexts[params.nodeId];
5038
+ return typeof tagId === "string" && tagId.trim().length > 0 ? tagId : void 0;
5039
+ }
5040
+ return params.fallbackTagContexts[0];
5041
+ }
4799
5042
  function mapDiagReason(reason) {
4800
5043
  switch (String(reason)) {
4801
5044
  case "unknown_service":
@@ -4821,6 +5064,7 @@ export {
4821
5064
  createFallbackEditor,
4822
5065
  createNodeIndex,
4823
5066
  filterServicesForVisibleGroup,
5067
+ getAssignedServiceIds,
4824
5068
  getEligibleFallbacks,
4825
5069
  getFallbackRegistrationInfo,
4826
5070
  normalise,