@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.
@@ -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,18 +1304,84 @@ 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) {
1292
- var _a, _b, _c, _d;
1293
- const ratePolicy = normalizeRatePolicy(
1294
- (_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
1295
- );
1378
+ var _a, _b, _c;
1379
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1296
1380
  for (const f of v.fields) {
1297
1381
  if (!isMultiField(f)) continue;
1298
1382
  const baseRates = [];
1299
- for (const o of (_b = f.options) != null ? _b : []) {
1300
- const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
1383
+ for (const o of (_a = f.options) != null ? _a : []) {
1384
+ const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1301
1385
  if (role !== "base") continue;
1302
1386
  const sid = o.service_id;
1303
1387
  if (!isServiceIdRef(sid)) continue;
@@ -2071,6 +2155,38 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
2071
2155
  }
2072
2156
  }
2073
2157
 
2158
+ // src/core/governance.ts
2159
+ var DEFAULT_FALLBACK_SETTINGS = {
2160
+ requireConstraintFit: true,
2161
+ ratePolicy: { kind: "lte_primary", pct: 5 },
2162
+ selectionStrategy: "priority",
2163
+ mode: "strict"
2164
+ };
2165
+ function resolveGlobalRatePolicy(options) {
2166
+ return normalizeRatePolicy(options.ratePolicy);
2167
+ }
2168
+ function resolveFallbackSettings(options) {
2169
+ var _a;
2170
+ return {
2171
+ ...DEFAULT_FALLBACK_SETTINGS,
2172
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2173
+ };
2174
+ }
2175
+ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2176
+ var _a, _b, _c, _d;
2177
+ const mergedFallbackSettings = {
2178
+ ...(_a = defaults.fallbackSettings) != null ? _a : {},
2179
+ ...(_b = overrides.fallbackSettings) != null ? _b : {}
2180
+ };
2181
+ return {
2182
+ ...defaults,
2183
+ ...overrides,
2184
+ policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2185
+ ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2186
+ fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2187
+ };
2188
+ }
2189
+
2074
2190
  // src/core/builder.ts
2075
2191
  var import_lodash_es2 = require("lodash-es");
2076
2192
  function createBuilder(opts = {}) {
@@ -2313,7 +2429,7 @@ var BuilderImpl = class {
2313
2429
  return out;
2314
2430
  }
2315
2431
  errors() {
2316
- return validate(this.props, this.options);
2432
+ return validate(this.props, mergeValidatorOptions({}, this.options));
2317
2433
  }
2318
2434
  getOptions() {
2319
2435
  return (0, import_lodash_es2.cloneDeep)(this.options);
@@ -2600,11 +2716,14 @@ function readVisibilitySimOpts(ctx) {
2600
2716
  };
2601
2717
  }
2602
2718
  function validate(props, ctx = {}) {
2603
- var _a, _b, _c, _d;
2719
+ var _a, _b, _c;
2720
+ const options = mergeValidatorOptions({}, ctx);
2721
+ const fallbackSettings = resolveFallbackSettings(options);
2722
+ const ratePolicy = resolveGlobalRatePolicy(options);
2604
2723
  const errors = [];
2605
- const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
2724
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
2606
2725
  const selectedKeys = new Set(
2607
- (_b = ctx.selectedOptionKeys) != null ? _b : []
2726
+ (_b = options.selectedOptionKeys) != null ? _b : []
2608
2727
  );
2609
2728
  const tags = Array.isArray(props.filters) ? props.filters : [];
2610
2729
  const fields = Array.isArray(props.fields) ? props.fields : [];
@@ -2614,8 +2733,12 @@ function validate(props, ctx = {}) {
2614
2733
  for (const f of fields) fieldById.set(f.id, f);
2615
2734
  const v = {
2616
2735
  props,
2617
- nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
2618
- options: ctx,
2736
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
2737
+ options: {
2738
+ ...options,
2739
+ ratePolicy,
2740
+ fallbackSettings
2741
+ },
2619
2742
  errors,
2620
2743
  serviceMap,
2621
2744
  selectedKeys,
@@ -2630,7 +2753,7 @@ function validate(props, ctx = {}) {
2630
2753
  validateIdentity(v);
2631
2754
  validateOptionMaps(v);
2632
2755
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
2633
- const visSim = readVisibilitySimOpts(ctx);
2756
+ const visSim = readVisibilitySimOpts(options);
2634
2757
  validateVisibility(v, visSim);
2635
2758
  applyPolicies(
2636
2759
  v.errors,
@@ -2651,7 +2774,7 @@ function validate(props, ctx = {}) {
2651
2774
  builder,
2652
2775
  services: serviceMap,
2653
2776
  tagId: tag.id,
2654
- ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
2777
+ ratePolicy,
2655
2778
  invalidFieldIds: v.invalidRateFieldIds
2656
2779
  });
2657
2780
  for (const diag of diags) {
@@ -2701,27 +2824,42 @@ var DEFAULT_SETTINGS = {
2701
2824
  mode: "strict"
2702
2825
  };
2703
2826
  function resolveServiceFallback(params) {
2704
- var _a, _b, _c, _d, _e;
2827
+ var _a, _b;
2705
2828
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2706
2829
  const { primary, nodeId, tagId, services } = params;
2707
- const fb = (_b = params.fallbacks) != null ? _b : {};
2708
- const tried = [];
2709
- const lists = [];
2710
- if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
2711
- 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();
2712
2837
  const primaryRate = rateOf(services, primary);
2713
- for (const list of lists) {
2714
- for (const cand of list) {
2715
- if (tried.includes(cand)) continue;
2716
- tried.push(cand);
2717
- const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
2718
- if (!candCap) continue;
2719
- 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
+ }
2720
2854
  if (s.requireConstraintFit && tagId) {
2721
- const ok = satisfiesTagConstraints(tagId, params, candCap);
2722
- if (!ok) continue;
2855
+ const fitsConstraints = satisfiesTagConstraints(
2856
+ tagId,
2857
+ params,
2858
+ capability
2859
+ );
2860
+ if (!fitsConstraints) continue;
2723
2861
  }
2724
- return cand;
2862
+ return candidate;
2725
2863
  }
2726
2864
  }
2727
2865
  return null;
@@ -2731,7 +2869,7 @@ function collectFailedFallbacks(props, services, settings) {
2731
2869
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
2732
2870
  const out = [];
2733
2871
  const fb = (_a = props.fallbacks) != null ? _a : {};
2734
- const primaryRate = (p) => rateOf(services, p);
2872
+ const primaryRate = (primary) => rateOf(services, primary);
2735
2873
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
2736
2874
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2737
2875
  if (!primary) {
@@ -2744,34 +2882,34 @@ function collectFailedFallbacks(props, services, settings) {
2744
2882
  });
2745
2883
  continue;
2746
2884
  }
2747
- for (const cand of list) {
2748
- const cap = getCap(services, cand);
2749
- if (!cap) {
2885
+ for (const candidate of list) {
2886
+ const capability = getCap(services, candidate);
2887
+ if (!capability) {
2750
2888
  out.push({
2751
2889
  scope: "node",
2752
2890
  nodeId,
2753
2891
  primary,
2754
- candidate: cand,
2892
+ candidate,
2755
2893
  reason: "unknown_service"
2756
2894
  });
2757
2895
  continue;
2758
2896
  }
2759
- if (String(cand) === String(primary)) {
2897
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2760
2898
  out.push({
2761
2899
  scope: "node",
2762
2900
  nodeId,
2763
2901
  primary,
2764
- candidate: cand,
2902
+ candidate,
2765
2903
  reason: "cycle"
2766
2904
  });
2767
2905
  continue;
2768
2906
  }
2769
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2907
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2770
2908
  out.push({
2771
2909
  scope: "node",
2772
2910
  nodeId,
2773
2911
  primary,
2774
- candidate: cand,
2912
+ candidate,
2775
2913
  reason: "rate_violation"
2776
2914
  });
2777
2915
  continue;
@@ -2781,58 +2919,55 @@ function collectFailedFallbacks(props, services, settings) {
2781
2919
  scope: "node",
2782
2920
  nodeId,
2783
2921
  primary,
2784
- candidate: cand,
2922
+ candidate,
2785
2923
  reason: "no_tag_context"
2786
2924
  });
2787
2925
  continue;
2788
2926
  }
2789
- let anyPass = false;
2790
- let anyFail = false;
2791
2927
  for (const tagId of tagContexts) {
2792
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
2793
- if (ok) anyPass = true;
2794
- else {
2795
- anyFail = true;
2796
- out.push({
2797
- scope: "node",
2798
- nodeId,
2799
- primary,
2800
- candidate: cand,
2801
- tagContext: tagId,
2802
- reason: "constraint_mismatch"
2803
- });
2804
- }
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
+ });
2805
2942
  }
2806
- void anyPass;
2807
- void anyFail;
2808
2943
  }
2809
2944
  }
2810
2945
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
2811
- for (const cand of list) {
2812
- const cap = getCap(services, cand);
2813
- if (!cap) {
2946
+ for (const candidate of list) {
2947
+ const capability = getCap(services, candidate);
2948
+ if (!capability) {
2814
2949
  out.push({
2815
2950
  scope: "global",
2816
2951
  primary,
2817
- candidate: cand,
2952
+ candidate,
2818
2953
  reason: "unknown_service"
2819
2954
  });
2820
2955
  continue;
2821
2956
  }
2822
- if (String(cand) === String(primary)) {
2957
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
2823
2958
  out.push({
2824
2959
  scope: "global",
2825
2960
  primary,
2826
- candidate: cand,
2961
+ candidate,
2827
2962
  reason: "cycle"
2828
2963
  });
2829
2964
  continue;
2830
2965
  }
2831
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2966
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
2832
2967
  out.push({
2833
2968
  scope: "global",
2834
2969
  primary,
2835
- candidate: cand,
2970
+ candidate,
2836
2971
  reason: "rate_violation"
2837
2972
  });
2838
2973
  }
@@ -2841,52 +2976,61 @@ function collectFailedFallbacks(props, services, settings) {
2841
2976
  return out;
2842
2977
  }
2843
2978
  function rateOf(map, id) {
2844
- var _a;
2979
+ var _a, _b;
2845
2980
  if (id === void 0 || id === null) return void 0;
2846
- const c = getCap(map, id);
2847
- 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;
2848
2982
  }
2849
- function passesRate(policy, primaryRate, candRate) {
2850
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
2983
+ function passesRate(policy, primaryRate, candidateRate) {
2984
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
2851
2985
  return false;
2852
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
2986
+ }
2987
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
2853
2988
  return false;
2854
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
2989
+ }
2990
+ return passesRatePolicy(
2991
+ normalizeRatePolicy(policy),
2992
+ primaryRate,
2993
+ candidateRate
2994
+ );
2855
2995
  }
2856
2996
  function getCap(map, id) {
2857
2997
  return getServiceCapability(map, id);
2858
2998
  }
2859
- function isCapFlagEnabled(cap, flagId) {
2999
+ function isCapFlagEnabled(capability, flagId) {
2860
3000
  var _a, _b;
2861
- 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;
2862
3002
  if (fromFlags === true) return true;
2863
3003
  if (fromFlags === false) return false;
2864
- const legacy = cap[flagId];
3004
+ const legacy = capability[flagId];
2865
3005
  return legacy === true;
2866
3006
  }
2867
- function satisfiesTagConstraints(tagId, ctx, cap) {
2868
- const tag = ctx.props.filters.find((t) => t.id === tagId);
2869
- const eff = tag == null ? void 0 : tag.constraints;
2870
- if (!eff) return true;
2871
- for (const [key, value] of Object.entries(eff)) {
2872
- 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)) {
2873
3013
  return false;
2874
3014
  }
2875
3015
  }
2876
3016
  return true;
2877
3017
  }
2878
3018
  function primaryForNode(props, nodeId) {
2879
- const tag = props.filters.find((t) => t.id === nodeId);
3019
+ const tag = props.filters.find((item) => item.id === nodeId);
2880
3020
  if (tag) {
2881
3021
  return { primary: tag.service_id, tagContexts: [tag.id] };
2882
3022
  }
2883
3023
  const field = props.fields.find(
2884
- (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)
2885
3025
  );
2886
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
2887
- const opt = field.options.find((o) => o.id === nodeId);
2888
- const contexts = bindIdsToArray(field.bind_id);
2889
- 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
+ };
2890
3034
  }
2891
3035
  function bindIdsToArray(bind) {
2892
3036
  if (!bind) return [];
@@ -2896,43 +3040,54 @@ function getEligibleFallbacks(params) {
2896
3040
  var _a, _b, _c, _d, _e, _f;
2897
3041
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2898
3042
  const { primary, nodeId, tagId, services } = params;
2899
- const fb = (_b = params.fallbacks) != null ? _b : {};
2900
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
2901
- excludes.add(String(primary));
2902
- const unique = (_d = params.unique) != null ? _d : true;
2903
- const lists = [];
2904
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
2905
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
2906
- 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 [];
2907
3056
  const primaryRate = rateOf(services, primary);
2908
3057
  const seen = /* @__PURE__ */ new Set();
2909
3058
  const eligible = [];
2910
- for (const list of lists) {
2911
- for (const cand of list) {
2912
- const key = String(cand);
2913
- if (excludes.has(key)) continue;
2914
- if (unique && seen.has(key)) continue;
2915
- seen.add(key);
2916
- const cap = getCap(services, cand);
2917
- if (!cap) continue;
2918
- 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
+ }
2919
3074
  if (s.requireConstraintFit && tagId) {
2920
- const ok = satisfiesTagConstraints(
3075
+ const fitsConstraints = satisfiesTagConstraints(
2921
3076
  tagId,
2922
3077
  { props: params.props, services },
2923
- cap
3078
+ capability
2924
3079
  );
2925
- if (!ok) continue;
3080
+ if (!fitsConstraints) continue;
2926
3081
  }
2927
- eligible.push(cand);
3082
+ eligible.push(candidateId);
2928
3083
  }
2929
3084
  }
2930
3085
  if (s.selectionStrategy === "cheapest") {
2931
- eligible.sort((a, b) => {
3086
+ eligible.sort((left, right) => {
2932
3087
  var _a2, _b2;
2933
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
2934
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
2935
- 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;
2936
3091
  });
2937
3092
  }
2938
3093
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -2940,10 +3095,104 @@ function getEligibleFallbacks(params) {
2940
3095
  }
2941
3096
  return eligible;
2942
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
+ }
2943
3138
  function getFallbackRegistrationInfo(props, nodeId) {
2944
3139
  const { primary, tagContexts } = primaryForNode(props, nodeId);
2945
3140
  return { primary, tagContexts };
2946
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
+ }
2947
3196
 
2948
3197
  // src/core/tag-relations.ts
2949
3198
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -3562,18 +3811,23 @@ function compilePolicies(raw) {
3562
3811
 
3563
3812
  // src/core/service-filter.ts
3564
3813
  function filterServicesForVisibleGroup(input, deps) {
3565
- var _a, _b, _c, _d, _e, _f;
3814
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
3566
3815
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
3816
+ const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
3567
3817
  const { context } = input;
3568
3818
  const usedSet = new Set(context.usedServiceIds.map(String));
3569
3819
  const primary = context.usedServiceIds[0];
3820
+ const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
3821
+ const resolvedRatePolicy = normalizeRatePolicy(
3822
+ (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
3823
+ );
3824
+ const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
3570
3825
  const fb = {
3571
- requireConstraintFit: true,
3572
- ratePolicy: { kind: "lte_primary", pct: 5 },
3573
- selectionStrategy: "priority",
3574
- mode: "strict",
3575
- ...(_d = context.fallback) != null ? _d : {}
3826
+ ...DEFAULT_FALLBACK_SETTINGS,
3827
+ ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
3828
+ ratePolicy: resolvedRatePolicy
3576
3829
  };
3830
+ const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
3577
3831
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
3578
3832
  deps.builder,
3579
3833
  context.tagId,
@@ -3598,11 +3852,11 @@ function filterServicesForVisibleGroup(input, deps) {
3598
3852
  const fitsConstraints = constraintFitOk(
3599
3853
  svcMap,
3600
3854
  cap.id,
3601
- (_e = context.effectiveConstraints) != null ? _e : {}
3855
+ (_k = context.effectiveConstraints) != null ? _k : {}
3602
3856
  );
3603
3857
  const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
3604
3858
  const polRes = evaluatePoliciesRaw(
3605
- (_f = context.policies) != null ? _f : [],
3859
+ policySource,
3606
3860
  [...context.usedServiceIds, id],
3607
3861
  svcMap,
3608
3862
  context.tagId,
@@ -4477,6 +4731,7 @@ function createFallbackEditor(options = {}) {
4477
4731
  const original = cloneFallbacks(options.fallbacks);
4478
4732
  let current = cloneFallbacks(options.fallbacks);
4479
4733
  const props = options.props;
4734
+ const snapshot = options.snapshot;
4480
4735
  const services = (_a = options.services) != null ? _a : {};
4481
4736
  const settings = (_b = options.settings) != null ? _b : {};
4482
4737
  function state() {
@@ -4571,7 +4826,11 @@ function createFallbackEditor(options = {}) {
4571
4826
  const allowed2 = [];
4572
4827
  for (const candidate of normalized) {
4573
4828
  const reasons = [];
4574
- if (String(candidate) === String(context.primary)) {
4829
+ if (isSameServiceCapabilityRef(
4830
+ services,
4831
+ candidate,
4832
+ context.primary
4833
+ )) {
4575
4834
  reasons.push("self_reference");
4576
4835
  }
4577
4836
  if (reasons.length) {
@@ -4659,7 +4918,19 @@ function createFallbackEditor(options = {}) {
4659
4918
  return writeScope(context, []);
4660
4919
  }
4661
4920
  function eligible(context, opt) {
4921
+ var _a2, _b2;
4662
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
+ );
4663
4934
  if (context.scope === "global") {
4664
4935
  return getEligibleFallbacks({
4665
4936
  primary: context.primary,
@@ -4667,9 +4938,10 @@ function createFallbackEditor(options = {}) {
4667
4938
  fallbacks: current,
4668
4939
  settings,
4669
4940
  props,
4670
- exclude: opt == null ? void 0 : opt.exclude,
4941
+ exclude,
4671
4942
  unique: opt == null ? void 0 : opt.unique,
4672
- limit: opt == null ? void 0 : opt.limit
4943
+ limit: opt == null ? void 0 : opt.limit,
4944
+ source
4673
4945
  });
4674
4946
  }
4675
4947
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -4677,14 +4949,19 @@ function createFallbackEditor(options = {}) {
4677
4949
  return getEligibleFallbacks({
4678
4950
  primary: info.primary,
4679
4951
  nodeId: context.nodeId,
4680
- tagId: info.tagContexts[0],
4952
+ tagId: resolveNodeTagContext({
4953
+ nodeId: context.nodeId,
4954
+ snapshot,
4955
+ fallbackTagContexts: info.tagContexts
4956
+ }),
4681
4957
  services,
4682
4958
  fallbacks: current,
4683
4959
  settings,
4684
4960
  props,
4685
- exclude: opt == null ? void 0 : opt.exclude,
4961
+ exclude,
4686
4962
  unique: opt == null ? void 0 : opt.unique,
4687
- limit: opt == null ? void 0 : opt.limit
4963
+ limit: opt == null ? void 0 : opt.limit,
4964
+ source
4688
4965
  });
4689
4966
  }
4690
4967
  function writeScope(context, nextList) {
@@ -4742,14 +5019,14 @@ function sameFallbacks(a, b) {
4742
5019
  function normalizeCandidateList(input, preserveOrder) {
4743
5020
  const out = [];
4744
5021
  for (const item of input != null ? input : []) {
4745
- if (!isValidServiceIdRef(item)) continue;
5022
+ if (!isValidServiceIdRef2(item)) continue;
4746
5023
  const exists = out.some((x) => String(x) === String(item));
4747
5024
  if (exists) continue;
4748
5025
  out.push(item);
4749
5026
  }
4750
5027
  return preserveOrder ? out : out;
4751
5028
  }
4752
- function isValidServiceIdRef(value) {
5029
+ function isValidServiceIdRef2(value) {
4753
5030
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
4754
5031
  }
4755
5032
  function clamp(n, min, max) {
@@ -4758,7 +5035,7 @@ function clamp(n, min, max) {
4758
5035
  function getNodeRegistrationInfo(props, nodeId) {
4759
5036
  const tag = props.filters.find((t) => t.id === nodeId);
4760
5037
  if (tag) {
4761
- if (!isValidServiceIdRef(tag.service_id)) {
5038
+ if (!isValidServiceIdRef2(tag.service_id)) {
4762
5039
  return { ok: false, reasons: ["no_primary"] };
4763
5040
  }
4764
5041
  return {
@@ -4771,7 +5048,7 @@ function getNodeRegistrationInfo(props, nodeId) {
4771
5048
  if (!hit) {
4772
5049
  return { ok: false, reasons: ["node_not_found"] };
4773
5050
  }
4774
- if (!isValidServiceIdRef(hit.option.service_id)) {
5051
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
4775
5052
  return { ok: false, reasons: ["no_primary"] };
4776
5053
  }
4777
5054
  return {
@@ -4793,6 +5070,15 @@ function bindIdsToArray2(v) {
4793
5070
  if (Array.isArray(v)) return v.filter(Boolean);
4794
5071
  return v ? [v] : [];
4795
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
+ }
4796
5082
  function mapDiagReason(reason) {
4797
5083
  switch (String(reason)) {
4798
5084
  case "unknown_service":
@@ -4819,6 +5105,7 @@ function mapDiagReason(reason) {
4819
5105
  createFallbackEditor,
4820
5106
  createNodeIndex,
4821
5107
  filterServicesForVisibleGroup,
5108
+ getAssignedServiceIds,
4822
5109
  getEligibleFallbacks,
4823
5110
  getFallbackRegistrationInfo,
4824
5111
  normalise,