@timeax/digital-service-engine 0.2.0 → 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.
@@ -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,18 +1264,84 @@ 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) {
1253
- var _a, _b, _c, _d;
1254
- const ratePolicy = normalizeRatePolicy(
1255
- (_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
1256
- );
1338
+ var _a, _b, _c;
1339
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1257
1340
  for (const f of v.fields) {
1258
1341
  if (!isMultiField(f)) continue;
1259
1342
  const baseRates = [];
1260
- for (const o of (_b = f.options) != null ? _b : []) {
1261
- const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
1343
+ for (const o of (_a = f.options) != null ? _a : []) {
1344
+ const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1262
1345
  if (role !== "base") continue;
1263
1346
  const sid = o.service_id;
1264
1347
  if (!isServiceIdRef(sid)) continue;
@@ -2032,6 +2115,38 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
2032
2115
  }
2033
2116
  }
2034
2117
 
2118
+ // src/core/governance.ts
2119
+ var DEFAULT_FALLBACK_SETTINGS = {
2120
+ requireConstraintFit: true,
2121
+ ratePolicy: { kind: "lte_primary", pct: 5 },
2122
+ selectionStrategy: "priority",
2123
+ mode: "strict"
2124
+ };
2125
+ function resolveGlobalRatePolicy(options) {
2126
+ return normalizeRatePolicy(options.ratePolicy);
2127
+ }
2128
+ function resolveFallbackSettings(options) {
2129
+ var _a;
2130
+ return {
2131
+ ...DEFAULT_FALLBACK_SETTINGS,
2132
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2133
+ };
2134
+ }
2135
+ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2136
+ var _a, _b, _c, _d;
2137
+ const mergedFallbackSettings = {
2138
+ ...(_a = defaults.fallbackSettings) != null ? _a : {},
2139
+ ...(_b = overrides.fallbackSettings) != null ? _b : {}
2140
+ };
2141
+ return {
2142
+ ...defaults,
2143
+ ...overrides,
2144
+ policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2145
+ ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2146
+ fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2147
+ };
2148
+ }
2149
+
2035
2150
  // src/core/builder.ts
2036
2151
  import { cloneDeep as cloneDeep2 } from "lodash-es";
2037
2152
  function createBuilder(opts = {}) {
@@ -2274,7 +2389,7 @@ var BuilderImpl = class {
2274
2389
  return out;
2275
2390
  }
2276
2391
  errors() {
2277
- return validate(this.props, this.options);
2392
+ return validate(this.props, mergeValidatorOptions({}, this.options));
2278
2393
  }
2279
2394
  getOptions() {
2280
2395
  return cloneDeep2(this.options);
@@ -2561,11 +2676,14 @@ function readVisibilitySimOpts(ctx) {
2561
2676
  };
2562
2677
  }
2563
2678
  function validate(props, ctx = {}) {
2564
- var _a, _b, _c, _d;
2679
+ var _a, _b, _c;
2680
+ const options = mergeValidatorOptions({}, ctx);
2681
+ const fallbackSettings = resolveFallbackSettings(options);
2682
+ const ratePolicy = resolveGlobalRatePolicy(options);
2565
2683
  const errors = [];
2566
- const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
2684
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
2567
2685
  const selectedKeys = new Set(
2568
- (_b = ctx.selectedOptionKeys) != null ? _b : []
2686
+ (_b = options.selectedOptionKeys) != null ? _b : []
2569
2687
  );
2570
2688
  const tags = Array.isArray(props.filters) ? props.filters : [];
2571
2689
  const fields = Array.isArray(props.fields) ? props.fields : [];
@@ -2575,8 +2693,12 @@ function validate(props, ctx = {}) {
2575
2693
  for (const f of fields) fieldById.set(f.id, f);
2576
2694
  const v = {
2577
2695
  props,
2578
- nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
2579
- options: ctx,
2696
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
2697
+ options: {
2698
+ ...options,
2699
+ ratePolicy,
2700
+ fallbackSettings
2701
+ },
2580
2702
  errors,
2581
2703
  serviceMap,
2582
2704
  selectedKeys,
@@ -2591,7 +2713,7 @@ function validate(props, ctx = {}) {
2591
2713
  validateIdentity(v);
2592
2714
  validateOptionMaps(v);
2593
2715
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
2594
- const visSim = readVisibilitySimOpts(ctx);
2716
+ const visSim = readVisibilitySimOpts(options);
2595
2717
  validateVisibility(v, visSim);
2596
2718
  applyPolicies(
2597
2719
  v.errors,
@@ -2612,7 +2734,7 @@ function validate(props, ctx = {}) {
2612
2734
  builder,
2613
2735
  services: serviceMap,
2614
2736
  tagId: tag.id,
2615
- ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
2737
+ ratePolicy,
2616
2738
  invalidFieldIds: v.invalidRateFieldIds
2617
2739
  });
2618
2740
  for (const diag of diags) {
@@ -2662,27 +2784,42 @@ var DEFAULT_SETTINGS = {
2662
2784
  mode: "strict"
2663
2785
  };
2664
2786
  function resolveServiceFallback(params) {
2665
- var _a, _b, _c, _d, _e;
2787
+ var _a, _b;
2666
2788
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2667
2789
  const { primary, nodeId, tagId, services } = params;
2668
- const fb = (_b = params.fallbacks) != null ? _b : {};
2669
- const tried = [];
2670
- const lists = [];
2671
- if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
2672
- 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();
2673
2797
  const primaryRate = rateOf(services, primary);
2674
- for (const list of lists) {
2675
- for (const cand of list) {
2676
- if (tried.includes(cand)) continue;
2677
- tried.push(cand);
2678
- const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
2679
- if (!candCap) continue;
2680
- 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
+ }
2681
2814
  if (s.requireConstraintFit && tagId) {
2682
- const ok = satisfiesTagConstraints(tagId, params, candCap);
2683
- if (!ok) continue;
2815
+ const fitsConstraints = satisfiesTagConstraints(
2816
+ tagId,
2817
+ params,
2818
+ capability
2819
+ );
2820
+ if (!fitsConstraints) continue;
2684
2821
  }
2685
- return cand;
2822
+ return candidate;
2686
2823
  }
2687
2824
  }
2688
2825
  return null;
@@ -2692,7 +2829,7 @@ function collectFailedFallbacks(props, services, settings) {
2692
2829
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
2693
2830
  const out = [];
2694
2831
  const fb = (_a = props.fallbacks) != null ? _a : {};
2695
- const primaryRate = (p) => rateOf(services, p);
2832
+ const primaryRate = (primary) => rateOf(services, primary);
2696
2833
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
2697
2834
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2698
2835
  if (!primary) {
@@ -2705,34 +2842,34 @@ function collectFailedFallbacks(props, services, settings) {
2705
2842
  });
2706
2843
  continue;
2707
2844
  }
2708
- for (const cand of list) {
2709
- const cap = getCap(services, cand);
2710
- if (!cap) {
2845
+ for (const candidate of list) {
2846
+ const capability = getCap(services, candidate);
2847
+ if (!capability) {
2711
2848
  out.push({
2712
2849
  scope: "node",
2713
2850
  nodeId,
2714
2851
  primary,
2715
- candidate: cand,
2852
+ candidate,
2716
2853
  reason: "unknown_service"
2717
2854
  });
2718
2855
  continue;
2719
2856
  }
2720
- if (String(cand) === String(primary)) {
2857
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2721
2858
  out.push({
2722
2859
  scope: "node",
2723
2860
  nodeId,
2724
2861
  primary,
2725
- candidate: cand,
2862
+ candidate,
2726
2863
  reason: "cycle"
2727
2864
  });
2728
2865
  continue;
2729
2866
  }
2730
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2867
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2731
2868
  out.push({
2732
2869
  scope: "node",
2733
2870
  nodeId,
2734
2871
  primary,
2735
- candidate: cand,
2872
+ candidate,
2736
2873
  reason: "rate_violation"
2737
2874
  });
2738
2875
  continue;
@@ -2742,58 +2879,55 @@ function collectFailedFallbacks(props, services, settings) {
2742
2879
  scope: "node",
2743
2880
  nodeId,
2744
2881
  primary,
2745
- candidate: cand,
2882
+ candidate,
2746
2883
  reason: "no_tag_context"
2747
2884
  });
2748
2885
  continue;
2749
2886
  }
2750
- let anyPass = false;
2751
- let anyFail = false;
2752
2887
  for (const tagId of tagContexts) {
2753
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
2754
- if (ok) anyPass = true;
2755
- else {
2756
- anyFail = true;
2757
- out.push({
2758
- scope: "node",
2759
- nodeId,
2760
- primary,
2761
- candidate: cand,
2762
- tagContext: tagId,
2763
- reason: "constraint_mismatch"
2764
- });
2765
- }
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
+ });
2766
2902
  }
2767
- void anyPass;
2768
- void anyFail;
2769
2903
  }
2770
2904
  }
2771
2905
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
2772
- for (const cand of list) {
2773
- const cap = getCap(services, cand);
2774
- if (!cap) {
2906
+ for (const candidate of list) {
2907
+ const capability = getCap(services, candidate);
2908
+ if (!capability) {
2775
2909
  out.push({
2776
2910
  scope: "global",
2777
2911
  primary,
2778
- candidate: cand,
2912
+ candidate,
2779
2913
  reason: "unknown_service"
2780
2914
  });
2781
2915
  continue;
2782
2916
  }
2783
- if (String(cand) === String(primary)) {
2917
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2784
2918
  out.push({
2785
2919
  scope: "global",
2786
2920
  primary,
2787
- candidate: cand,
2921
+ candidate,
2788
2922
  reason: "cycle"
2789
2923
  });
2790
2924
  continue;
2791
2925
  }
2792
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2926
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2793
2927
  out.push({
2794
2928
  scope: "global",
2795
2929
  primary,
2796
- candidate: cand,
2930
+ candidate,
2797
2931
  reason: "rate_violation"
2798
2932
  });
2799
2933
  }
@@ -2802,52 +2936,61 @@ function collectFailedFallbacks(props, services, settings) {
2802
2936
  return out;
2803
2937
  }
2804
2938
  function rateOf(map, id) {
2805
- var _a;
2939
+ var _a, _b;
2806
2940
  if (id === void 0 || id === null) return void 0;
2807
- const c = getCap(map, id);
2808
- 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;
2809
2942
  }
2810
- function passesRate(policy, primaryRate, candRate) {
2811
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
2943
+ function passesRate(policy, primaryRate, candidateRate) {
2944
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
2812
2945
  return false;
2813
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
2946
+ }
2947
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
2814
2948
  return false;
2815
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
2949
+ }
2950
+ return passesRatePolicy(
2951
+ normalizeRatePolicy(policy),
2952
+ primaryRate,
2953
+ candidateRate
2954
+ );
2816
2955
  }
2817
2956
  function getCap(map, id) {
2818
2957
  return getServiceCapability(map, id);
2819
2958
  }
2820
- function isCapFlagEnabled(cap, flagId) {
2959
+ function isCapFlagEnabled(capability, flagId) {
2821
2960
  var _a, _b;
2822
- 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;
2823
2962
  if (fromFlags === true) return true;
2824
2963
  if (fromFlags === false) return false;
2825
- const legacy = cap[flagId];
2964
+ const legacy = capability[flagId];
2826
2965
  return legacy === true;
2827
2966
  }
2828
- function satisfiesTagConstraints(tagId, ctx, cap) {
2829
- const tag = ctx.props.filters.find((t) => t.id === tagId);
2830
- const eff = tag == null ? void 0 : tag.constraints;
2831
- if (!eff) return true;
2832
- for (const [key, value] of Object.entries(eff)) {
2833
- 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)) {
2834
2973
  return false;
2835
2974
  }
2836
2975
  }
2837
2976
  return true;
2838
2977
  }
2839
2978
  function primaryForNode(props, nodeId) {
2840
- const tag = props.filters.find((t) => t.id === nodeId);
2979
+ const tag = props.filters.find((item) => item.id === nodeId);
2841
2980
  if (tag) {
2842
2981
  return { primary: tag.service_id, tagContexts: [tag.id] };
2843
2982
  }
2844
2983
  const field = props.fields.find(
2845
- (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)
2846
2985
  );
2847
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
2848
- const opt = field.options.find((o) => o.id === nodeId);
2849
- const contexts = bindIdsToArray(field.bind_id);
2850
- 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
+ };
2851
2994
  }
2852
2995
  function bindIdsToArray(bind) {
2853
2996
  if (!bind) return [];
@@ -2857,43 +3000,54 @@ function getEligibleFallbacks(params) {
2857
3000
  var _a, _b, _c, _d, _e, _f;
2858
3001
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2859
3002
  const { primary, nodeId, tagId, services } = params;
2860
- const fb = (_b = params.fallbacks) != null ? _b : {};
2861
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
2862
- excludes.add(String(primary));
2863
- const unique = (_d = params.unique) != null ? _d : true;
2864
- const lists = [];
2865
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
2866
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
2867
- 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 [];
2868
3016
  const primaryRate = rateOf(services, primary);
2869
3017
  const seen = /* @__PURE__ */ new Set();
2870
3018
  const eligible = [];
2871
- for (const list of lists) {
2872
- for (const cand of list) {
2873
- const key = String(cand);
2874
- if (excludes.has(key)) continue;
2875
- if (unique && seen.has(key)) continue;
2876
- seen.add(key);
2877
- const cap = getCap(services, cand);
2878
- if (!cap) continue;
2879
- 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
+ }
2880
3034
  if (s.requireConstraintFit && tagId) {
2881
- const ok = satisfiesTagConstraints(
3035
+ const fitsConstraints = satisfiesTagConstraints(
2882
3036
  tagId,
2883
3037
  { props: params.props, services },
2884
- cap
3038
+ capability
2885
3039
  );
2886
- if (!ok) continue;
3040
+ if (!fitsConstraints) continue;
2887
3041
  }
2888
- eligible.push(cand);
3042
+ eligible.push(candidateId);
2889
3043
  }
2890
3044
  }
2891
3045
  if (s.selectionStrategy === "cheapest") {
2892
- eligible.sort((a, b) => {
3046
+ eligible.sort((left, right) => {
2893
3047
  var _a2, _b2;
2894
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
2895
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
2896
- 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;
2897
3051
  });
2898
3052
  }
2899
3053
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -2901,10 +3055,104 @@ function getEligibleFallbacks(params) {
2901
3055
  }
2902
3056
  return eligible;
2903
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
+ }
2904
3098
  function getFallbackRegistrationInfo(props, nodeId) {
2905
3099
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2906
3100
  return { primary, tagContexts };
2907
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
+ }
2908
3156
 
2909
3157
  // src/core/tag-relations.ts
2910
3158
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -3523,18 +3771,23 @@ function compilePolicies(raw) {
3523
3771
 
3524
3772
  // src/core/service-filter.ts
3525
3773
  function filterServicesForVisibleGroup(input, deps) {
3526
- var _a, _b, _c, _d, _e, _f;
3774
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
3527
3775
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
3776
+ const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
3528
3777
  const { context } = input;
3529
3778
  const usedSet = new Set(context.usedServiceIds.map(String));
3530
3779
  const primary = context.usedServiceIds[0];
3780
+ const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
3781
+ const resolvedRatePolicy = normalizeRatePolicy(
3782
+ (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
3783
+ );
3784
+ const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
3531
3785
  const fb = {
3532
- requireConstraintFit: true,
3533
- ratePolicy: { kind: "lte_primary", pct: 5 },
3534
- selectionStrategy: "priority",
3535
- mode: "strict",
3536
- ...(_d = context.fallback) != null ? _d : {}
3786
+ ...DEFAULT_FALLBACK_SETTINGS,
3787
+ ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
3788
+ ratePolicy: resolvedRatePolicy
3537
3789
  };
3790
+ const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
3538
3791
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
3539
3792
  deps.builder,
3540
3793
  context.tagId,
@@ -3559,11 +3812,11 @@ function filterServicesForVisibleGroup(input, deps) {
3559
3812
  const fitsConstraints = constraintFitOk(
3560
3813
  svcMap,
3561
3814
  cap.id,
3562
- (_e = context.effectiveConstraints) != null ? _e : {}
3815
+ (_k = context.effectiveConstraints) != null ? _k : {}
3563
3816
  );
3564
3817
  const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
3565
3818
  const polRes = evaluatePoliciesRaw(
3566
- (_f = context.policies) != null ? _f : [],
3819
+ policySource,
3567
3820
  [...context.usedServiceIds, id],
3568
3821
  svcMap,
3569
3822
  context.tagId,
@@ -4438,6 +4691,7 @@ function createFallbackEditor(options = {}) {
4438
4691
  const original = cloneFallbacks(options.fallbacks);
4439
4692
  let current = cloneFallbacks(options.fallbacks);
4440
4693
  const props = options.props;
4694
+ const snapshot = options.snapshot;
4441
4695
  const services = (_a = options.services) != null ? _a : {};
4442
4696
  const settings = (_b = options.settings) != null ? _b : {};
4443
4697
  function state() {
@@ -4532,7 +4786,11 @@ function createFallbackEditor(options = {}) {
4532
4786
  const allowed2 = [];
4533
4787
  for (const candidate of normalized) {
4534
4788
  const reasons = [];
4535
- if (String(candidate) === String(context.primary)) {
4789
+ if (isSameServiceCapabilityRef(
4790
+ services,
4791
+ candidate,
4792
+ context.primary
4793
+ )) {
4536
4794
  reasons.push("self_reference");
4537
4795
  }
4538
4796
  if (reasons.length) {
@@ -4620,7 +4878,19 @@ function createFallbackEditor(options = {}) {
4620
4878
  return writeScope(context, []);
4621
4879
  }
4622
4880
  function eligible(context, opt) {
4881
+ var _a2, _b2;
4623
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
+ );
4624
4894
  if (context.scope === "global") {
4625
4895
  return getEligibleFallbacks({
4626
4896
  primary: context.primary,
@@ -4628,9 +4898,10 @@ function createFallbackEditor(options = {}) {
4628
4898
  fallbacks: current,
4629
4899
  settings,
4630
4900
  props,
4631
- exclude: opt == null ? void 0 : opt.exclude,
4901
+ exclude,
4632
4902
  unique: opt == null ? void 0 : opt.unique,
4633
- limit: opt == null ? void 0 : opt.limit
4903
+ limit: opt == null ? void 0 : opt.limit,
4904
+ source
4634
4905
  });
4635
4906
  }
4636
4907
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -4638,14 +4909,19 @@ function createFallbackEditor(options = {}) {
4638
4909
  return getEligibleFallbacks({
4639
4910
  primary: info.primary,
4640
4911
  nodeId: context.nodeId,
4641
- tagId: info.tagContexts[0],
4912
+ tagId: resolveNodeTagContext({
4913
+ nodeId: context.nodeId,
4914
+ snapshot,
4915
+ fallbackTagContexts: info.tagContexts
4916
+ }),
4642
4917
  services,
4643
4918
  fallbacks: current,
4644
4919
  settings,
4645
4920
  props,
4646
- exclude: opt == null ? void 0 : opt.exclude,
4921
+ exclude,
4647
4922
  unique: opt == null ? void 0 : opt.unique,
4648
- limit: opt == null ? void 0 : opt.limit
4923
+ limit: opt == null ? void 0 : opt.limit,
4924
+ source
4649
4925
  });
4650
4926
  }
4651
4927
  function writeScope(context, nextList) {
@@ -4703,14 +4979,14 @@ function sameFallbacks(a, b) {
4703
4979
  function normalizeCandidateList(input, preserveOrder) {
4704
4980
  const out = [];
4705
4981
  for (const item of input != null ? input : []) {
4706
- if (!isValidServiceIdRef(item)) continue;
4982
+ if (!isValidServiceIdRef2(item)) continue;
4707
4983
  const exists = out.some((x) => String(x) === String(item));
4708
4984
  if (exists) continue;
4709
4985
  out.push(item);
4710
4986
  }
4711
4987
  return preserveOrder ? out : out;
4712
4988
  }
4713
- function isValidServiceIdRef(value) {
4989
+ function isValidServiceIdRef2(value) {
4714
4990
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
4715
4991
  }
4716
4992
  function clamp(n, min, max) {
@@ -4719,7 +4995,7 @@ function clamp(n, min, max) {
4719
4995
  function getNodeRegistrationInfo(props, nodeId) {
4720
4996
  const tag = props.filters.find((t) => t.id === nodeId);
4721
4997
  if (tag) {
4722
- if (!isValidServiceIdRef(tag.service_id)) {
4998
+ if (!isValidServiceIdRef2(tag.service_id)) {
4723
4999
  return { ok: false, reasons: ["no_primary"] };
4724
5000
  }
4725
5001
  return {
@@ -4732,7 +5008,7 @@ function getNodeRegistrationInfo(props, nodeId) {
4732
5008
  if (!hit) {
4733
5009
  return { ok: false, reasons: ["node_not_found"] };
4734
5010
  }
4735
- if (!isValidServiceIdRef(hit.option.service_id)) {
5011
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
4736
5012
  return { ok: false, reasons: ["no_primary"] };
4737
5013
  }
4738
5014
  return {
@@ -4754,6 +5030,15 @@ function bindIdsToArray2(v) {
4754
5030
  if (Array.isArray(v)) return v.filter(Boolean);
4755
5031
  return v ? [v] : [];
4756
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
+ }
4757
5042
  function mapDiagReason(reason) {
4758
5043
  switch (String(reason)) {
4759
5044
  case "unknown_service":
@@ -4779,6 +5064,7 @@ export {
4779
5064
  createFallbackEditor,
4780
5065
  createNodeIndex,
4781
5066
  filterServicesForVisibleGroup,
5067
+ getAssignedServiceIds,
4782
5068
  getEligibleFallbacks,
4783
5069
  getFallbackRegistrationInfo,
4784
5070
  normalise,