@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.
@@ -1909,6 +1909,9 @@ function toFiniteNumber(v) {
1909
1909
  const n = Number(v);
1910
1910
  return Number.isFinite(n) ? n : NaN;
1911
1911
  }
1912
+ function isValidServiceIdRef(value) {
1913
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
1914
+ }
1912
1915
  function constraintFitOk(svcMap, candidate, constraints) {
1913
1916
  const cap = getServiceCapability(svcMap, candidate);
1914
1917
  if (!cap) return false;
@@ -1917,18 +1920,32 @@ function constraintFitOk(svcMap, candidate, constraints) {
1917
1920
  return !(constraints.cancel === true && !cap.cancel);
1918
1921
  }
1919
1922
  function getServiceCapability(svcMap, candidate) {
1920
- if (candidate === void 0 || candidate === null) return void 0;
1921
- const direct = svcMap[candidate];
1922
- if (direct) return direct;
1923
- const byString = svcMap[String(candidate)];
1924
- if (byString) return byString;
1925
- if (typeof candidate === "string") {
1926
- const maybeNumber = Number(candidate);
1927
- if (Number.isFinite(maybeNumber)) {
1928
- return svcMap[maybeNumber];
1929
- }
1923
+ var _a;
1924
+ return (_a = getServiceCapabilityEntry(svcMap, candidate)) == null ? void 0 : _a.capability;
1925
+ }
1926
+ function getServiceCapabilityCanonicalRef(svcMap, candidate) {
1927
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1928
+ if (!entry) return void 0;
1929
+ return getCanonicalServiceRef(entry.key, entry.capability);
1930
+ }
1931
+ function getServiceCapabilityAliases(svcMap, candidate) {
1932
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1933
+ if (!entry) return [];
1934
+ return collectServiceRefAliases(entry.key, entry.capability);
1935
+ }
1936
+ function isSameServiceCapabilityRef(svcMap, left, right) {
1937
+ if (!isValidServiceIdRef(left) || !isValidServiceIdRef(right)) return false;
1938
+ const leftAliases = new Set(
1939
+ getServiceCapabilityAliases(svcMap, left).map((value) => String(value))
1940
+ );
1941
+ if (!leftAliases.size) {
1942
+ leftAliases.add(String(left));
1930
1943
  }
1931
- return void 0;
1944
+ const rightAliases = getServiceCapabilityAliases(svcMap, right);
1945
+ if (!rightAliases.length) {
1946
+ return leftAliases.has(String(right));
1947
+ }
1948
+ return rightAliases.some((value) => leftAliases.has(String(value)));
1932
1949
  }
1933
1950
  function normalizeRatePolicy(policy) {
1934
1951
  var _a;
@@ -1964,18 +1981,84 @@ function rateOk(svcMap, candidate, primary, policy) {
1964
1981
  if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
1965
1982
  return passesRatePolicy(policy.ratePolicy, pRate, cRate);
1966
1983
  }
1984
+ function getServiceCapabilityEntry(svcMap, candidate) {
1985
+ if (candidate === void 0 || candidate === null) return void 0;
1986
+ const direct = svcMap[candidate];
1987
+ if (direct) {
1988
+ return { key: String(candidate), capability: direct };
1989
+ }
1990
+ const byString = svcMap[String(candidate)];
1991
+ if (byString) {
1992
+ return { key: String(candidate), capability: byString };
1993
+ }
1994
+ if (typeof candidate === "string") {
1995
+ const maybeNumber = Number(candidate);
1996
+ if (Number.isFinite(maybeNumber)) {
1997
+ const byNumber = svcMap[maybeNumber];
1998
+ if (byNumber) {
1999
+ return { key: String(maybeNumber), capability: byNumber };
2000
+ }
2001
+ }
2002
+ }
2003
+ const target = String(candidate);
2004
+ for (const [key, capability] of Object.entries(svcMap != null ? svcMap : {})) {
2005
+ if (collectServiceRefAliases(key, capability).some(
2006
+ (alias) => String(alias) === target
2007
+ )) {
2008
+ return { key, capability };
2009
+ }
2010
+ }
2011
+ return void 0;
2012
+ }
2013
+ function collectServiceRefAliases(key, capability) {
2014
+ const out = [];
2015
+ const seen = /* @__PURE__ */ new Set();
2016
+ const push = (value) => {
2017
+ if (!isValidServiceIdRef(value)) return;
2018
+ const normalized = normalizeServiceRef(value);
2019
+ if (!normalized) return;
2020
+ const aliasKey = String(normalized);
2021
+ if (seen.has(aliasKey)) return;
2022
+ seen.add(aliasKey);
2023
+ out.push(normalized);
2024
+ };
2025
+ push(getCanonicalServiceRef(key, capability));
2026
+ push(capability.service);
2027
+ push(capability.key);
2028
+ push(capability.id);
2029
+ return out;
2030
+ }
2031
+ function getCanonicalServiceRef(key, capability) {
2032
+ const explicitRefs = [capability.service, capability.key, capability.id];
2033
+ for (const ref of explicitRefs) {
2034
+ if (!isValidServiceIdRef(ref)) continue;
2035
+ if (String(ref) === key) {
2036
+ return ref;
2037
+ }
2038
+ }
2039
+ return normalizeServiceRef(key);
2040
+ }
2041
+ function normalizeServiceRef(value) {
2042
+ if (!isValidServiceIdRef(value)) return void 0;
2043
+ if (typeof value === "number") return value;
2044
+ const trimmed = value.trim();
2045
+ if (!trimmed) return void 0;
2046
+ const asNumber = Number(trimmed);
2047
+ if (Number.isFinite(asNumber) && String(asNumber) === trimmed) {
2048
+ return asNumber;
2049
+ }
2050
+ return trimmed;
2051
+ }
1967
2052
 
1968
2053
  // src/core/validate/steps/rates.ts
1969
2054
  function validateRates(v) {
1970
- var _a, _b, _c, _d;
1971
- const ratePolicy = normalizeRatePolicy(
1972
- (_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
1973
- );
2055
+ var _a, _b, _c;
2056
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1974
2057
  for (const f of v.fields) {
1975
2058
  if (!isMultiField(f)) continue;
1976
2059
  const baseRates = [];
1977
- for (const o of (_b = f.options) != null ? _b : []) {
1978
- const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
2060
+ for (const o of (_a = f.options) != null ? _a : []) {
2061
+ const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1979
2062
  if (role !== "base") continue;
1980
2063
  const sid = o.service_id;
1981
2064
  if (!isServiceIdRef(sid)) continue;
@@ -2749,6 +2832,38 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
2749
2832
  }
2750
2833
  }
2751
2834
 
2835
+ // src/core/governance.ts
2836
+ var DEFAULT_FALLBACK_SETTINGS = {
2837
+ requireConstraintFit: true,
2838
+ ratePolicy: { kind: "lte_primary", pct: 5 },
2839
+ selectionStrategy: "priority",
2840
+ mode: "strict"
2841
+ };
2842
+ function resolveGlobalRatePolicy(options) {
2843
+ return normalizeRatePolicy(options.ratePolicy);
2844
+ }
2845
+ function resolveFallbackSettings(options) {
2846
+ var _a;
2847
+ return {
2848
+ ...DEFAULT_FALLBACK_SETTINGS,
2849
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2850
+ };
2851
+ }
2852
+ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2853
+ var _a, _b, _c, _d;
2854
+ const mergedFallbackSettings = {
2855
+ ...(_a = defaults.fallbackSettings) != null ? _a : {},
2856
+ ...(_b = overrides.fallbackSettings) != null ? _b : {}
2857
+ };
2858
+ return {
2859
+ ...defaults,
2860
+ ...overrides,
2861
+ policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2862
+ ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2863
+ fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2864
+ };
2865
+ }
2866
+
2752
2867
  // src/core/builder.ts
2753
2868
  var import_lodash_es2 = require("lodash-es");
2754
2869
  function createBuilder(opts = {}) {
@@ -2991,7 +3106,7 @@ var BuilderImpl = class {
2991
3106
  return out;
2992
3107
  }
2993
3108
  errors() {
2994
- return validate(this.props, this.options);
3109
+ return validate(this.props, mergeValidatorOptions({}, this.options));
2995
3110
  }
2996
3111
  getOptions() {
2997
3112
  return (0, import_lodash_es2.cloneDeep)(this.options);
@@ -3278,11 +3393,14 @@ function readVisibilitySimOpts(ctx) {
3278
3393
  };
3279
3394
  }
3280
3395
  function validate(props, ctx = {}) {
3281
- var _a, _b, _c, _d;
3396
+ var _a, _b, _c;
3397
+ const options = mergeValidatorOptions({}, ctx);
3398
+ const fallbackSettings = resolveFallbackSettings(options);
3399
+ const ratePolicy = resolveGlobalRatePolicy(options);
3282
3400
  const errors = [];
3283
- const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
3401
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3284
3402
  const selectedKeys = new Set(
3285
- (_b = ctx.selectedOptionKeys) != null ? _b : []
3403
+ (_b = options.selectedOptionKeys) != null ? _b : []
3286
3404
  );
3287
3405
  const tags = Array.isArray(props.filters) ? props.filters : [];
3288
3406
  const fields = Array.isArray(props.fields) ? props.fields : [];
@@ -3292,8 +3410,12 @@ function validate(props, ctx = {}) {
3292
3410
  for (const f of fields) fieldById.set(f.id, f);
3293
3411
  const v = {
3294
3412
  props,
3295
- nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
3296
- options: ctx,
3413
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3414
+ options: {
3415
+ ...options,
3416
+ ratePolicy,
3417
+ fallbackSettings
3418
+ },
3297
3419
  errors,
3298
3420
  serviceMap,
3299
3421
  selectedKeys,
@@ -3308,7 +3430,7 @@ function validate(props, ctx = {}) {
3308
3430
  validateIdentity(v);
3309
3431
  validateOptionMaps(v);
3310
3432
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3311
- const visSim = readVisibilitySimOpts(ctx);
3433
+ const visSim = readVisibilitySimOpts(options);
3312
3434
  validateVisibility(v, visSim);
3313
3435
  applyPolicies(
3314
3436
  v.errors,
@@ -3329,7 +3451,7 @@ function validate(props, ctx = {}) {
3329
3451
  builder,
3330
3452
  services: serviceMap,
3331
3453
  tagId: tag.id,
3332
- ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
3454
+ ratePolicy,
3333
3455
  invalidFieldIds: v.invalidRateFieldIds
3334
3456
  });
3335
3457
  for (const diag of diags) {
@@ -3372,7 +3494,7 @@ function collectFailedFallbacks(props, services, settings) {
3372
3494
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
3373
3495
  const out = [];
3374
3496
  const fb = (_a = props.fallbacks) != null ? _a : {};
3375
- const primaryRate = (p) => rateOf(services, p);
3497
+ const primaryRate = (primary) => rateOf(services, primary);
3376
3498
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
3377
3499
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3378
3500
  if (!primary) {
@@ -3385,34 +3507,34 @@ function collectFailedFallbacks(props, services, settings) {
3385
3507
  });
3386
3508
  continue;
3387
3509
  }
3388
- for (const cand of list) {
3389
- const cap = getCap(services, cand);
3390
- if (!cap) {
3510
+ for (const candidate of list) {
3511
+ const capability = getCap(services, candidate);
3512
+ if (!capability) {
3391
3513
  out.push({
3392
3514
  scope: "node",
3393
3515
  nodeId,
3394
3516
  primary,
3395
- candidate: cand,
3517
+ candidate,
3396
3518
  reason: "unknown_service"
3397
3519
  });
3398
3520
  continue;
3399
3521
  }
3400
- if (String(cand) === String(primary)) {
3522
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3401
3523
  out.push({
3402
3524
  scope: "node",
3403
3525
  nodeId,
3404
3526
  primary,
3405
- candidate: cand,
3527
+ candidate,
3406
3528
  reason: "cycle"
3407
3529
  });
3408
3530
  continue;
3409
3531
  }
3410
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3532
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3411
3533
  out.push({
3412
3534
  scope: "node",
3413
3535
  nodeId,
3414
3536
  primary,
3415
- candidate: cand,
3537
+ candidate,
3416
3538
  reason: "rate_violation"
3417
3539
  });
3418
3540
  continue;
@@ -3422,58 +3544,55 @@ function collectFailedFallbacks(props, services, settings) {
3422
3544
  scope: "node",
3423
3545
  nodeId,
3424
3546
  primary,
3425
- candidate: cand,
3547
+ candidate,
3426
3548
  reason: "no_tag_context"
3427
3549
  });
3428
3550
  continue;
3429
3551
  }
3430
- let anyPass = false;
3431
- let anyFail = false;
3432
3552
  for (const tagId of tagContexts) {
3433
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
3434
- if (ok) anyPass = true;
3435
- else {
3436
- anyFail = true;
3437
- out.push({
3438
- scope: "node",
3439
- nodeId,
3440
- primary,
3441
- candidate: cand,
3442
- tagContext: tagId,
3443
- reason: "constraint_mismatch"
3444
- });
3445
- }
3553
+ const fitsConstraints = s.requireConstraintFit ? satisfiesTagConstraints(
3554
+ tagId,
3555
+ { services, props },
3556
+ capability
3557
+ ) : true;
3558
+ if (fitsConstraints) continue;
3559
+ out.push({
3560
+ scope: "node",
3561
+ nodeId,
3562
+ primary,
3563
+ candidate,
3564
+ tagContext: tagId,
3565
+ reason: "constraint_mismatch"
3566
+ });
3446
3567
  }
3447
- void anyPass;
3448
- void anyFail;
3449
3568
  }
3450
3569
  }
3451
3570
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
3452
- for (const cand of list) {
3453
- const cap = getCap(services, cand);
3454
- if (!cap) {
3571
+ for (const candidate of list) {
3572
+ const capability = getCap(services, candidate);
3573
+ if (!capability) {
3455
3574
  out.push({
3456
3575
  scope: "global",
3457
3576
  primary,
3458
- candidate: cand,
3577
+ candidate,
3459
3578
  reason: "unknown_service"
3460
3579
  });
3461
3580
  continue;
3462
3581
  }
3463
- if (String(cand) === String(primary)) {
3582
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3464
3583
  out.push({
3465
3584
  scope: "global",
3466
3585
  primary,
3467
- candidate: cand,
3586
+ candidate,
3468
3587
  reason: "cycle"
3469
3588
  });
3470
3589
  continue;
3471
3590
  }
3472
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3591
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3473
3592
  out.push({
3474
3593
  scope: "global",
3475
3594
  primary,
3476
- candidate: cand,
3595
+ candidate,
3477
3596
  reason: "rate_violation"
3478
3597
  });
3479
3598
  }
@@ -3482,52 +3601,61 @@ function collectFailedFallbacks(props, services, settings) {
3482
3601
  return out;
3483
3602
  }
3484
3603
  function rateOf(map, id) {
3485
- var _a;
3604
+ var _a, _b;
3486
3605
  if (id === void 0 || id === null) return void 0;
3487
- const c = getCap(map, id);
3488
- return (_a = c == null ? void 0 : c.rate) != null ? _a : void 0;
3606
+ return (_b = (_a = getCap(map, id)) == null ? void 0 : _a.rate) != null ? _b : void 0;
3489
3607
  }
3490
- function passesRate(policy, primaryRate, candRate) {
3491
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
3608
+ function passesRate(policy, primaryRate, candidateRate) {
3609
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
3492
3610
  return false;
3493
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
3611
+ }
3612
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
3494
3613
  return false;
3495
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
3614
+ }
3615
+ return passesRatePolicy(
3616
+ normalizeRatePolicy(policy),
3617
+ primaryRate,
3618
+ candidateRate
3619
+ );
3496
3620
  }
3497
3621
  function getCap(map, id) {
3498
3622
  return getServiceCapability(map, id);
3499
3623
  }
3500
- function isCapFlagEnabled(cap, flagId) {
3624
+ function isCapFlagEnabled(capability, flagId) {
3501
3625
  var _a, _b;
3502
- const fromFlags = (_b = (_a = cap.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
3626
+ const fromFlags = (_b = (_a = capability.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
3503
3627
  if (fromFlags === true) return true;
3504
3628
  if (fromFlags === false) return false;
3505
- const legacy = cap[flagId];
3629
+ const legacy = capability[flagId];
3506
3630
  return legacy === true;
3507
3631
  }
3508
- function satisfiesTagConstraints(tagId, ctx, cap) {
3509
- const tag = ctx.props.filters.find((t) => t.id === tagId);
3510
- const eff = tag == null ? void 0 : tag.constraints;
3511
- if (!eff) return true;
3512
- for (const [key, value] of Object.entries(eff)) {
3513
- if (value === true && !isCapFlagEnabled(cap, key)) {
3632
+ function satisfiesTagConstraints(tagId, ctx, capability) {
3633
+ const tag = ctx.props.filters.find((item) => item.id === tagId);
3634
+ const effectiveConstraints2 = tag == null ? void 0 : tag.constraints;
3635
+ if (!effectiveConstraints2) return true;
3636
+ for (const [key, value] of Object.entries(effectiveConstraints2)) {
3637
+ if (value === true && !isCapFlagEnabled(capability, key)) {
3514
3638
  return false;
3515
3639
  }
3516
3640
  }
3517
3641
  return true;
3518
3642
  }
3519
3643
  function primaryForNode(props, nodeId) {
3520
- const tag = props.filters.find((t) => t.id === nodeId);
3644
+ const tag = props.filters.find((item) => item.id === nodeId);
3521
3645
  if (tag) {
3522
3646
  return { primary: tag.service_id, tagContexts: [tag.id] };
3523
3647
  }
3524
3648
  const field = props.fields.find(
3525
- (f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId)
3649
+ (item) => Array.isArray(item.options) && item.options.some((option2) => option2.id === nodeId)
3526
3650
  );
3527
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3528
- const opt = field.options.find((o) => o.id === nodeId);
3529
- const contexts = bindIdsToArray(field.bind_id);
3530
- return { primary: opt.service_id, tagContexts: contexts };
3651
+ if (!field) {
3652
+ return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3653
+ }
3654
+ const option = field.options.find((item) => item.id === nodeId);
3655
+ return {
3656
+ primary: option.service_id,
3657
+ tagContexts: bindIdsToArray(field.bind_id)
3658
+ };
3531
3659
  }
3532
3660
  function bindIdsToArray(bind) {
3533
3661
  if (!bind) return [];
@@ -3537,43 +3665,54 @@ function getEligibleFallbacks(params) {
3537
3665
  var _a, _b, _c, _d, _e, _f;
3538
3666
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
3539
3667
  const { primary, nodeId, tagId, services } = params;
3540
- const fb = (_b = params.fallbacks) != null ? _b : {};
3541
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
3542
- excludes.add(String(primary));
3543
- const unique = (_d = params.unique) != null ? _d : true;
3544
- const lists = [];
3545
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
3546
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
3547
- if (!lists.length) return [];
3668
+ const excludes = /* @__PURE__ */ new Set();
3669
+ for (const ref of (_b = params.exclude) != null ? _b : []) {
3670
+ addComparableServiceRef(excludes, services, ref);
3671
+ }
3672
+ addComparableServiceRef(excludes, services, primary);
3673
+ const source = (_c = params.source) != null ? _c : "registered";
3674
+ const candidateLists = source === "all_services" ? [listServicePoolCandidates(services)] : listRegisteredFallbackCandidates(
3675
+ (_d = params.fallbacks) != null ? _d : {},
3676
+ primary,
3677
+ nodeId,
3678
+ services
3679
+ );
3680
+ if (!candidateLists.length) return [];
3548
3681
  const primaryRate = rateOf(services, primary);
3549
3682
  const seen = /* @__PURE__ */ new Set();
3550
3683
  const eligible = [];
3551
- for (const list of lists) {
3552
- for (const cand of list) {
3553
- const key = String(cand);
3554
- if (excludes.has(key)) continue;
3555
- if (unique && seen.has(key)) continue;
3556
- seen.add(key);
3557
- const cap = getCap(services, cand);
3558
- if (!cap) continue;
3559
- if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
3684
+ for (const list of candidateLists) {
3685
+ for (const candidate of list) {
3686
+ if (hasComparableServiceRef(excludes, services, candidate)) continue;
3687
+ const capability = getCap(services, candidate);
3688
+ if (!capability) continue;
3689
+ const candidateId = (_e = getServiceCapabilityCanonicalRef(services, candidate)) != null ? _e : candidate;
3690
+ const candidateIdentity = getComparableServiceRefKey(
3691
+ services,
3692
+ candidateId
3693
+ );
3694
+ if (((_f = params.unique) != null ? _f : true) && seen.has(candidateIdentity)) continue;
3695
+ seen.add(candidateIdentity);
3696
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
3697
+ continue;
3698
+ }
3560
3699
  if (s.requireConstraintFit && tagId) {
3561
- const ok = satisfiesTagConstraints(
3700
+ const fitsConstraints = satisfiesTagConstraints(
3562
3701
  tagId,
3563
3702
  { props: params.props, services },
3564
- cap
3703
+ capability
3565
3704
  );
3566
- if (!ok) continue;
3705
+ if (!fitsConstraints) continue;
3567
3706
  }
3568
- eligible.push(cand);
3707
+ eligible.push(candidateId);
3569
3708
  }
3570
3709
  }
3571
3710
  if (s.selectionStrategy === "cheapest") {
3572
- eligible.sort((a, b) => {
3711
+ eligible.sort((left, right) => {
3573
3712
  var _a2, _b2;
3574
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
3575
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
3576
- return ra - rb;
3713
+ const leftRate = (_a2 = rateOf(services, left)) != null ? _a2 : Infinity;
3714
+ const rightRate = (_b2 = rateOf(services, right)) != null ? _b2 : Infinity;
3715
+ return leftRate - rightRate;
3577
3716
  });
3578
3717
  }
3579
3718
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -3581,10 +3720,104 @@ function getEligibleFallbacks(params) {
3581
3720
  }
3582
3721
  return eligible;
3583
3722
  }
3723
+ function getAssignedServiceIds(params) {
3724
+ var _a, _b, _c, _d, _e;
3725
+ const seen = /* @__PURE__ */ new Set();
3726
+ const out = [];
3727
+ const push = (value) => {
3728
+ if (!isValidServiceIdRef(value)) return;
3729
+ const key = String(value);
3730
+ if (seen.has(key)) return;
3731
+ seen.add(key);
3732
+ out.push(value);
3733
+ };
3734
+ const props = params.props;
3735
+ if (props) {
3736
+ for (const tag of (_a = props.filters) != null ? _a : []) {
3737
+ push(tag.service_id);
3738
+ }
3739
+ for (const field of (_b = props.fields) != null ? _b : []) {
3740
+ const fieldService = field.service_id;
3741
+ if (field.button === true) {
3742
+ push(fieldService);
3743
+ }
3744
+ for (const option of (_c = field.options) != null ? _c : []) {
3745
+ if (option.pricing_role === "utility") continue;
3746
+ push(option.service_id);
3747
+ }
3748
+ }
3749
+ }
3750
+ const snapshot = params.snapshot;
3751
+ if (snapshot) {
3752
+ for (const serviceId of (_d = snapshot.services) != null ? _d : []) {
3753
+ push(serviceId);
3754
+ }
3755
+ for (const list of Object.values((_e = snapshot.serviceMap) != null ? _e : {})) {
3756
+ for (const serviceId of list != null ? list : []) {
3757
+ push(serviceId);
3758
+ }
3759
+ }
3760
+ }
3761
+ return out;
3762
+ }
3584
3763
  function getFallbackRegistrationInfo(props, nodeId) {
3585
3764
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3586
3765
  return { primary, tagContexts };
3587
3766
  }
3767
+ function listRegisteredFallbackCandidates(fallbacks, primary, nodeId, services) {
3768
+ var _a, _b;
3769
+ const lists = [];
3770
+ if (nodeId && ((_a = fallbacks.nodes) == null ? void 0 : _a[nodeId])) {
3771
+ lists.push(fallbacks.nodes[nodeId]);
3772
+ }
3773
+ for (const [registeredPrimary, list] of Object.entries((_b = fallbacks.global) != null ? _b : {})) {
3774
+ if (!isMatchingServiceRef(services, registeredPrimary, primary)) continue;
3775
+ lists.push(list);
3776
+ }
3777
+ return lists;
3778
+ }
3779
+ function listServicePoolCandidates(services) {
3780
+ const seen = /* @__PURE__ */ new Set();
3781
+ const out = [];
3782
+ for (const [key, capability] of Object.entries(services != null ? services : {})) {
3783
+ const candidate = getServicePoolCandidateId(key, capability);
3784
+ if (!isValidServiceIdRef(candidate)) continue;
3785
+ const identity = getComparableServiceRefKey(services, candidate);
3786
+ if (seen.has(identity)) continue;
3787
+ seen.add(identity);
3788
+ out.push(candidate);
3789
+ }
3790
+ return out;
3791
+ }
3792
+ function getServicePoolCandidateId(key, capability) {
3793
+ var _a;
3794
+ return (_a = getServiceCapabilityCanonicalRef({ [key]: capability }, key)) != null ? _a : key;
3795
+ }
3796
+ function addComparableServiceRef(target, services, value) {
3797
+ for (const ref of getComparableServiceRefs(services, value)) {
3798
+ target.add(ref);
3799
+ }
3800
+ }
3801
+ function hasComparableServiceRef(target, services, value) {
3802
+ return getComparableServiceRefs(services, value).some((ref) => target.has(ref));
3803
+ }
3804
+ function getComparableServiceRefKey(services, value) {
3805
+ if (!isValidServiceIdRef(value)) return "";
3806
+ const canonical = getServiceCapabilityCanonicalRef(services, value);
3807
+ return String(canonical != null ? canonical : value);
3808
+ }
3809
+ function getComparableServiceRefs(services, value) {
3810
+ if (!isValidServiceIdRef(value)) return [];
3811
+ const aliases = getServiceCapabilityAliases(services, value);
3812
+ if (!aliases.length) {
3813
+ return [String(value)];
3814
+ }
3815
+ return aliases.map((ref) => String(ref));
3816
+ }
3817
+ function isMatchingServiceRef(services, left, right) {
3818
+ if (!services) return String(left) === String(right);
3819
+ return isSameServiceCapabilityRef(services, left, right);
3820
+ }
3588
3821
 
3589
3822
  // src/core/tag-relations.ts
3590
3823
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -4203,18 +4436,23 @@ function compilePolicies(raw) {
4203
4436
 
4204
4437
  // src/core/service-filter.ts
4205
4438
  function filterServicesForVisibleGroup(input, deps) {
4206
- var _a, _b, _c, _d, _e, _f;
4439
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4207
4440
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
4441
+ const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
4208
4442
  const { context } = input;
4209
4443
  const usedSet = new Set(context.usedServiceIds.map(String));
4210
4444
  const primary = context.usedServiceIds[0];
4445
+ const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
4446
+ const resolvedRatePolicy = normalizeRatePolicy(
4447
+ (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
4448
+ );
4449
+ const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
4211
4450
  const fb = {
4212
- requireConstraintFit: true,
4213
- ratePolicy: { kind: "lte_primary", pct: 5 },
4214
- selectionStrategy: "priority",
4215
- mode: "strict",
4216
- ...(_d = context.fallback) != null ? _d : {}
4451
+ ...DEFAULT_FALLBACK_SETTINGS,
4452
+ ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
4453
+ ratePolicy: resolvedRatePolicy
4217
4454
  };
4455
+ const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
4218
4456
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
4219
4457
  deps.builder,
4220
4458
  context.tagId,
@@ -4239,11 +4477,11 @@ function filterServicesForVisibleGroup(input, deps) {
4239
4477
  const fitsConstraints = constraintFitOk(
4240
4478
  svcMap,
4241
4479
  cap.id,
4242
- (_e = context.effectiveConstraints) != null ? _e : {}
4480
+ (_k = context.effectiveConstraints) != null ? _k : {}
4243
4481
  );
4244
4482
  const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
4245
4483
  const polRes = evaluatePoliciesRaw(
4246
- (_f = context.policies) != null ? _f : [],
4484
+ policySource,
4247
4485
  [...context.usedServiceIds, id],
4248
4486
  svcMap,
4249
4487
  context.tagId,
@@ -5118,6 +5356,7 @@ function createFallbackEditor(options = {}) {
5118
5356
  const original = cloneFallbacks(options.fallbacks);
5119
5357
  let current = cloneFallbacks(options.fallbacks);
5120
5358
  const props = options.props;
5359
+ const snapshot = options.snapshot;
5121
5360
  const services = (_a = options.services) != null ? _a : {};
5122
5361
  const settings = (_b = options.settings) != null ? _b : {};
5123
5362
  function state() {
@@ -5212,7 +5451,11 @@ function createFallbackEditor(options = {}) {
5212
5451
  const allowed2 = [];
5213
5452
  for (const candidate of normalized) {
5214
5453
  const reasons = [];
5215
- if (String(candidate) === String(context.primary)) {
5454
+ if (isSameServiceCapabilityRef(
5455
+ services,
5456
+ candidate,
5457
+ context.primary
5458
+ )) {
5216
5459
  reasons.push("self_reference");
5217
5460
  }
5218
5461
  if (reasons.length) {
@@ -5300,7 +5543,19 @@ function createFallbackEditor(options = {}) {
5300
5543
  return writeScope(context, []);
5301
5544
  }
5302
5545
  function eligible(context, opt) {
5546
+ var _a2, _b2;
5303
5547
  if (!props) return [];
5548
+ const source = (_a2 = opt == null ? void 0 : opt.source) != null ? _a2 : "all_services";
5549
+ const exclude2 = normalizeCandidateList(
5550
+ [
5551
+ ...(_b2 = opt == null ? void 0 : opt.exclude) != null ? _b2 : [],
5552
+ ...source === "all_services" ? [
5553
+ ...getAssignedServiceIds({ props, snapshot }),
5554
+ ...getScope(context)
5555
+ ] : []
5556
+ ],
5557
+ true
5558
+ );
5304
5559
  if (context.scope === "global") {
5305
5560
  return getEligibleFallbacks({
5306
5561
  primary: context.primary,
@@ -5308,9 +5563,10 @@ function createFallbackEditor(options = {}) {
5308
5563
  fallbacks: current,
5309
5564
  settings,
5310
5565
  props,
5311
- exclude: opt == null ? void 0 : opt.exclude,
5566
+ exclude: exclude2,
5312
5567
  unique: opt == null ? void 0 : opt.unique,
5313
- limit: opt == null ? void 0 : opt.limit
5568
+ limit: opt == null ? void 0 : opt.limit,
5569
+ source
5314
5570
  });
5315
5571
  }
5316
5572
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -5318,14 +5574,19 @@ function createFallbackEditor(options = {}) {
5318
5574
  return getEligibleFallbacks({
5319
5575
  primary: info.primary,
5320
5576
  nodeId: context.nodeId,
5321
- tagId: info.tagContexts[0],
5577
+ tagId: resolveNodeTagContext({
5578
+ nodeId: context.nodeId,
5579
+ snapshot,
5580
+ fallbackTagContexts: info.tagContexts
5581
+ }),
5322
5582
  services,
5323
5583
  fallbacks: current,
5324
5584
  settings,
5325
5585
  props,
5326
- exclude: opt == null ? void 0 : opt.exclude,
5586
+ exclude: exclude2,
5327
5587
  unique: opt == null ? void 0 : opt.unique,
5328
- limit: opt == null ? void 0 : opt.limit
5588
+ limit: opt == null ? void 0 : opt.limit,
5589
+ source
5329
5590
  });
5330
5591
  }
5331
5592
  function writeScope(context, nextList) {
@@ -5383,14 +5644,14 @@ function sameFallbacks(a, b) {
5383
5644
  function normalizeCandidateList(input, preserveOrder) {
5384
5645
  const out = [];
5385
5646
  for (const item of input != null ? input : []) {
5386
- if (!isValidServiceIdRef(item)) continue;
5647
+ if (!isValidServiceIdRef2(item)) continue;
5387
5648
  const exists = out.some((x) => String(x) === String(item));
5388
5649
  if (exists) continue;
5389
5650
  out.push(item);
5390
5651
  }
5391
5652
  return preserveOrder ? out : out;
5392
5653
  }
5393
- function isValidServiceIdRef(value) {
5654
+ function isValidServiceIdRef2(value) {
5394
5655
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
5395
5656
  }
5396
5657
  function clamp(n, min, max) {
@@ -5399,7 +5660,7 @@ function clamp(n, min, max) {
5399
5660
  function getNodeRegistrationInfo(props, nodeId) {
5400
5661
  const tag = props.filters.find((t) => t.id === nodeId);
5401
5662
  if (tag) {
5402
- if (!isValidServiceIdRef(tag.service_id)) {
5663
+ if (!isValidServiceIdRef2(tag.service_id)) {
5403
5664
  return { ok: false, reasons: ["no_primary"] };
5404
5665
  }
5405
5666
  return {
@@ -5412,7 +5673,7 @@ function getNodeRegistrationInfo(props, nodeId) {
5412
5673
  if (!hit) {
5413
5674
  return { ok: false, reasons: ["node_not_found"] };
5414
5675
  }
5415
- if (!isValidServiceIdRef(hit.option.service_id)) {
5676
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
5416
5677
  return { ok: false, reasons: ["no_primary"] };
5417
5678
  }
5418
5679
  return {
@@ -5434,6 +5695,15 @@ function bindIdsToArray2(v) {
5434
5695
  if (Array.isArray(v)) return v.filter(Boolean);
5435
5696
  return v ? [v] : [];
5436
5697
  }
5698
+ function resolveNodeTagContext(params) {
5699
+ var _a, _b, _c;
5700
+ const nodeContexts = (_c = (_b = (_a = params.snapshot) == null ? void 0 : _a.meta) == null ? void 0 : _b.context) == null ? void 0 : _c.nodeContexts;
5701
+ if (nodeContexts && Object.prototype.hasOwnProperty.call(nodeContexts, params.nodeId)) {
5702
+ const tagId = nodeContexts[params.nodeId];
5703
+ return typeof tagId === "string" && tagId.trim().length > 0 ? tagId : void 0;
5704
+ }
5705
+ return params.fallbackTagContexts[0];
5706
+ }
5437
5707
  function mapDiagReason(reason) {
5438
5708
  switch (String(reason)) {
5439
5709
  case "unknown_service":
@@ -6987,6 +7257,8 @@ function filterServicesForVisibleGroup2(ctx, candidates, input) {
6987
7257
  usedServiceIds: input.usedServiceIds,
6988
7258
  effectiveConstraints: input.effectiveConstraints,
6989
7259
  policies: input.policies,
7260
+ ratePolicy: input.ratePolicy,
7261
+ fallbackSettings: input.fallbackSettings,
6990
7262
  fallback: input.fallback
6991
7263
  }
6992
7264
  };
@@ -12123,10 +12395,10 @@ function FallbackEditorInner({ className }) {
12123
12395
  "div",
12124
12396
  {
12125
12397
  className: [
12126
- "min-h-screen bg-zinc-100 p-4 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
12398
+ "h-full min-h-0 overflow-hidden bg-zinc-100 p-4 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
12127
12399
  className
12128
12400
  ].filter(Boolean).join(" "),
12129
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-auto flex max-w-7xl flex-col gap-4", children: [
12401
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex h-full min-h-0 flex-col gap-4", children: [
12130
12402
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12131
12403
  FallbackEditorHeader,
12132
12404
  {
@@ -12138,9 +12410,9 @@ function FallbackEditorInner({ className }) {
12138
12410
  saving: headerSaving
12139
12411
  }
12140
12412
  ),
12141
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "grid gap-4 xl:grid-cols-[300px_minmax(0,1fr)_360px]", children: [
12413
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "grid min-h-0 flex-1 gap-4 overflow-hidden xl:grid-cols-[300px_minmax(0,1fr)_360px]", children: [
12142
12414
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FallbackServiceSidebar, {}),
12143
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex min-h-0 flex-col gap-4", children: [
12415
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: [
12144
12416
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [
12145
12417
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
12146
12418
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: activeServiceId !== void 0 ? `Service #${String(activeServiceId)}` : "No service selected" }),
@@ -12284,7 +12556,7 @@ function FallbackDetailsPanel() {
12284
12556
  () => services.find((s) => String(s.id) === String(activeServiceId)),
12285
12557
  [services, activeServiceId]
12286
12558
  );
12287
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("aside", { className: "flex min-h-0 flex-col gap-4", children: [
12559
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("aside", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: [
12288
12560
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12289
12561
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary service info" }),
12290
12562
  !service ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "mt-3 text-sm text-zinc-500 dark:text-zinc-400", children: "No service selected." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "mt-3 space-y-2 text-sm", children: [
@@ -12446,7 +12718,7 @@ function FallbackSettingsPanel() {
12446
12718
  kind: "lte_primary",
12447
12719
  pct: 5
12448
12720
  };
12449
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12721
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 overflow-y-auto", children: [
12450
12722
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
12451
12723
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
12452
12724
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Fallback settings" }),
@@ -12645,7 +12917,7 @@ function FallbackServiceSidebar() {
12645
12917
  }
12646
12918
  );
12647
12919
  }, [query, services]);
12648
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("aside", { className: "flex min-h-0 flex-col rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12920
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("aside", { className: "flex h-full min-h-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12649
12921
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12650
12922
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary services" }),
12651
12923
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Services currently active in the builder/runtime context." })
@@ -12660,7 +12932,7 @@ function FallbackServiceSidebar() {
12660
12932
  className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
12661
12933
  }
12662
12934
  ),
12663
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-3 flex-1 space-y-2 overflow-auto", children: filtered.map((service) => {
12935
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-3 flex-1 space-y-2 overflow-y-auto", children: filtered.map((service) => {
12664
12936
  var _a, _b;
12665
12937
  const active = String(service.id) === String(activeServiceId);
12666
12938
  const count = get(service.id).length;
@@ -12701,11 +12973,172 @@ function FallbackServiceSidebar() {
12701
12973
  }
12702
12974
 
12703
12975
  // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
12704
- var import_react23 = __toESM(require("react"), 1);
12705
-
12706
- // src/react/fallback-editor/FallbackAddCandidatesDialog.tsx
12707
12976
  var import_react21 = __toESM(require("react"), 1);
12708
12977
  var import_jsx_runtime12 = require("react/jsx-runtime");
12978
+ function FallbackRegistrationsPanel() {
12979
+ const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
12980
+ const registrations = useActiveFallbackRegistrations();
12981
+ const eligibleServices = useEligibleServiceList();
12982
+ const [candidatePickerOpen, setCandidatePickerOpen] = import_react21.default.useState(false);
12983
+ const [candidateContext, setCandidateContext] = import_react21.default.useState(null);
12984
+ const [candidatePrimaryId, setCandidatePrimaryId] = import_react21.default.useState(void 0);
12985
+ const [registrationDialogOpen, setRegistrationDialogOpen] = import_react21.default.useState(false);
12986
+ const makeContext = import_react21.default.useCallback(
12987
+ (registration) => {
12988
+ if (registration.scope === "global") {
12989
+ return {
12990
+ scope: "global",
12991
+ primary: registration.primary
12992
+ };
12993
+ }
12994
+ return {
12995
+ scope: "node",
12996
+ nodeId: registration.scopeId
12997
+ };
12998
+ },
12999
+ []
13000
+ );
13001
+ const openCandidatePicker = import_react21.default.useCallback(
13002
+ (context, primaryId) => {
13003
+ setCandidateContext(context);
13004
+ setCandidatePrimaryId(primaryId);
13005
+ setCandidatePickerOpen(true);
13006
+ },
13007
+ []
13008
+ );
13009
+ if (activeServiceId === void 0 || activeServiceId === null) {
13010
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 ", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "Select a primary service to start editing." }) });
13011
+ }
13012
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
13013
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 overflow-y-auto", children: [
13014
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
13015
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
13016
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
13017
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use eligible services as fallback candidates for the selected primary." })
13018
+ ] }),
13019
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13020
+ "button",
13021
+ {
13022
+ type: "button",
13023
+ onClick: () => setRegistrationDialogOpen(true),
13024
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13025
+ children: "Add registration"
13026
+ }
13027
+ )
13028
+ ] }),
13029
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "space-y-4", children: registrations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "No registrations yet for this primary service." }) : registrations.map((reg, index) => {
13030
+ var _a;
13031
+ const context = makeContext(reg);
13032
+ const candidates = reg.services;
13033
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
13034
+ "div",
13035
+ {
13036
+ className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
13037
+ children: [
13038
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
13039
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
13040
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: reg.scope === "global" ? "Global registration" : `Node \xB7 ${reg.scopeId}` }),
13041
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
13042
+ "Primary #",
13043
+ String(reg.primary)
13044
+ ] })
13045
+ ] }),
13046
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "rounded-full border border-zinc-200 bg-white px-2 py-1 text-[11px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300", children: [
13047
+ reg.scope,
13048
+ reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
13049
+ ] })
13050
+ ] }),
13051
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-4 flex flex-wrap gap-2", children: candidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No fallback services yet." }) : candidates.map((candidate) => {
13052
+ var _a2;
13053
+ const preview = check(context, [
13054
+ candidate
13055
+ ]);
13056
+ const rejected = preview.rejected[0];
13057
+ const tone = rejected ? "border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300" : "border-zinc-200 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200";
13058
+ const service = eligibleServices.find(
13059
+ (s) => String(s.id) === String(candidate)
13060
+ );
13061
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
13062
+ "div",
13063
+ {
13064
+ className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13065
+ children: [
13066
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13067
+ rejected ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13068
+ ", "
13069
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13070
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13071
+ "button",
13072
+ {
13073
+ type: "button",
13074
+ onClick: () => remove2(
13075
+ context,
13076
+ candidate
13077
+ ),
13078
+ className: "text-current/70 hover:text-current",
13079
+ children: "\xD7"
13080
+ }
13081
+ )
13082
+ ]
13083
+ },
13084
+ String(candidate)
13085
+ );
13086
+ }) }),
13087
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mt-4 flex gap-2", children: [
13088
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13089
+ "button",
13090
+ {
13091
+ type: "button",
13092
+ onClick: () => openCandidatePicker(
13093
+ context,
13094
+ reg.primary
13095
+ ),
13096
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800",
13097
+ children: "Add fallback"
13098
+ }
13099
+ ),
13100
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13101
+ "button",
13102
+ {
13103
+ type: "button",
13104
+ onClick: () => clear(context),
13105
+ className: "rounded-xl border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-900/50 dark:bg-zinc-900 dark:text-red-300 dark:hover:bg-red-950/20",
13106
+ children: "Clear"
13107
+ }
13108
+ )
13109
+ ] })
13110
+ ]
13111
+ },
13112
+ `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13113
+ );
13114
+ }) })
13115
+ ] }),
13116
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13117
+ FallbackAddRegistrationDialog,
13118
+ {
13119
+ open: registrationDialogOpen,
13120
+ onClose: () => setRegistrationDialogOpen(false),
13121
+ onSelect: (context, primaryId) => {
13122
+ setRegistrationDialogOpen(false);
13123
+ openCandidatePicker(context, primaryId);
13124
+ }
13125
+ }
13126
+ ),
13127
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13128
+ FallbackAddCandidatesDialog,
13129
+ {
13130
+ open: candidatePickerOpen,
13131
+ onClose: () => setCandidatePickerOpen(false),
13132
+ context: candidateContext,
13133
+ primaryId: candidatePrimaryId
13134
+ }
13135
+ )
13136
+ ] });
13137
+ }
13138
+
13139
+ // src/react/fallback-editor/FallbackAddCandidatesDialog.tsx
13140
+ var import_react25 = __toESM(require("react"), 1);
13141
+ var import_jsx_runtime13 = require("react/jsx-runtime");
12709
13142
  function FallbackAddCandidatesDialog({
12710
13143
  open,
12711
13144
  onClose,
@@ -12714,23 +13147,23 @@ function FallbackAddCandidatesDialog({
12714
13147
  }) {
12715
13148
  const { eligible, addMany } = useFallbackEditor();
12716
13149
  const eligibleServices = useEligibleServiceList();
12717
- const [query, setQuery] = import_react21.default.useState("");
12718
- const [filterEligibleOnly, setFilterEligibleOnly] = import_react21.default.useState(true);
12719
- const [selected, setSelected] = import_react21.default.useState(/* @__PURE__ */ new Set());
12720
- const [submitting, setSubmitting] = import_react21.default.useState(false);
12721
- import_react21.default.useEffect(() => {
13150
+ const [query, setQuery] = import_react25.default.useState("");
13151
+ const [filterEligibleOnly, setFilterEligibleOnly] = import_react25.default.useState(true);
13152
+ const [selected, setSelected] = import_react25.default.useState(/* @__PURE__ */ new Set());
13153
+ const [submitting, setSubmitting] = import_react25.default.useState(false);
13154
+ import_react25.default.useEffect(() => {
12722
13155
  if (!open) {
12723
13156
  setQuery("");
12724
13157
  setFilterEligibleOnly(true);
12725
13158
  setSelected(/* @__PURE__ */ new Set());
12726
13159
  }
12727
13160
  }, [open]);
12728
- const allowedIds = import_react21.default.useMemo(() => {
13161
+ const allowedIds = import_react25.default.useMemo(() => {
12729
13162
  if (!context) return null;
12730
13163
  if (!filterEligibleOnly) return null;
12731
13164
  return new Set(eligible(context).map((id) => String(id)));
12732
13165
  }, [context, filterEligibleOnly, eligible]);
12733
- const items = import_react21.default.useMemo(() => {
13166
+ const items = import_react25.default.useMemo(() => {
12734
13167
  const q = query.trim().toLowerCase();
12735
13168
  return eligibleServices.filter((service) => {
12736
13169
  var _a, _b;
@@ -12765,13 +13198,13 @@ function FallbackAddCandidatesDialog({
12765
13198
  }
12766
13199
  }
12767
13200
  if (!open || !context) return null;
12768
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex max-h-[85vh] w-full max-w-3xl flex-col rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
12769
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12770
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
12771
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Search and select one or more eligible fallback candidates." })
13201
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex max-h-[85vh] w-full max-w-3xl flex-col rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
13202
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13203
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
13204
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Search and select one or more eligible fallback candidates." })
12772
13205
  ] }),
12773
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col gap-3 p-4", children: [
12774
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13206
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-3 p-4", children: [
13207
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
12775
13208
  "input",
12776
13209
  {
12777
13210
  value: query,
@@ -12780,8 +13213,8 @@ function FallbackAddCandidatesDialog({
12780
13213
  className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
12781
13214
  }
12782
13215
  ),
12783
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
12784
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13216
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
13217
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
12785
13218
  "input",
12786
13219
  {
12787
13220
  type: "checkbox",
@@ -12792,7 +13225,7 @@ function FallbackAddCandidatesDialog({
12792
13225
  ),
12793
13226
  "Filter eligible only"
12794
13227
  ] }),
12795
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13228
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
12796
13229
  VirtualServiceList,
12797
13230
  {
12798
13231
  items,
@@ -12802,13 +13235,13 @@ function FallbackAddCandidatesDialog({
12802
13235
  }
12803
13236
  )
12804
13237
  ] }),
12805
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
12806
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
13238
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13239
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
12807
13240
  selected.size,
12808
13241
  " selected"
12809
13242
  ] }),
12810
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-2", children: [
12811
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13243
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex gap-2", children: [
13244
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
12812
13245
  "button",
12813
13246
  {
12814
13247
  type: "button",
@@ -12817,7 +13250,7 @@ function FallbackAddCandidatesDialog({
12817
13250
  children: "Cancel"
12818
13251
  }
12819
13252
  ),
12820
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
13253
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
12821
13254
  "button",
12822
13255
  {
12823
13256
  type: "button",
@@ -12833,8 +13266,8 @@ function FallbackAddCandidatesDialog({
12833
13266
  }
12834
13267
 
12835
13268
  // src/react/fallback-editor/FallbackAddRegistrationDialog.tsx
12836
- var import_react22 = __toESM(require("react"), 1);
12837
- var import_jsx_runtime13 = require("react/jsx-runtime");
13269
+ var import_react26 = __toESM(require("react"), 1);
13270
+ var import_jsx_runtime14 = require("react/jsx-runtime");
12838
13271
  function FallbackAddRegistrationDialog({
12839
13272
  open,
12840
13273
  onClose,
@@ -12842,23 +13275,23 @@ function FallbackAddRegistrationDialog({
12842
13275
  }) {
12843
13276
  const { activeServiceId, serviceProps, snapshot } = useFallbackEditor();
12844
13277
  const registrations = useActiveFallbackRegistrations();
12845
- const [scope, setScope] = import_react22.default.useState("global");
12846
- const [nodeId, setNodeId] = import_react22.default.useState("");
12847
- const mode = import_react22.default.useMemo(() => {
13278
+ const [scope, setScope] = import_react26.default.useState("global");
13279
+ const [nodeId, setNodeId] = import_react26.default.useState("");
13280
+ const mode = import_react26.default.useMemo(() => {
12848
13281
  if (snapshot) return "snapshot";
12849
13282
  if (serviceProps) return "props";
12850
13283
  return "none";
12851
13284
  }, [snapshot, serviceProps]);
12852
- import_react22.default.useEffect(() => {
13285
+ import_react26.default.useEffect(() => {
12853
13286
  if (open) {
12854
13287
  setScope("global");
12855
13288
  setNodeId("");
12856
13289
  }
12857
13290
  }, [open]);
12858
- const hasGlobal = import_react22.default.useMemo(() => {
13291
+ const hasGlobal = import_react26.default.useMemo(() => {
12859
13292
  return registrations.some((r) => r.scope === "global");
12860
13293
  }, [registrations]);
12861
- const nodeTargets = import_react22.default.useMemo(() => {
13294
+ const nodeTargets = import_react26.default.useMemo(() => {
12862
13295
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
12863
13296
  if (activeServiceId === void 0 || activeServiceId === null) {
12864
13297
  return [];
@@ -12948,12 +13381,12 @@ function FallbackAddRegistrationDialog({
12948
13381
  }
12949
13382
  return [];
12950
13383
  }, [mode, snapshot, serviceProps, activeServiceId]);
12951
- import_react22.default.useEffect(() => {
13384
+ import_react26.default.useEffect(() => {
12952
13385
  if (hasGlobal && scope === "global") {
12953
13386
  setScope("node");
12954
13387
  }
12955
13388
  }, [hasGlobal, scope]);
12956
- import_react22.default.useEffect(() => {
13389
+ import_react26.default.useEffect(() => {
12957
13390
  if (scope === "node" && nodeId) {
12958
13391
  const exists = nodeTargets.some((node) => node.id === nodeId);
12959
13392
  if (!exists) setNodeId("");
@@ -12984,17 +13417,17 @@ function FallbackAddRegistrationDialog({
12984
13417
  }
12985
13418
  if (!open) return null;
12986
13419
  const nodeScopeDisabled = nodeTargets.length === 0;
12987
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "w-full max-w-lg rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
12988
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12989
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
12990
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
13420
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "w-full max-w-lg rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
13421
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13422
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
13423
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
12991
13424
  ] }),
12992
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "space-y-4 p-4", children: [
12993
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "space-y-2", children: [
12994
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
12995
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "grid gap-2", children: [
12996
- !hasGlobal && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("label", { className: "flex cursor-pointer items-start gap-3 rounded-xl border border-zinc-200 p-3 dark:border-zinc-800", children: [
12997
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
13425
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "space-y-4 p-4", children: [
13426
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "space-y-2", children: [
13427
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
13428
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "grid gap-2", children: [
13429
+ !hasGlobal && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("label", { className: "flex cursor-pointer items-start gap-3 rounded-xl border border-zinc-200 p-3 dark:border-zinc-800", children: [
13430
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
12998
13431
  "input",
12999
13432
  {
13000
13433
  type: "radio",
@@ -13004,12 +13437,12 @@ function FallbackAddRegistrationDialog({
13004
13437
  className: "mt-1 h-4 w-4"
13005
13438
  }
13006
13439
  ),
13007
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
13008
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
13009
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
13440
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
13441
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
13442
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
13010
13443
  ] })
13011
13444
  ] }),
13012
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
13445
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
13013
13446
  "label",
13014
13447
  {
13015
13448
  className: [
@@ -13017,7 +13450,7 @@ function FallbackAddRegistrationDialog({
13017
13450
  nodeScopeDisabled ? "cursor-not-allowed border-zinc-200 opacity-60 dark:border-zinc-800" : "cursor-pointer border-zinc-200 dark:border-zinc-800"
13018
13451
  ].join(" "),
13019
13452
  children: [
13020
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
13453
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13021
13454
  "input",
13022
13455
  {
13023
13456
  type: "radio",
@@ -13028,26 +13461,26 @@ function FallbackAddRegistrationDialog({
13028
13461
  className: "mt-1 h-4 w-4"
13029
13462
  }
13030
13463
  ),
13031
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
13032
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
13033
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "Pick a node currently active in the order snapshot for this primary service." : mode === "props" ? "Pick a tag, field, or option from ServiceProps that maps to this primary service." : "Node-scoped registration is unavailable without OrderSnapshot or ServiceProps." })
13464
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
13465
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
13466
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "Pick a node currently active in the order snapshot for this primary service." : mode === "props" ? "Pick a tag, field, or option from ServiceProps that maps to this primary service." : "Node-scoped registration is unavailable without OrderSnapshot or ServiceProps." })
13034
13467
  ] })
13035
13468
  ]
13036
13469
  }
13037
13470
  )
13038
13471
  ] })
13039
13472
  ] }),
13040
- scope === "node" && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "space-y-2", children: [
13041
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
13042
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
13473
+ scope === "node" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "space-y-2", children: [
13474
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
13475
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
13043
13476
  "select",
13044
13477
  {
13045
13478
  value: nodeId,
13046
13479
  onChange: (e) => setNodeId(e.target.value),
13047
13480
  className: "w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100",
13048
13481
  children: [
13049
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: "", children: "Select node\u2026" }),
13050
- nodeTargets.map((node) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("option", { value: node.id, children: [
13482
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("option", { value: "", children: "Select node\u2026" }),
13483
+ nodeTargets.map((node) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("option", { value: node.id, children: [
13051
13484
  "[",
13052
13485
  node.kind,
13053
13486
  "] ",
@@ -13058,11 +13491,11 @@ function FallbackAddRegistrationDialog({
13058
13491
  ]
13059
13492
  }
13060
13493
  ),
13061
- nodeScopeDisabled ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "No active snapshot nodes were found for this primary service." : mode === "props" ? "No ServiceProps nodes were found for this primary service." : "Node-scoped registration requires either OrderSnapshot or ServiceProps." }) : null
13494
+ nodeScopeDisabled ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "No active snapshot nodes were found for this primary service." : mode === "props" ? "No ServiceProps nodes were found for this primary service." : "Node-scoped registration requires either OrderSnapshot or ServiceProps." }) : null
13062
13495
  ] })
13063
13496
  ] }),
13064
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13065
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
13497
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13498
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13066
13499
  "button",
13067
13500
  {
13068
13501
  type: "button",
@@ -13071,7 +13504,7 @@ function FallbackAddRegistrationDialog({
13071
13504
  children: "Cancel"
13072
13505
  }
13073
13506
  ),
13074
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
13507
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13075
13508
  "button",
13076
13509
  {
13077
13510
  type: "button",
@@ -13114,169 +13547,6 @@ function resolveNodeMeta(props, nodeId) {
13114
13547
  }
13115
13548
  return { kind: "node", label: nodeId };
13116
13549
  }
13117
-
13118
- // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
13119
- var import_jsx_runtime14 = require("react/jsx-runtime");
13120
- function FallbackRegistrationsPanel() {
13121
- const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
13122
- const registrations = useActiveFallbackRegistrations();
13123
- const eligibleServices = useEligibleServiceList();
13124
- const [candidatePickerOpen, setCandidatePickerOpen] = import_react23.default.useState(false);
13125
- const [candidateContext, setCandidateContext] = import_react23.default.useState(null);
13126
- const [candidatePrimaryId, setCandidatePrimaryId] = import_react23.default.useState(void 0);
13127
- const [registrationDialogOpen, setRegistrationDialogOpen] = import_react23.default.useState(false);
13128
- const makeContext = import_react23.default.useCallback(
13129
- (registration) => {
13130
- if (registration.scope === "global") {
13131
- return {
13132
- scope: "global",
13133
- primary: registration.primary
13134
- };
13135
- }
13136
- return {
13137
- scope: "node",
13138
- nodeId: registration.scopeId
13139
- };
13140
- },
13141
- []
13142
- );
13143
- const openCandidatePicker = import_react23.default.useCallback(
13144
- (context, primaryId) => {
13145
- setCandidateContext(context);
13146
- setCandidatePrimaryId(primaryId);
13147
- setCandidatePickerOpen(true);
13148
- },
13149
- []
13150
- );
13151
- if (activeServiceId === void 0 || activeServiceId === null) {
13152
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "Select a primary service to start editing." }) });
13153
- }
13154
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
13155
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
13156
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
13157
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
13158
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
13159
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use eligible services as fallback candidates for the selected primary." })
13160
- ] }),
13161
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13162
- "button",
13163
- {
13164
- type: "button",
13165
- onClick: () => setRegistrationDialogOpen(true),
13166
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13167
- children: "Add registration"
13168
- }
13169
- )
13170
- ] }),
13171
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "space-y-4", children: registrations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "No registrations yet for this primary service." }) : registrations.map((reg, index) => {
13172
- var _a;
13173
- const context = makeContext(reg);
13174
- const candidates = reg.services;
13175
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
13176
- "div",
13177
- {
13178
- className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
13179
- children: [
13180
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
13181
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
13182
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: reg.scope === "global" ? "Global registration" : `Node \xB7 ${reg.scopeId}` }),
13183
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
13184
- "Primary #",
13185
- String(reg.primary)
13186
- ] })
13187
- ] }),
13188
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "rounded-full border border-zinc-200 bg-white px-2 py-1 text-[11px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300", children: [
13189
- reg.scope,
13190
- reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
13191
- ] })
13192
- ] }),
13193
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "mt-4 flex flex-wrap gap-2", children: candidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No fallback services yet." }) : candidates.map((candidate) => {
13194
- var _a2;
13195
- const preview = check(context, [
13196
- candidate
13197
- ]);
13198
- const rejected = preview.rejected[0];
13199
- const tone = rejected ? "border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300" : "border-zinc-200 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200";
13200
- const service = eligibleServices.find(
13201
- (s) => String(s.id) === String(candidate)
13202
- );
13203
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
13204
- "div",
13205
- {
13206
- className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13207
- children: [
13208
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13209
- rejected ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13210
- ", "
13211
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13212
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13213
- "button",
13214
- {
13215
- type: "button",
13216
- onClick: () => remove2(
13217
- context,
13218
- candidate
13219
- ),
13220
- className: "text-current/70 hover:text-current",
13221
- children: "\xD7"
13222
- }
13223
- )
13224
- ]
13225
- },
13226
- String(candidate)
13227
- );
13228
- }) }),
13229
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "mt-4 flex gap-2", children: [
13230
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13231
- "button",
13232
- {
13233
- type: "button",
13234
- onClick: () => openCandidatePicker(
13235
- context,
13236
- reg.primary
13237
- ),
13238
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800",
13239
- children: "Add fallback"
13240
- }
13241
- ),
13242
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13243
- "button",
13244
- {
13245
- type: "button",
13246
- onClick: () => clear(context),
13247
- className: "rounded-xl border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-900/50 dark:bg-zinc-900 dark:text-red-300 dark:hover:bg-red-950/20",
13248
- children: "Clear"
13249
- }
13250
- )
13251
- ] })
13252
- ]
13253
- },
13254
- `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13255
- );
13256
- }) })
13257
- ] }),
13258
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13259
- FallbackAddRegistrationDialog,
13260
- {
13261
- open: registrationDialogOpen,
13262
- onClose: () => setRegistrationDialogOpen(false),
13263
- onSelect: (context, primaryId) => {
13264
- setRegistrationDialogOpen(false);
13265
- openCandidatePicker(context, primaryId);
13266
- }
13267
- }
13268
- ),
13269
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
13270
- FallbackAddCandidatesDialog,
13271
- {
13272
- open: candidatePickerOpen,
13273
- onClose: () => setCandidatePickerOpen(false),
13274
- context: candidateContext,
13275
- primaryId: candidatePrimaryId
13276
- }
13277
- )
13278
- ] });
13279
- }
13280
13550
  // Annotate the CommonJS export names for ESM import in node:
13281
13551
  0 && (module.exports = {
13282
13552
  CanvasAPI,