@timeax/digital-service-engine 0.2.7 → 0.2.9

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.
@@ -607,13 +607,7 @@ function resolveRootTags(tags) {
607
607
  const roots = tags.filter((t) => !t.bind_id);
608
608
  return roots.length ? roots : tags.slice(0, 1);
609
609
  }
610
- function isEffectfulTrigger(v, trigger) {
611
- var _a, _b, _c, _d, _e, _f;
612
- const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
613
- const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
614
- return ((_d = (_c = inc[trigger]) == null ? void 0 : _c.length) != null ? _d : 0) > 0 || ((_f = (_e = exc[trigger]) == null ? void 0 : _e.length) != null ? _f : 0) > 0;
615
- }
616
- function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
610
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
617
611
  var _a;
618
612
  const visible = visibleFieldsUnder(v.props, tagId, {
619
613
  selectedKeys
@@ -622,11 +616,11 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectfu
622
616
  for (const f of visible) {
623
617
  if (f.button === true) {
624
618
  const t = f.id;
625
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
619
+ if (effectfulKeys.has(t)) triggers.push(t);
626
620
  }
627
621
  for (const o of (_a = f.options) != null ? _a : []) {
628
- const t = `${f.id}::${o.id}`;
629
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
622
+ const t = o.id;
623
+ if (effectfulKeys.has(t)) triggers.push(t);
630
624
  }
631
625
  }
632
626
  triggers.sort();
@@ -726,20 +720,38 @@ function dedupeErrorsInPlace(v, startIndex) {
726
720
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
727
721
  }
728
722
  function validateVisibility(v, options = {}) {
729
- var _a, _b, _c;
723
+ var _a, _b, _c, _d, _e;
724
+ v.simulatedVisibilityContexts = [];
730
725
  const simulate = options.simulate === true;
731
726
  if (!simulate) {
732
727
  runVisibilityRulesOnce(v);
728
+ for (const tag of v.tags) {
729
+ v.simulatedVisibilityContexts.push({
730
+ tagId: tag.id,
731
+ selectedKeys: Array.from(v.selectedKeys),
732
+ visibleFieldIds: v.fieldsVisibleUnder(tag.id).map((f) => f.id)
733
+ });
734
+ }
733
735
  return;
734
736
  }
735
737
  const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
736
738
  const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
737
739
  const onlyEffectful = options.onlyEffectfulTriggers !== false;
740
+ const effectfulKeys = /* @__PURE__ */ new Set();
741
+ if (onlyEffectful) {
742
+ for (const key of Object.keys((_c = v.props.includes_for_buttons) != null ? _c : {})) {
743
+ effectfulKeys.add(key);
744
+ }
745
+ for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
746
+ effectfulKeys.add(key);
747
+ }
748
+ }
738
749
  const roots = resolveRootTags(v.tags);
739
750
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
740
- const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
751
+ const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
741
752
  const errorsStart = v.errors.length;
742
753
  const visited = /* @__PURE__ */ new Set();
754
+ const seenContexts = /* @__PURE__ */ new Set();
743
755
  const stack = [];
744
756
  for (const rt of rootTags) {
745
757
  stack.push({
@@ -752,10 +764,27 @@ function validateVisibility(v, options = {}) {
752
764
  while (stack.length) {
753
765
  if (validatedStates >= maxStates) break;
754
766
  const state = stack.pop();
755
- const sig = stableKeyOfSelection(state.selected);
767
+ const sig = `${state.rootTagId}::${stableKeyOfSelection(state.selected)}`;
756
768
  if (visited.has(sig)) continue;
757
769
  visited.add(sig);
758
770
  v.selectedKeys = state.selected;
771
+ const visibleNow = visibleFieldsUnder(v.props, state.rootTagId, {
772
+ selectedKeys: state.selected
773
+ }).map((f) => f.id);
774
+ const context = {
775
+ tagId: state.rootTagId,
776
+ selectedKeys: Array.from(state.selected),
777
+ visibleFieldIds: visibleNow
778
+ };
779
+ const contextKey = [
780
+ context.tagId,
781
+ [...context.selectedKeys].sort().join("|"),
782
+ [...context.visibleFieldIds].sort().join("|")
783
+ ].join("::");
784
+ if (!seenContexts.has(contextKey)) {
785
+ seenContexts.add(contextKey);
786
+ v.simulatedVisibilityContexts.push(context);
787
+ }
759
788
  validatedStates++;
760
789
  runVisibilityRulesOnce(v);
761
790
  if (state.depth >= maxDepth) continue;
@@ -763,7 +792,7 @@ function validateVisibility(v, options = {}) {
763
792
  v,
764
793
  state.rootTagId,
765
794
  state.selected,
766
- onlyEffectful
795
+ effectfulKeys
767
796
  );
768
797
  for (let i = triggers.length - 1; i >= 0; i--) {
769
798
  const trig = triggers[i];
@@ -1535,1273 +1564,1335 @@ function validateRates(v) {
1535
1564
  }
1536
1565
  }
1537
1566
 
1538
- // src/core/validate/steps/constraints.ts
1539
- function constraintKeysInChain(v, tagId) {
1540
- const keys = [];
1541
- const seenKeys = /* @__PURE__ */ new Set();
1542
- let cur = tagId;
1543
- const seenTags = /* @__PURE__ */ new Set();
1544
- while (cur && !seenTags.has(cur)) {
1545
- seenTags.add(cur);
1546
- const t = v.tagById.get(cur);
1547
- const c = t == null ? void 0 : t.constraints;
1548
- if (c && typeof c === "object") {
1549
- for (const k of Object.keys(c)) {
1550
- if (!seenKeys.has(k)) {
1551
- seenKeys.add(k);
1552
- keys.push(k);
1553
- }
1554
- }
1555
- }
1556
- cur = t == null ? void 0 : t.bind_id;
1557
- }
1558
- return keys;
1559
- }
1560
- function effectiveConstraints(v, tagId) {
1561
- var _a;
1562
- const out = {};
1563
- const keys = constraintKeysInChain(v, tagId);
1564
- for (const key of keys) {
1565
- let cur = tagId;
1566
- const seen = /* @__PURE__ */ new Set();
1567
- while (cur && !seen.has(cur)) {
1568
- seen.add(cur);
1569
- const t = v.tagById.get(cur);
1570
- const val = (_a = t == null ? void 0 : t.constraints) == null ? void 0 : _a[key];
1571
- if (val === true || val === false) {
1572
- out[key] = val;
1573
- break;
1574
- }
1575
- cur = t == null ? void 0 : t.bind_id;
1576
- }
1567
+ // src/core/rate-coherence.ts
1568
+ function uniqueStrings(values) {
1569
+ const out = /* @__PURE__ */ new Set();
1570
+ for (const value of values) {
1571
+ if (!value) continue;
1572
+ out.add(value);
1577
1573
  }
1578
- return out;
1574
+ return Array.from(out);
1579
1575
  }
1580
- function validateConstraints(v) {
1576
+ function buildTriggerEffectMap(props) {
1581
1577
  var _a, _b;
1582
- for (const t of v.tags) {
1583
- const eff = effectiveConstraints(v, t.id);
1584
- const hasAnyRequired = Object.values(eff).some(
1585
- (x) => x === true
1586
- );
1587
- if (!hasAnyRequired) continue;
1588
- const visible = v.fieldsVisibleUnder(t.id);
1589
- for (const f of visible) {
1590
- for (const o of (_a = f.options) != null ? _a : []) {
1591
- if (!isServiceIdRef(o.service_id)) continue;
1592
- const svc = getServiceCapability(v.serviceMap, o.service_id);
1593
- if (!svc || typeof svc !== "object") continue;
1594
- for (const [k, val] of Object.entries(eff)) {
1595
- if (val === true && !isServiceFlagEnabled(svc, k)) {
1596
- v.errors.push({
1597
- code: "unsupported_constraint",
1598
- severity: "error",
1599
- message: `Service option "${o.id}" under tag "${t.id}" does not support required constraint "${k}".`,
1600
- nodeId: t.id,
1601
- details: withAffected(
1602
- {
1603
- flag: k,
1604
- serviceId: o.service_id,
1605
- fieldId: f.id,
1606
- optionId: o.id
1607
- },
1608
- [t.id, f.id, o.id]
1609
- )
1610
- });
1611
- }
1612
- }
1613
- }
1614
- }
1615
- }
1616
- for (const t of v.tags) {
1617
- const sid = t.service_id;
1618
- if (!isServiceIdRef(sid)) continue;
1619
- const svc = getServiceCapability(v.serviceMap, sid);
1620
- if (!svc || typeof svc !== "object") continue;
1621
- const eff = effectiveConstraints(v, t.id);
1622
- for (const [k, val] of Object.entries(eff)) {
1623
- if (val === true && !isServiceFlagEnabled(svc, k)) {
1624
- v.errors.push({
1625
- code: "unsupported_constraint",
1626
- severity: "error",
1627
- message: `Tag "${t.id}" maps to service "${String(
1628
- sid
1629
- )}" which does not support required constraint "${k}".`,
1630
- nodeId: t.id,
1631
- details: { flag: k, serviceId: sid }
1632
- });
1633
- }
1578
+ const map = /* @__PURE__ */ new Map();
1579
+ const ensure = (key) => {
1580
+ let item = map.get(key);
1581
+ if (!item) {
1582
+ item = { includes: /* @__PURE__ */ new Set(), excludes: /* @__PURE__ */ new Set() };
1583
+ map.set(key, item);
1634
1584
  }
1585
+ return item;
1586
+ };
1587
+ for (const [key, ids] of Object.entries((_a = props.includes_for_buttons) != null ? _a : {})) {
1588
+ const item = ensure(key);
1589
+ for (const id of ids != null ? ids : []) item.includes.add(id);
1635
1590
  }
1636
- for (const t of v.tags) {
1637
- const ov = t.constraints_overrides;
1638
- if (!ov || typeof ov !== "object") continue;
1639
- for (const k of Object.keys(ov)) {
1640
- const row = ov[k];
1641
- if (!row) continue;
1642
- const from = row.from === true;
1643
- const to = row.to === true;
1644
- const origin = String((_b = row.origin) != null ? _b : "");
1645
- v.errors.push({
1646
- code: "constraint_overridden",
1647
- severity: "warning",
1648
- message: origin ? `Constraint "${k}" on tag "${t.id}" was overridden by ancestor "${origin}" (${String(from)} \u2192 ${String(
1649
- to
1650
- )}).` : `Constraint "${k}" on tag "${t.id}" was overridden by an ancestor (${String(from)} \u2192 ${String(to)}).`,
1651
- nodeId: t.id,
1652
- details: withAffected(
1653
- { flag: k, from, to, origin },
1654
- origin ? [t.id, origin] : void 0
1655
- )
1656
- });
1657
- }
1591
+ for (const [key, ids] of Object.entries((_b = props.excludes_for_buttons) != null ? _b : {})) {
1592
+ const item = ensure(key);
1593
+ for (const id of ids != null ? ids : []) item.excludes.add(id);
1658
1594
  }
1595
+ return map;
1659
1596
  }
1660
-
1661
- // src/core/validate/steps/custom.ts
1662
- function validateCustomFields(v) {
1663
- for (const f of v.fields) {
1664
- if (f.type !== "custom") continue;
1665
- if (!f.component || !String(f.component).trim()) {
1666
- v.errors.push({
1667
- code: "custom_component_missing",
1668
- severity: "error",
1669
- message: `Custom field "${f.id}" is missing a valid component reference.`,
1670
- nodeId: f.id
1671
- });
1597
+ function isRefExcludedBySelectedKeys(ref, selectedKeys, effectMap) {
1598
+ for (const key of selectedKeys) {
1599
+ const effects = effectMap.get(key);
1600
+ if (!effects) continue;
1601
+ if (ref.fieldId && effects.excludes.has(ref.fieldId) || effects.excludes.has(ref.nodeId)) {
1602
+ return true;
1672
1603
  }
1673
1604
  }
1605
+ return false;
1674
1606
  }
1675
-
1676
- // src/core/validate/steps/global-utility-guard.ts
1677
- function validateGlobalUtilityGuard(v) {
1607
+ function validateRateCoherenceDeep(params) {
1678
1608
  var _a, _b, _c;
1679
- if (!v.options.globalUtilityGuard) return;
1680
- let hasUtility = false;
1681
- let hasBase = false;
1682
- for (const f of v.fields) {
1683
- for (const o of (_a = f.options) != null ? _a : []) {
1684
- if (!isServiceIdRef(o.service_id)) continue;
1685
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1686
- if (role === "base") hasBase = true;
1687
- else if (role === "utility") hasUtility = true;
1688
- if (hasUtility && hasBase) break;
1609
+ const { builder, services, tagId } = params;
1610
+ const ratePolicy = normalizeRatePolicy(params.ratePolicy);
1611
+ const props = builder.getProps();
1612
+ const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
1613
+ const fields = (_b = props.fields) != null ? _b : [];
1614
+ const fieldById = new Map(fields.map((f) => [f.id, f]));
1615
+ const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
1616
+ const tag = tagById.get(tagId);
1617
+ const baselineFieldIds = builder.visibleFields(tagId, []);
1618
+ const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
1619
+ const anchors = collectAnchors(baselineFields);
1620
+ const diagnostics = [];
1621
+ const seen = /* @__PURE__ */ new Set();
1622
+ for (const anchor of anchors) {
1623
+ const selectedKeys = anchor.kind === "option" ? [anchor.id] : [anchor.fieldId];
1624
+ const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
1625
+ const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
1626
+ for (const fieldId of visibleInvalidFieldIds) {
1627
+ const key = `internal|${tagId}|${fieldId}`;
1628
+ if (seen.has(key)) continue;
1629
+ seen.add(key);
1630
+ diagnostics.push({
1631
+ kind: "internal_field",
1632
+ scope: "visible_group",
1633
+ tagId,
1634
+ fieldId,
1635
+ nodeId: fieldId,
1636
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
1637
+ simulationAnchor: {
1638
+ kind: anchor.kind,
1639
+ id: anchor.id,
1640
+ fieldId: anchor.fieldId,
1641
+ label: anchor.label
1642
+ },
1643
+ invalidFieldIds: [fieldId],
1644
+ affectedIds: uniqueStrings([
1645
+ tagId,
1646
+ anchor.id,
1647
+ anchor.fieldId,
1648
+ fieldId
1649
+ ])
1650
+ });
1689
1651
  }
1690
- if (hasUtility && hasBase) break;
1691
- }
1692
- if (hasUtility && !hasBase) {
1693
- v.errors.push({
1694
- code: "utility_without_base",
1695
- severity: "warning",
1696
- message: "Global utility guard: utility-priced options exist but no base-priced options were found.",
1697
- nodeId: "global",
1698
- details: { scope: "global" }
1652
+ const references = visibleFields.flatMap(
1653
+ (field) => collectFieldReferences(field, services)
1654
+ );
1655
+ if (references.length <= 1) continue;
1656
+ const primary = references.reduce((best, current) => {
1657
+ if (current.rate !== best.rate) {
1658
+ return current.rate > best.rate ? current : best;
1659
+ }
1660
+ const bestKey = `${best.fieldId}|${best.nodeId}`;
1661
+ const currentKey = `${current.fieldId}|${current.nodeId}`;
1662
+ return currentKey < bestKey ? current : best;
1699
1663
  });
1664
+ for (const candidate of references) {
1665
+ if (candidate.nodeId === primary.nodeId) continue;
1666
+ if (candidate.fieldId === primary.fieldId) continue;
1667
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
1668
+ continue;
1669
+ }
1670
+ const key = contextualKey(tagId, primary, candidate, ratePolicy);
1671
+ if (seen.has(key)) continue;
1672
+ seen.add(key);
1673
+ diagnostics.push({
1674
+ kind: "contextual",
1675
+ scope: "visible_group",
1676
+ tagId,
1677
+ nodeId: candidate.nodeId,
1678
+ primary: toDiagnosticRef(primary),
1679
+ offender: toDiagnosticRef(candidate),
1680
+ policy: ratePolicy.kind,
1681
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
1682
+ message: explainRateMismatch(
1683
+ ratePolicy,
1684
+ primary,
1685
+ candidate,
1686
+ describeLabel(tag)
1687
+ ),
1688
+ simulationAnchor: {
1689
+ kind: anchor.kind,
1690
+ id: anchor.id,
1691
+ fieldId: anchor.fieldId,
1692
+ label: anchor.label
1693
+ },
1694
+ invalidFieldIds: visibleInvalidFieldIds,
1695
+ affectedIds: uniqueStrings([
1696
+ tagId,
1697
+ ...selectedKeys,
1698
+ anchor.id,
1699
+ anchor.fieldId,
1700
+ primary.nodeId,
1701
+ primary.fieldId,
1702
+ candidate.nodeId,
1703
+ candidate.fieldId
1704
+ ]),
1705
+ affectedServiceIds: uniqueStrings([
1706
+ primary.service_id == null ? void 0 : String(primary.service_id),
1707
+ candidate.service_id == null ? void 0 : String(candidate.service_id)
1708
+ ])
1709
+ });
1710
+ }
1700
1711
  }
1712
+ return diagnostics;
1701
1713
  }
1702
-
1703
- // src/core/validate/steps/unbound.ts
1704
- function validateUnboundFields(v) {
1714
+ function collectAnchors(fields) {
1705
1715
  var _a, _b;
1706
- const boundFieldIds = /* @__PURE__ */ new Set();
1707
- for (const f of v.fields) {
1708
- if (f.bind_id) boundFieldIds.add(f.id);
1709
- }
1710
- const includedByTag = /* @__PURE__ */ new Set();
1711
- for (const t of v.tags) {
1712
- for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
1713
- }
1714
- const includedByOption = /* @__PURE__ */ new Set();
1715
- for (const arr of Object.values((_b = v.props.includes_for_buttons) != null ? _b : {})) {
1716
- for (const id of arr != null ? arr : []) includedByOption.add(id);
1717
- }
1718
- for (const f of v.fields) {
1719
- if (!boundFieldIds.has(f.id) && !includedByTag.has(f.id) && !includedByOption.has(f.id)) {
1720
- v.errors.push({
1721
- code: "field_unbound",
1722
- severity: "error",
1723
- message: `Field "${f.id}" is unbound: it is not bound to any tag and not included by tags or option maps.`,
1724
- nodeId: f.id,
1725
- details: withAffected(
1726
- {
1727
- fieldId: f.id,
1728
- bound: false,
1729
- // exposing these helps editors explain "why"
1730
- includedByTag: includedByTag.has(f.id),
1731
- includedByOption: includedByOption.has(f.id)
1732
- },
1733
- [f.id]
1734
- )
1735
- });
1716
+ const anchors = [];
1717
+ for (const field of fields) {
1718
+ if (!isButton(field)) continue;
1719
+ if (Array.isArray(field.options) && field.options.length > 0) {
1720
+ for (const option of field.options) {
1721
+ anchors.push({
1722
+ kind: "option",
1723
+ id: option.id,
1724
+ fieldId: field.id,
1725
+ label: (_a = option.label) != null ? _a : option.id
1726
+ });
1727
+ }
1728
+ continue;
1736
1729
  }
1730
+ anchors.push({
1731
+ kind: "field",
1732
+ id: field.id,
1733
+ fieldId: field.id,
1734
+ label: (_b = field.label) != null ? _b : field.id
1735
+ });
1737
1736
  }
1737
+ return anchors;
1738
1738
  }
1739
-
1740
- // src/core/validate/steps/fallbacks.ts
1741
- function codeForReason(reason) {
1742
- switch (reason) {
1743
- case "unknown_service":
1744
- return "fallback_unknown_service";
1745
- case "no_primary":
1746
- return "fallback_no_primary";
1747
- case "rate_violation":
1748
- return "fallback_rate_violation";
1749
- case "constraint_mismatch":
1750
- return "fallback_constraint_mismatch";
1751
- case "cycle":
1752
- return "fallback_cycle";
1753
- default:
1754
- return "fallback_bad_node";
1739
+ function collectFieldReferences(field, services) {
1740
+ var _a;
1741
+ const members = collectBaseMembers(field, services);
1742
+ if (members.length === 0) return [];
1743
+ if (isMultiField(field)) {
1744
+ const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
1745
+ return [
1746
+ {
1747
+ refKind: "multi",
1748
+ nodeId: field.id,
1749
+ fieldId: field.id,
1750
+ label: (_a = field.label) != null ? _a : field.id,
1751
+ rate: averageRate,
1752
+ members
1753
+ }
1754
+ ];
1755
1755
  }
1756
+ return members.map((member) => ({
1757
+ refKind: "single",
1758
+ nodeId: member.id,
1759
+ fieldId: field.id,
1760
+ label: member.label,
1761
+ rate: member.rate,
1762
+ service_id: member.service_id,
1763
+ members: [member]
1764
+ }));
1756
1765
  }
1757
- function messageFor(code, d) {
1758
- const n = d.nodeId ? `node "${String(d.nodeId)}"` : "node";
1759
- switch (code) {
1760
- case "fallback_unknown_service":
1761
- return `Fallback candidate "${String(
1762
- d.candidate
1763
- )}" is unknown for ${n}.`;
1764
- case "fallback_no_primary":
1765
- return `Fallback rule has no primary service for ${n}.`;
1766
- case "fallback_rate_violation":
1767
- return `Fallback candidate "${String(
1768
- d.candidate
1769
- )}" violates the base-rate rules for ${n}.`;
1770
- case "fallback_constraint_mismatch":
1771
- return `Fallback candidate "${String(
1772
- d.candidate
1773
- )}" does not satisfy required constraints for ${n}.`;
1774
- case "fallback_cycle":
1775
- return `Fallback rules contain a cycle for ${n}.`;
1776
- default:
1777
- return `Fallback rule is invalid for ${n}.`;
1766
+ function collectBaseMembers(field, services) {
1767
+ var _a, _b, _c;
1768
+ const members = [];
1769
+ if (Array.isArray(field.options) && field.options.length > 0) {
1770
+ for (const option of field.options) {
1771
+ const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
1772
+ if (role2 !== "base") continue;
1773
+ if (option.service_id === void 0 || option.service_id === null) {
1774
+ continue;
1775
+ }
1776
+ const cap2 = getServiceCapability(services, option.service_id);
1777
+ if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
1778
+ continue;
1779
+ }
1780
+ members.push({
1781
+ kind: "option",
1782
+ id: option.id,
1783
+ fieldId: field.id,
1784
+ label: (_b = option.label) != null ? _b : option.id,
1785
+ service_id: option.service_id,
1786
+ rate: cap2.rate
1787
+ });
1788
+ }
1789
+ return members;
1778
1790
  }
1779
- }
1780
- function validateFallbacks(v) {
1781
- var _a, _b, _c, _d;
1782
- const mode = (_b = (_a = v.options.fallbackSettings) == null ? void 0 : _a.mode) != null ? _b : "strict";
1783
- if (!v.props.fallbacks) return;
1784
- const diags = collectFailedFallbacks(v.props, (_c = v.options.serviceMap) != null ? _c : {}, {
1785
- ...v.options.fallbackSettings,
1786
- mode: "dev"
1787
- });
1788
- if (mode !== "strict") return;
1789
- for (const d of diags) {
1790
- if (d.scope === "global") continue;
1791
- const code = codeForReason(
1792
- String((_d = d.reason) != null ? _d : "fallback_bad_node")
1793
- );
1794
- const nodeId = d.nodeId ? String(d.nodeId) : void 0;
1795
- const tagContext = d.tagContext;
1796
- const affectedIds = [];
1797
- if (nodeId) affectedIds.push(nodeId);
1798
- if (typeof tagContext === "string" && tagContext && tagContext !== nodeId)
1799
- affectedIds.push(tagContext);
1800
- v.errors.push({
1801
- code,
1802
- severity: "error",
1803
- message: messageFor(code, {
1804
- nodeId,
1805
- primary: d.primary,
1806
- candidate: d.candidate,
1807
- tagContext,
1808
- scope: d.scope
1809
- }),
1810
- nodeId,
1811
- details: withAffected(
1812
- {
1813
- primary: d.primary,
1814
- candidate: d.candidate,
1815
- tagContext,
1816
- scope: d.scope
1817
- },
1818
- affectedIds.length > 1 ? affectedIds : void 0
1819
- )
1820
- });
1791
+ const role = normalizeRole(field.pricing_role, "base");
1792
+ if (role !== "base") return members;
1793
+ if (field.service_id === void 0 || field.service_id === null) return members;
1794
+ const cap = getServiceCapability(services, field.service_id);
1795
+ if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
1796
+ return members;
1821
1797
  }
1798
+ members.push({
1799
+ kind: "field",
1800
+ id: field.id,
1801
+ fieldId: field.id,
1802
+ label: (_c = field.label) != null ? _c : field.id,
1803
+ service_id: field.service_id,
1804
+ rate: cap.rate
1805
+ });
1806
+ return members;
1822
1807
  }
1823
-
1824
- // src/core/validate/policies/collect-service-items.ts
1825
- function asArray(v) {
1826
- if (v === void 0) return void 0;
1827
- return Array.isArray(v) ? v : [v];
1808
+ function isButton(field) {
1809
+ if (field.button === true) return true;
1810
+ return Array.isArray(field.options) && field.options.length > 0;
1828
1811
  }
1829
- function isServiceIdRef2(v) {
1830
- return typeof v === "string" || typeof v === "number" && Number.isFinite(v);
1812
+ function normalizeRole(role, fallback) {
1813
+ return role === "base" || role === "utility" ? role : fallback;
1831
1814
  }
1832
- function svcSnapshot(serviceMap, sid) {
1833
- const svc = serviceMap[sid];
1834
- if (!svc) return { id: sid };
1835
- const meta = svc.meta && typeof svc.meta === "object" ? svc.meta : {};
1815
+ function toDiagnosticRef(reference) {
1836
1816
  return {
1837
- ...svc,
1838
- id: sid,
1839
- ...meta
1817
+ nodeId: reference.nodeId,
1818
+ fieldId: reference.fieldId,
1819
+ label: reference.label,
1820
+ refKind: reference.refKind,
1821
+ service_id: reference.service_id,
1822
+ rate: reference.rate
1840
1823
  };
1841
1824
  }
1842
- function pushItem(out, next) {
1843
- var _a;
1844
- const key = `${String(next.serviceId)}|${next.role}`;
1845
- const existing = out.get(key);
1846
- if (!existing) {
1847
- out.set(key, {
1848
- tagId: next.tagId,
1849
- fieldId: next.fieldId,
1850
- optionId: next.optionId,
1851
- nodeId: next.nodeId,
1852
- serviceId: next.serviceId,
1853
- role: next.role,
1854
- service: next.service,
1855
- affectedIds: Array.from(new Set(next.affectedIds))
1856
- });
1857
- return;
1858
- }
1859
- const mergedIds = Array.from(
1860
- /* @__PURE__ */ new Set([...existing.affectedIds, ...next.affectedIds])
1861
- );
1862
- out.set(key, {
1863
- ...existing,
1864
- tagId: (_a = existing.tagId) != null ? _a : next.tagId,
1865
- affectedIds: mergedIds
1866
- });
1825
+ function contextualKey(tagId, primary, candidate, ratePolicy) {
1826
+ const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
1827
+ return [
1828
+ "contextual",
1829
+ tagId,
1830
+ primary.fieldId,
1831
+ primary.nodeId,
1832
+ candidate.fieldId,
1833
+ candidate.nodeId,
1834
+ `${ratePolicy.kind}${pctKey}`
1835
+ ].join("|");
1867
1836
  }
1868
- function fieldRoleOf(f, o) {
1837
+ function describeLabel(tag) {
1869
1838
  var _a, _b;
1870
- const roleRaw = (_b = (_a = o == null ? void 0 : o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
1871
- return roleRaw === "utility" ? "utility" : "base";
1839
+ return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
1872
1840
  }
1873
- function applyFilterAllowLists(tagId, fieldId, filter) {
1874
- const tagAllow = asArray(filter == null ? void 0 : filter.tag_id);
1875
- const fieldAllow = asArray(filter == null ? void 0 : filter.field_id);
1876
- if (tagAllow) {
1877
- if (!tagId) return false;
1878
- if (!tagAllow.includes(tagId)) return false;
1841
+ function explainRateMismatch(policy, primary, candidate, where) {
1842
+ var _a, _b;
1843
+ const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
1844
+ const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
1845
+ switch (policy.kind) {
1846
+ case "eq_primary":
1847
+ return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
1848
+ case "lte_primary":
1849
+ return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
1850
+ case "within_pct":
1851
+ return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
1852
+ case "at_least_pct_lower":
1853
+ return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
1879
1854
  }
1880
- if (fieldAllow) {
1881
- if (!fieldId) return false;
1882
- if (!fieldAllow.includes(fieldId)) return false;
1855
+ }
1856
+
1857
+ // src/core/validate/steps/rate-coherence.ts
1858
+ function normalizeRole2(role, fallback) {
1859
+ return role === "base" || role === "utility" ? role : fallback;
1860
+ }
1861
+ function uniqueStrings2(values) {
1862
+ const out = /* @__PURE__ */ new Set();
1863
+ for (const value of values) {
1864
+ if (!value) continue;
1865
+ out.add(value);
1883
1866
  }
1884
- return true;
1867
+ return Array.from(out);
1885
1868
  }
1886
- function collectServiceItems(args) {
1869
+ function getRate(serviceMap, serviceId) {
1870
+ const cap = getServiceCapability(serviceMap, serviceId);
1871
+ const rate = cap == null ? void 0 : cap.rate;
1872
+ if (typeof rate !== "number" || !Number.isFinite(rate)) return void 0;
1873
+ return rate;
1874
+ }
1875
+ function collectContextRefs(tag, visibleFields, serviceMap) {
1887
1876
  var _a, _b, _c, _d, _e;
1888
- const filter = args.filter;
1889
- const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
1890
- const where = filter == null ? void 0 : filter.where;
1891
- const out = /* @__PURE__ */ new Map();
1892
- const addServiceRef = (ref) => {
1893
- if (roleFilter !== "both" && ref.role !== roleFilter) return;
1894
- if (!applyFilterAllowLists(ref.tagId, ref.fieldId, filter)) return;
1895
- const svc = args.serviceMap[ref.serviceId];
1896
- if (where && svc && !matchesWhere(svc, where)) return;
1897
- pushItem(out, {
1898
- ...ref,
1899
- service: svcSnapshot(args.serviceMap, ref.serviceId)
1900
- });
1901
- };
1902
- if (args.mode === "global") {
1903
- for (const t of (_b = args.tags) != null ? _b : []) {
1904
- const sid = t.service_id;
1905
- if (!isServiceIdRef2(sid)) continue;
1906
- addServiceRef({
1907
- tagId: t.id,
1908
- serviceId: sid,
1909
- role: "base",
1910
- affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
1911
- });
1912
- }
1913
- } else if (args.mode === "visible_group") {
1914
- const t = args.tag;
1915
- const sid = t ? t.service_id : void 0;
1916
- if (t && isServiceIdRef2(sid)) {
1917
- addServiceRef({
1918
- tagId: t.id,
1919
- serviceId: sid,
1920
- role: "base",
1921
- affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
1922
- });
1877
+ const serviceRefs = [];
1878
+ let tagDefault;
1879
+ if (tag.service_id !== void 0 && tag.service_id !== null) {
1880
+ const tagRate = getRate(serviceMap, tag.service_id);
1881
+ if (tagRate != null) {
1882
+ tagDefault = {
1883
+ key: tag.id,
1884
+ nodeId: tag.id,
1885
+ nodeKind: "tag",
1886
+ serviceId: tag.service_id,
1887
+ rate: tagRate,
1888
+ label: (_a = tag.label) != null ? _a : tag.id,
1889
+ pricingRole: "base"
1890
+ };
1923
1891
  }
1924
1892
  }
1925
- const fields = (_c = args.fields) != null ? _c : [];
1926
- for (const f of fields) {
1927
- const fSid = f.service_id;
1928
- if (isServiceIdRef2(fSid)) {
1929
- addServiceRef({
1930
- tagId: args.tagId,
1931
- fieldId: f.id,
1932
- serviceId: fSid,
1933
- role: "base",
1934
- affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
1935
- });
1893
+ for (const field of visibleFields) {
1894
+ const fieldRole = normalizeRole2(field.pricing_role, "base");
1895
+ if (field.service_id !== void 0 && field.service_id !== null) {
1896
+ const rate = getRate(serviceMap, field.service_id);
1897
+ if (rate != null) {
1898
+ serviceRefs.push({
1899
+ key: field.id,
1900
+ nodeId: field.id,
1901
+ fieldId: field.id,
1902
+ nodeKind: "button",
1903
+ serviceId: field.service_id,
1904
+ rate,
1905
+ label: (_b = field.label) != null ? _b : field.id,
1906
+ pricingRole: fieldRole
1907
+ });
1908
+ }
1936
1909
  }
1937
- for (const o of (_d = f.options) != null ? _d : []) {
1938
- const oSid = o.service_id;
1939
- if (!isServiceIdRef2(oSid)) continue;
1940
- const role = fieldRoleOf(f, o);
1941
- addServiceRef({
1942
- tagId: args.tagId,
1943
- fieldId: f.id,
1944
- optionId: o.id,
1945
- serviceId: oSid,
1946
- role,
1947
- affectedIds: [
1948
- `field:${f.id}`,
1949
- `option:${o.id}`,
1950
- `service:${String(oSid)}`
1951
- ]
1910
+ for (const option of (_c = field.options) != null ? _c : []) {
1911
+ if (option.service_id === void 0 || option.service_id === null) continue;
1912
+ const rate = getRate(serviceMap, option.service_id);
1913
+ if (rate == null) continue;
1914
+ serviceRefs.push({
1915
+ key: option.id,
1916
+ nodeId: option.id,
1917
+ fieldId: field.id,
1918
+ nodeKind: "option",
1919
+ serviceId: option.service_id,
1920
+ rate,
1921
+ label: (_d = option.label) != null ? _d : option.id,
1922
+ pricingRole: normalizeRole2((_e = option.pricing_role) != null ? _e : field.pricing_role, "base")
1952
1923
  });
1953
1924
  }
1954
1925
  }
1955
- const fb = args.props.fallbacks;
1956
- if (!fb) return Array.from(out.values());
1957
- const addFallbackNode = (nodeId, list) => {
1958
- const arr = Array.isArray(list) ? list : [];
1959
- for (const cand of arr) {
1960
- if (!isServiceIdRef2(cand)) continue;
1961
- addServiceRef({
1962
- tagId: args.tagId,
1963
- nodeId,
1964
- serviceId: cand,
1965
- role: "base",
1966
- affectedIds: [`fallback-node:${nodeId}`, `service:${String(cand)}`]
1967
- });
1968
- }
1969
- };
1970
- const addFallbackGlobal = (primaryKey, list) => {
1971
- const primaryId = primaryKey;
1972
- addServiceRef({
1973
- tagId: args.tagId,
1974
- nodeId: primaryKey,
1975
- serviceId: primaryId,
1976
- role: "base",
1977
- affectedIds: [
1978
- `fallback-global-primary:${primaryKey}`,
1979
- `service:${String(primaryId)}`
1980
- ]
1926
+ return { tagDefault, serviceRefs };
1927
+ }
1928
+ function pickHighestRatePrimary(refs) {
1929
+ return refs.reduce((best, cur) => {
1930
+ if (!best) return cur;
1931
+ if (cur.rate > best.rate) return cur;
1932
+ if (cur.rate < best.rate) return best;
1933
+ return cur.nodeId < best.nodeId ? cur : best;
1934
+ }, void 0);
1935
+ }
1936
+ function validateRateCoherenceForVisibleContext(params) {
1937
+ const { v, tagId, selectedKeys, visibleFieldIds, effectMap, seen } = params;
1938
+ const tag = v.tagById.get(tagId);
1939
+ if (!tag) return;
1940
+ const visibleFields = visibleFieldIds.map((id) => v.fieldById.get(id)).filter(Boolean);
1941
+ const { tagDefault, serviceRefs: allServiceRefs } = collectContextRefs(
1942
+ tag,
1943
+ visibleFields,
1944
+ v.serviceMap
1945
+ );
1946
+ const baseRefs = allServiceRefs.filter((ref) => ref.pricingRole === "base");
1947
+ if (baseRefs.length === 0 && !tagDefault) return;
1948
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1949
+ const visibleInvalidFieldIds = visibleFieldIds.filter(
1950
+ (fieldId) => v.invalidRateFieldIds.has(fieldId)
1951
+ );
1952
+ for (const fieldId of visibleInvalidFieldIds) {
1953
+ const internalKey = [
1954
+ "rate-coherence-internal",
1955
+ tagId,
1956
+ [...selectedKeys].sort().join("|"),
1957
+ fieldId
1958
+ ].join("::");
1959
+ if (seen.has(internalKey)) continue;
1960
+ seen.add(internalKey);
1961
+ v.errors.push({
1962
+ code: "rate_coherence_violation",
1963
+ severity: "error",
1964
+ nodeId: fieldId,
1965
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
1966
+ details: {
1967
+ kind: "internal_field",
1968
+ tagId,
1969
+ selectedKeys: [...selectedKeys],
1970
+ visibleFieldIds: [...visibleFieldIds],
1971
+ fieldId,
1972
+ invalidFieldIds: [fieldId],
1973
+ affectedIds: uniqueStrings2([tagId, ...selectedKeys, fieldId])
1974
+ }
1981
1975
  });
1982
- const arr = Array.isArray(list) ? list : [];
1983
- for (const cand of arr) {
1984
- if (!isServiceIdRef2(cand)) continue;
1985
- addServiceRef({
1986
- tagId: args.tagId,
1987
- nodeId: primaryKey,
1988
- serviceId: cand,
1989
- role: "base",
1990
- affectedIds: [
1991
- `fallback-global:${primaryKey}`,
1992
- `service:${String(cand)}`
1993
- ]
1994
- });
1995
- }
1996
- };
1997
- const includeAllFallbacks = args.mode === "global";
1998
- const includeGroupFallbacks = args.mode === "visible_group";
1999
- const nodes = fb.nodes && typeof fb.nodes === "object" ? fb.nodes : void 0;
2000
- if (nodes) {
2001
- if (includeAllFallbacks) {
2002
- for (const [nodeId, list] of Object.entries(nodes)) {
2003
- addFallbackNode(nodeId, list);
1976
+ }
1977
+ const selectedSet = new Set(selectedKeys);
1978
+ const selectedServiceRefs = baseRefs.filter((ref) => selectedSet.has(ref.key));
1979
+ if (baseRefs.length === 0) return;
1980
+ for (let i = 0; i < baseRefs.length; i++) {
1981
+ for (let j = i + 1; j < baseRefs.length; j++) {
1982
+ const left = baseRefs[i];
1983
+ const right = baseRefs[j];
1984
+ const hypotheticalKeys = [...selectedKeys, left.key, right.key];
1985
+ const survivingRefs = baseRefs.filter(
1986
+ (ref) => !isRefExcludedBySelectedKeys(
1987
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
1988
+ hypotheticalKeys,
1989
+ effectMap
1990
+ )
1991
+ );
1992
+ const survivingSet = new Set(survivingRefs.map((ref) => ref.nodeId));
1993
+ if (!survivingSet.has(left.nodeId) || !survivingSet.has(right.nodeId)) {
1994
+ continue;
2004
1995
  }
2005
- } else if (includeGroupFallbacks) {
2006
- const allowNodes = new Set(
2007
- Array.isArray(args.visibleNodeIds) ? args.visibleNodeIds : []
1996
+ if (survivingRefs.length <= 1) continue;
1997
+ const survivingSelected = survivingRefs.filter(
1998
+ (ref) => selectedSet.has(ref.key)
2008
1999
  );
2009
- for (const nodeId of allowNodes) {
2010
- addFallbackNode(nodeId, nodes[nodeId]);
2000
+ const tagIsCompeting = survivingSelected.length === 0;
2001
+ const primary = pickHighestRatePrimary(survivingRefs);
2002
+ if (!primary) continue;
2003
+ const comparePool = survivingRefs.filter((ref) => ref.nodeId !== primary.nodeId);
2004
+ for (const candidate of comparePool) {
2005
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) continue;
2006
+ const issueKey = [
2007
+ "rate-coherence-context",
2008
+ tagId,
2009
+ [...selectedKeys].sort().join("|"),
2010
+ [...survivingRefs.map((r) => r.nodeId).sort()].join("|"),
2011
+ primary.nodeId,
2012
+ candidate.nodeId,
2013
+ ratePolicy.kind,
2014
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2015
+ ].join("::");
2016
+ if (seen.has(issueKey)) continue;
2017
+ seen.add(issueKey);
2018
+ v.errors.push({
2019
+ code: "rate_coherence_violation",
2020
+ severity: "error",
2021
+ nodeId: candidate.nodeId,
2022
+ message: "Visible service context contains incompatible base service rates.",
2023
+ details: {
2024
+ kind: "selected_context",
2025
+ tagId,
2026
+ selectedKeys: [...selectedKeys],
2027
+ visibleFieldIds: [...visibleFieldIds],
2028
+ primary: {
2029
+ nodeId: primary.nodeId,
2030
+ fieldId: primary.fieldId,
2031
+ service_id: primary.serviceId,
2032
+ serviceId: primary.serviceId,
2033
+ rate: primary.rate
2034
+ },
2035
+ candidate: {
2036
+ nodeId: candidate.nodeId,
2037
+ fieldId: candidate.fieldId,
2038
+ service_id: candidate.serviceId,
2039
+ serviceId: candidate.serviceId,
2040
+ rate: candidate.rate
2041
+ },
2042
+ policy: ratePolicy.kind,
2043
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2044
+ invalidFieldIds: visibleInvalidFieldIds,
2045
+ affectedIds: uniqueStrings2([
2046
+ tagId,
2047
+ ...selectedKeys,
2048
+ primary.nodeId,
2049
+ primary.fieldId,
2050
+ candidate.nodeId,
2051
+ candidate.fieldId,
2052
+ tagIsCompeting ? tagDefault == null ? void 0 : tagDefault.nodeId : void 0
2053
+ ]),
2054
+ affectedServiceIds: uniqueStrings2([
2055
+ String(primary.serviceId),
2056
+ String(candidate.serviceId)
2057
+ ])
2058
+ }
2059
+ });
2011
2060
  }
2012
2061
  }
2013
2062
  }
2014
- const globalFb = fb.global && typeof fb.global === "object" ? fb.global : void 0;
2015
- if (globalFb) {
2016
- if (includeAllFallbacks) {
2017
- for (const [primaryKey, list] of Object.entries(globalFb)) {
2018
- addFallbackGlobal(primaryKey, list);
2019
- }
2020
- } else if (includeGroupFallbacks) {
2021
- const allowPrimaries = new Set(
2022
- ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
2023
- );
2024
- for (const primaryKey of allowPrimaries) {
2025
- const list = globalFb[primaryKey];
2026
- if (list === void 0) continue;
2027
- addFallbackGlobal(primaryKey, list);
2028
- }
2063
+ if (selectedServiceRefs.length === 0 && tagDefault && baseRefs.length > 0) {
2064
+ const survivingByDefault = baseRefs.filter(
2065
+ (ref) => !isRefExcludedBySelectedKeys(
2066
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
2067
+ selectedKeys,
2068
+ effectMap
2069
+ )
2070
+ );
2071
+ for (const candidate of survivingByDefault) {
2072
+ if (passesRatePolicy(ratePolicy, tagDefault.rate, candidate.rate)) continue;
2073
+ const issueKey = [
2074
+ "rate-coherence-default",
2075
+ tagId,
2076
+ [...selectedKeys].sort().join("|"),
2077
+ tagDefault.nodeId,
2078
+ candidate.nodeId,
2079
+ ratePolicy.kind,
2080
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2081
+ ].join("::");
2082
+ if (seen.has(issueKey)) continue;
2083
+ seen.add(issueKey);
2084
+ v.errors.push({
2085
+ code: "rate_coherence_violation",
2086
+ severity: "error",
2087
+ nodeId: candidate.nodeId,
2088
+ message: "Visible service context contains incompatible base service rates.",
2089
+ details: {
2090
+ kind: "selected_context",
2091
+ tagId,
2092
+ selectedKeys: [...selectedKeys],
2093
+ visibleFieldIds: [...visibleFieldIds],
2094
+ primary: {
2095
+ nodeId: tagDefault.nodeId,
2096
+ service_id: tagDefault.serviceId,
2097
+ serviceId: tagDefault.serviceId,
2098
+ rate: tagDefault.rate
2099
+ },
2100
+ candidate: {
2101
+ nodeId: candidate.nodeId,
2102
+ fieldId: candidate.fieldId,
2103
+ service_id: candidate.serviceId,
2104
+ serviceId: candidate.serviceId,
2105
+ rate: candidate.rate
2106
+ },
2107
+ policy: ratePolicy.kind,
2108
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2109
+ invalidFieldIds: visibleInvalidFieldIds,
2110
+ affectedIds: uniqueStrings2([
2111
+ tagId,
2112
+ ...selectedKeys,
2113
+ tagDefault.nodeId,
2114
+ candidate.nodeId,
2115
+ candidate.fieldId
2116
+ ]),
2117
+ affectedServiceIds: uniqueStrings2([
2118
+ String(tagDefault.serviceId),
2119
+ String(candidate.serviceId)
2120
+ ])
2121
+ }
2122
+ });
2029
2123
  }
2030
2124
  }
2031
- return Array.from(out.values());
2032
2125
  }
2033
-
2034
- // src/core/validate/policies/ops.ts
2035
- function evalPolicyOp(op, values, rule) {
2036
- switch (op) {
2037
- case "all_equal": {
2038
- const set = new Set(
2039
- values.map((v) => JSON.stringify(v))
2040
- );
2041
- return set.size <= 1;
2042
- }
2043
- case "no_mix": {
2044
- const set = new Set(
2045
- values.map((v) => JSON.stringify(v))
2046
- );
2047
- return set.size <= 1;
2048
- }
2049
- case "unique": {
2050
- const seen = /* @__PURE__ */ new Set();
2051
- for (const v of values) {
2052
- const k = JSON.stringify(v);
2053
- if (seen.has(k)) return false;
2054
- seen.add(k);
2055
- }
2056
- return true;
2057
- }
2058
- case "all_true": {
2059
- return values.every((v) => v === true);
2060
- }
2061
- case "any_true": {
2062
- return values.some((v) => v === true);
2063
- }
2064
- case "max_count": {
2065
- const limit = typeof rule.value === "number" ? rule.value : Infinity;
2066
- return values.length <= limit;
2067
- }
2068
- case "min_count": {
2069
- const min = typeof rule.value === "number" ? rule.value : 0;
2070
- return values.length >= min;
2071
- }
2072
- default:
2073
- return true;
2126
+ function validateRateCoherence(v) {
2127
+ if (Object.keys(v.serviceMap).length === 0 || v.tags.length === 0) return;
2128
+ const effectMap = buildTriggerEffectMap(v.props);
2129
+ const seen = /* @__PURE__ */ new Set();
2130
+ for (const context of v.simulatedVisibilityContexts) {
2131
+ validateRateCoherenceForVisibleContext({
2132
+ v,
2133
+ tagId: context.tagId,
2134
+ selectedKeys: context.selectedKeys,
2135
+ visibleFieldIds: context.visibleFieldIds,
2136
+ effectMap,
2137
+ seen
2138
+ });
2074
2139
  }
2075
2140
  }
2076
2141
 
2077
- // src/core/validate/policies/apply-policies.ts
2078
- function uniq(arr) {
2079
- return Array.from(new Set(arr));
2080
- }
2081
- function stableSeverity(s) {
2082
- if (s === "warning") return "warning";
2083
- if (s === "error") return "error";
2084
- return "error";
2085
- }
2086
- function defaultPolicyMessage(rule) {
2087
- if (typeof rule.message === "string" && rule.message.trim())
2088
- return rule.message;
2089
- if (typeof rule.label === "string" && rule.label.trim())
2090
- return rule.label.trim();
2091
- return `Policy "${rule.id}" violated`;
2092
- }
2093
- function affectedFromItems(items) {
2094
- var _a;
2095
- const ids = [];
2096
- for (const it of items) {
2097
- for (const x of (_a = it.affectedIds) != null ? _a : []) ids.push(x);
2098
- ids.push(`service:${String(it.serviceId)}`);
2099
- }
2100
- return uniq(ids);
2101
- }
2102
- function visibleGroupNodeIds(tag, fields) {
2103
- var _a;
2104
- const ids = [tag.id];
2105
- for (const f of fields) {
2106
- for (const o of (_a = f.options) != null ? _a : []) {
2107
- ids.push(o.id);
2142
+ // src/core/validate/steps/constraints.ts
2143
+ function constraintKeysInChain(v, tagId) {
2144
+ const keys = [];
2145
+ const seenKeys = /* @__PURE__ */ new Set();
2146
+ let cur = tagId;
2147
+ const seenTags = /* @__PURE__ */ new Set();
2148
+ while (cur && !seenTags.has(cur)) {
2149
+ seenTags.add(cur);
2150
+ const t = v.tagById.get(cur);
2151
+ const c = t == null ? void 0 : t.constraints;
2152
+ if (c && typeof c === "object") {
2153
+ for (const k of Object.keys(c)) {
2154
+ if (!seenKeys.has(k)) {
2155
+ seenKeys.add(k);
2156
+ keys.push(k);
2157
+ }
2158
+ }
2108
2159
  }
2160
+ cur = t == null ? void 0 : t.bind_id;
2109
2161
  }
2110
- return uniq(ids);
2162
+ return keys;
2111
2163
  }
2112
- function visibleGroupPrimaries(tag, fields) {
2164
+ function effectiveConstraints(v, tagId) {
2113
2165
  var _a;
2114
- const prim = [];
2115
- const tagSid = tag.service_id;
2116
- if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
2117
- prim.push(tagSid);
2118
- }
2119
- for (const f of fields) {
2120
- const fsid = f.service_id;
2121
- if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
2122
- prim.push(fsid);
2123
- }
2124
- for (const o of (_a = f.options) != null ? _a : []) {
2125
- const osid = o.service_id;
2126
- if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
2127
- prim.push(osid);
2166
+ const out = {};
2167
+ const keys = constraintKeysInChain(v, tagId);
2168
+ for (const key of keys) {
2169
+ let cur = tagId;
2170
+ const seen = /* @__PURE__ */ new Set();
2171
+ while (cur && !seen.has(cur)) {
2172
+ seen.add(cur);
2173
+ const t = v.tagById.get(cur);
2174
+ const val = (_a = t == null ? void 0 : t.constraints) == null ? void 0 : _a[key];
2175
+ if (val === true || val === false) {
2176
+ out[key] = val;
2177
+ break;
2128
2178
  }
2179
+ cur = t == null ? void 0 : t.bind_id;
2129
2180
  }
2130
2181
  }
2131
- return uniq(prim);
2182
+ return out;
2132
2183
  }
2133
- function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder, tags) {
2134
- var _a, _b, _c, _d, _e;
2135
- if (!(policies == null ? void 0 : policies.length)) return;
2136
- const tagById = /* @__PURE__ */ new Map();
2137
- for (const t of tags) tagById.set(t.id, t);
2138
- for (const rule of policies) {
2139
- const projPath = (_a = rule.projection) != null ? _a : "service.id";
2140
- const severity = stableSeverity(
2141
- rule.severity
2184
+ function validateConstraints(v) {
2185
+ var _a, _b;
2186
+ for (const t of v.tags) {
2187
+ const eff = effectiveConstraints(v, t.id);
2188
+ const hasAnyRequired = Object.values(eff).some(
2189
+ (x) => x === true
2142
2190
  );
2143
- const message = defaultPolicyMessage(rule);
2144
- if (rule.scope === "global") {
2145
- const tagAllow = Array.isArray(
2146
- (_b = rule.filter) == null ? void 0 : _b.tag_id
2147
- ) ? (_c = rule.filter) == null ? void 0 : _c.tag_id : ((_d = rule.filter) == null ? void 0 : _d.tag_id) ? [rule.filter.tag_id] : void 0;
2148
- let items = [];
2149
- if (tagAllow && tagAllow.length) {
2150
- const merged = /* @__PURE__ */ new Map();
2151
- for (const id of tagAllow) {
2152
- const t = tagById.get(id);
2153
- if (!t) continue;
2154
- const visibleFields = fieldsVisibleUnder(t.id);
2155
- const nodeIds = visibleGroupNodeIds(
2156
- t,
2157
- visibleFields
2158
- );
2159
- const primaries = visibleGroupPrimaries(
2160
- t,
2161
- visibleFields
2162
- );
2163
- const sub = collectServiceItems({
2164
- mode: "visible_group",
2165
- props,
2166
- serviceMap,
2167
- tag: t,
2168
- tagId: t.id,
2169
- fields: visibleFields,
2170
- filter: rule.filter,
2171
- visibleNodeIds: nodeIds,
2172
- visiblePrimaries: primaries
2173
- });
2174
- for (const it of sub) {
2175
- const k = `${String(it.serviceId)}|${it.role}`;
2176
- const existing = merged.get(k);
2177
- if (!existing) {
2178
- merged.set(k, it);
2179
- } else {
2180
- merged.set(k, {
2181
- ...existing,
2182
- affectedIds: uniq([
2183
- ...existing.affectedIds,
2184
- ...it.affectedIds
2185
- ])
2186
- });
2187
- }
2191
+ if (!hasAnyRequired) continue;
2192
+ const visible = v.fieldsVisibleUnder(t.id);
2193
+ for (const f of visible) {
2194
+ for (const o of (_a = f.options) != null ? _a : []) {
2195
+ if (!isServiceIdRef(o.service_id)) continue;
2196
+ const svc = getServiceCapability(v.serviceMap, o.service_id);
2197
+ if (!svc || typeof svc !== "object") continue;
2198
+ for (const [k, val] of Object.entries(eff)) {
2199
+ if (val === true && !isServiceFlagEnabled(svc, k)) {
2200
+ v.errors.push({
2201
+ code: "unsupported_constraint",
2202
+ severity: "error",
2203
+ message: `Service option "${o.id}" under tag "${t.id}" does not support required constraint "${k}".`,
2204
+ nodeId: t.id,
2205
+ details: withAffected(
2206
+ {
2207
+ flag: k,
2208
+ serviceId: o.service_id,
2209
+ fieldId: f.id,
2210
+ optionId: o.id
2211
+ },
2212
+ [t.id, f.id, o.id]
2213
+ )
2214
+ });
2188
2215
  }
2189
2216
  }
2190
- items = Array.from(merged.values());
2191
- } else {
2192
- const allFields = (_e = props.fields) != null ? _e : [];
2193
- items = collectServiceItems({
2194
- mode: "global",
2195
- props,
2196
- serviceMap,
2197
- tags,
2198
- fields: allFields,
2199
- filter: rule.filter
2200
- });
2201
- }
2202
- const values = items.map(
2203
- (it) => getByPath(it, projPath)
2204
- );
2205
- if (!evalPolicyOp(rule.op, values, rule)) {
2206
- errors.push({
2207
- code: "policy_violation",
2208
- severity,
2209
- message,
2210
- nodeId: "global",
2211
- details: {
2212
- ruleId: rule.id,
2213
- scope: "global",
2214
- op: rule.op,
2215
- projection: projPath,
2216
- count: items.length,
2217
- affectedIds: affectedFromItems(items)
2218
- }
2219
- });
2220
2217
  }
2221
- continue;
2222
2218
  }
2223
- for (const t of tags) {
2224
- const visibleFields = fieldsVisibleUnder(t.id);
2225
- const nodeIds = visibleGroupNodeIds(t, visibleFields);
2226
- const primaries = visibleGroupPrimaries(t, visibleFields);
2227
- const items = collectServiceItems({
2228
- mode: "visible_group",
2229
- props,
2230
- serviceMap,
2231
- tag: t,
2232
- tagId: t.id,
2233
- fields: visibleFields,
2234
- filter: rule.filter,
2235
- visibleNodeIds: nodeIds,
2236
- visiblePrimaries: primaries
2237
- });
2238
- if (!items.length) continue;
2239
- const values = items.map(
2240
- (it) => getByPath(it, projPath)
2241
- );
2242
- if (!evalPolicyOp(rule.op, values, rule)) {
2243
- errors.push({
2244
- code: "policy_violation",
2245
- severity,
2246
- message,
2219
+ }
2220
+ for (const t of v.tags) {
2221
+ const sid = t.service_id;
2222
+ if (!isServiceIdRef(sid)) continue;
2223
+ const svc = getServiceCapability(v.serviceMap, sid);
2224
+ if (!svc || typeof svc !== "object") continue;
2225
+ const eff = effectiveConstraints(v, t.id);
2226
+ for (const [k, val] of Object.entries(eff)) {
2227
+ if (val === true && !isServiceFlagEnabled(svc, k)) {
2228
+ v.errors.push({
2229
+ code: "unsupported_constraint",
2230
+ severity: "error",
2231
+ message: `Tag "${t.id}" maps to service "${String(
2232
+ sid
2233
+ )}" which does not support required constraint "${k}".`,
2247
2234
  nodeId: t.id,
2248
- details: {
2249
- ruleId: rule.id,
2250
- scope: "visible_group",
2251
- op: rule.op,
2252
- projection: projPath,
2253
- count: items.length,
2254
- affectedIds: affectedFromItems(items)
2255
- }
2235
+ details: { flag: k, serviceId: sid }
2256
2236
  });
2257
2237
  }
2258
2238
  }
2259
2239
  }
2240
+ for (const t of v.tags) {
2241
+ const ov = t.constraints_overrides;
2242
+ if (!ov || typeof ov !== "object") continue;
2243
+ for (const k of Object.keys(ov)) {
2244
+ const row = ov[k];
2245
+ if (!row) continue;
2246
+ const from = row.from === true;
2247
+ const to = row.to === true;
2248
+ const origin = String((_b = row.origin) != null ? _b : "");
2249
+ v.errors.push({
2250
+ code: "constraint_overridden",
2251
+ severity: "warning",
2252
+ message: origin ? `Constraint "${k}" on tag "${t.id}" was overridden by ancestor "${origin}" (${String(from)} \u2192 ${String(
2253
+ to
2254
+ )}).` : `Constraint "${k}" on tag "${t.id}" was overridden by an ancestor (${String(from)} \u2192 ${String(to)}).`,
2255
+ nodeId: t.id,
2256
+ details: withAffected(
2257
+ { flag: k, from, to, origin },
2258
+ origin ? [t.id, origin] : void 0
2259
+ )
2260
+ });
2261
+ }
2262
+ }
2260
2263
  }
2261
2264
 
2262
- // src/core/governance.ts
2263
- var DEFAULT_FALLBACK_SETTINGS = {
2264
- requireConstraintFit: true,
2265
- ratePolicy: { kind: "lte_primary", pct: 5 },
2266
- selectionStrategy: "priority",
2267
- mode: "strict"
2268
- };
2269
- function resolveGlobalRatePolicy(options) {
2270
- return normalizeRatePolicy(options.ratePolicy);
2271
- }
2272
- function resolveFallbackSettings(options) {
2273
- var _a;
2274
- return {
2275
- ...DEFAULT_FALLBACK_SETTINGS,
2276
- ...(_a = options.fallbackSettings) != null ? _a : {}
2277
- };
2278
- }
2279
- function mergeValidatorOptions(defaults = {}, overrides = {}) {
2280
- var _a, _b, _c, _d;
2281
- const mergedFallbackSettings = {
2282
- ...(_a = defaults.fallbackSettings) != null ? _a : {},
2283
- ...(_b = overrides.fallbackSettings) != null ? _b : {}
2284
- };
2285
- return {
2286
- ...defaults,
2287
- ...overrides,
2288
- policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2289
- ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2290
- fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2291
- };
2292
- }
2293
-
2294
- // src/core/builder.ts
2295
- import { cloneDeep as cloneDeep2 } from "lodash-es";
2296
- function createBuilder(opts = {}) {
2297
- return new BuilderImpl(opts);
2298
- }
2299
- var BuilderImpl = class {
2300
- constructor(opts = {}) {
2301
- this.props = {
2302
- filters: [],
2303
- fields: [],
2304
- schema_version: "1.0"
2305
- };
2306
- this.tagById = /* @__PURE__ */ new Map();
2307
- this.fieldById = /* @__PURE__ */ new Map();
2308
- this.optionOwnerById = /* @__PURE__ */ new Map();
2309
- this._nodemap = null;
2310
- this.options = { ...opts };
2311
- }
2312
- /* ───── lifecycle ─────────────────────────────────────────────────────── */
2313
- isTagId(id) {
2314
- return this.tagById.has(id);
2315
- }
2316
- isFieldId(id) {
2317
- return this.fieldById.has(id);
2265
+ // src/core/validate/steps/custom.ts
2266
+ function validateCustomFields(v) {
2267
+ for (const f of v.fields) {
2268
+ if (f.type !== "custom") continue;
2269
+ if (!f.component || !String(f.component).trim()) {
2270
+ v.errors.push({
2271
+ code: "custom_component_missing",
2272
+ severity: "error",
2273
+ message: `Custom field "${f.id}" is missing a valid component reference.`,
2274
+ nodeId: f.id
2275
+ });
2276
+ }
2318
2277
  }
2319
- isOptionId(id) {
2320
- return this.optionOwnerById.has(id);
2278
+ }
2279
+
2280
+ // src/core/validate/steps/global-utility-guard.ts
2281
+ function validateGlobalUtilityGuard(v) {
2282
+ var _a, _b, _c;
2283
+ if (!v.options.globalUtilityGuard) return;
2284
+ let hasUtility = false;
2285
+ let hasBase = false;
2286
+ for (const f of v.fields) {
2287
+ for (const o of (_a = f.options) != null ? _a : []) {
2288
+ if (!isServiceIdRef(o.service_id)) continue;
2289
+ const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
2290
+ if (role === "base") hasBase = true;
2291
+ else if (role === "utility") hasUtility = true;
2292
+ if (hasUtility && hasBase) break;
2293
+ }
2294
+ if (hasUtility && hasBase) break;
2321
2295
  }
2322
- load(raw) {
2323
- const next = normalise(raw, {
2324
- defaultPricingRole: "base",
2325
- constraints: this.getConstraints().map((item) => item.label)
2296
+ if (hasUtility && !hasBase) {
2297
+ v.errors.push({
2298
+ code: "utility_without_base",
2299
+ severity: "warning",
2300
+ message: "Global utility guard: utility-priced options exist but no base-priced options were found.",
2301
+ nodeId: "global",
2302
+ details: { scope: "global" }
2326
2303
  });
2327
- this.props = next;
2328
- this.rebuildIndexes();
2329
- }
2330
- getProps() {
2331
- return this.props;
2332
2304
  }
2333
- setOptions(patch) {
2334
- this.options = { ...this.options, ...patch };
2305
+ }
2306
+
2307
+ // src/core/validate/steps/unbound.ts
2308
+ function validateUnboundFields(v) {
2309
+ var _a, _b;
2310
+ const boundFieldIds = /* @__PURE__ */ new Set();
2311
+ for (const f of v.fields) {
2312
+ if (f.bind_id) boundFieldIds.add(f.id);
2335
2313
  }
2336
- getServiceMap() {
2337
- var _a;
2338
- return (_a = this.options.serviceMap) != null ? _a : {};
2314
+ const includedByTag = /* @__PURE__ */ new Set();
2315
+ for (const t of v.tags) {
2316
+ for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
2339
2317
  }
2340
- getConstraints() {
2341
- var _a;
2342
- const serviceMap = this.getServiceMap();
2343
- const out = /* @__PURE__ */ new Set();
2344
- const guard = /* @__PURE__ */ new Set();
2345
- for (const svc of Object.values(serviceMap)) {
2346
- const flags = (_a = svc.flags) != null ? _a : {};
2347
- for (const flagId of Object.keys(flags)) {
2348
- if (guard.has(flagId)) continue;
2349
- guard.add(flagId);
2350
- out.add({
2351
- id: flagId,
2352
- value: flagId,
2353
- label: flagId,
2354
- description: flags[flagId].description
2355
- });
2356
- }
2357
- }
2358
- return Array.from(out);
2318
+ const includedByOption = /* @__PURE__ */ new Set();
2319
+ for (const arr of Object.values((_b = v.props.includes_for_buttons) != null ? _b : {})) {
2320
+ for (const id of arr != null ? arr : []) includedByOption.add(id);
2359
2321
  }
2360
- /* ───── querying ─────────────────────────────────────────────────────── */
2361
- tree() {
2362
- var _a, _b, _c, _d;
2363
- const nodes = [];
2364
- const edges = [];
2365
- const showSet = toStringSet(this.options.showOptionNodes);
2366
- for (const t of this.props.filters) {
2367
- nodes.push({ id: t.id, kind: "tag", label: t.label });
2368
- }
2369
- for (const t of this.props.filters) {
2370
- if (t.bind_id) {
2371
- edges.push({
2372
- from: t.bind_id,
2373
- to: t.id,
2374
- kind: "child"
2375
- });
2376
- }
2377
- }
2378
- for (const f of this.props.fields) {
2379
- nodes.push({
2380
- id: f.id,
2381
- kind: "field",
2382
- label: f.label,
2383
- bind_type: f.pricing_role === "utility" ? "utility" : f.bind_id ? "bound" : null
2322
+ for (const f of v.fields) {
2323
+ if (!boundFieldIds.has(f.id) && !includedByTag.has(f.id) && !includedByOption.has(f.id)) {
2324
+ v.errors.push({
2325
+ code: "field_unbound",
2326
+ severity: "error",
2327
+ message: `Field "${f.id}" is unbound: it is not bound to any tag and not included by tags or option maps.`,
2328
+ nodeId: f.id,
2329
+ details: withAffected(
2330
+ {
2331
+ fieldId: f.id,
2332
+ bound: false,
2333
+ // exposing these helps editors explain "why"
2334
+ includedByTag: includedByTag.has(f.id),
2335
+ includedByOption: includedByOption.has(f.id)
2336
+ },
2337
+ [f.id]
2338
+ )
2384
2339
  });
2385
2340
  }
2386
- for (const f of this.props.fields) {
2387
- const b = f.bind_id;
2388
- if (Array.isArray(b)) {
2389
- for (const tagId of b)
2390
- edges.push({
2391
- from: tagId,
2392
- to: f.id,
2393
- kind: "bind"
2394
- });
2395
- } else if (typeof b === "string") {
2396
- edges.push({ from: b, to: f.id, kind: "bind" });
2397
- }
2398
- }
2399
- for (const f of this.props.fields) {
2400
- const showOptions = showSet.has(f.id);
2401
- if (!showOptions) continue;
2402
- if (!Array.isArray(f.options)) continue;
2403
- for (const o of f.options) {
2404
- nodes.push({
2405
- id: o.id,
2406
- kind: "option",
2407
- label: o.label
2408
- });
2409
- const e = {
2410
- from: f.id,
2411
- to: o.id,
2412
- kind: "option",
2413
- meta: { ownerField: f.id }
2414
- };
2415
- edges.push(e);
2416
- }
2417
- }
2418
- for (const t of this.props.filters) {
2419
- for (const id of (_a = t.includes) != null ? _a : []) {
2420
- edges.push({ from: t.id, to: id, kind: "include" });
2421
- }
2422
- for (const id of (_b = t.excludes) != null ? _b : []) {
2423
- edges.push({ from: t.id, to: id, kind: "exclude" });
2424
- }
2425
- }
2426
- const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
2427
- const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
2428
- const pushButtonEdge = (keyId, targetFieldId, kind) => {
2429
- var _a2;
2430
- const owner = this.optionOwnerById.get(keyId);
2431
- const ownerFieldId = (_a2 = owner == null ? void 0 : owner.fieldId) != null ? _a2 : this.fieldById.has(keyId) ? keyId : void 0;
2432
- if (!ownerFieldId) return;
2433
- const fromNode = owner && showSet.has(owner.fieldId) ? keyId : ownerFieldId;
2434
- const meta = owner ? showSet.has(owner.fieldId) ? {
2435
- via: "option-visible",
2436
- ownerField: owner.fieldId,
2437
- sourceOption: keyId
2438
- } : {
2439
- via: "option-hidden",
2440
- ownerField: owner.fieldId,
2441
- sourceOption: keyId
2442
- } : { via: "field-button" };
2443
- const e = { from: fromNode, to: targetFieldId, kind, meta };
2444
- edges.push(e);
2445
- };
2446
- for (const [keyId, arr] of Object.entries(incMap)) {
2447
- for (const fid of arr != null ? arr : [])
2448
- pushButtonEdge(keyId, fid, "include");
2449
- }
2450
- for (const [keyId, arr] of Object.entries(excMap)) {
2451
- for (const fid of arr != null ? arr : [])
2452
- pushButtonEdge(keyId, fid, "exclude");
2453
- }
2454
- return { nodes, edges };
2455
2341
  }
2456
- cleanedProps() {
2457
- var _a, _b, _c, _d, _e;
2458
- const fieldIds = new Set(this.props.fields.map((f) => f.id));
2459
- const optionIds = /* @__PURE__ */ new Set();
2460
- this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
2461
- const includedByTag = /* @__PURE__ */ new Set();
2462
- const excludedAnywhere = /* @__PURE__ */ new Set();
2463
- for (const t of this.props.filters) {
2464
- for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
2465
- for (const id of (_b = t.excludes) != null ? _b : []) excludedAnywhere.add(id);
2466
- }
2467
- const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
2468
- const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
2469
- const includedByButtons = /* @__PURE__ */ new Set();
2470
- const referencedKeys = /* @__PURE__ */ new Set();
2471
- const referencedOwnerFields = /* @__PURE__ */ new Set();
2472
- for (const [key, arr] of Object.entries(incMap)) {
2473
- referencedKeys.add(key);
2474
- const owner = this.optionOwnerById.get(key);
2475
- if (owner) referencedOwnerFields.add(owner.fieldId);
2476
- for (const fid of arr != null ? arr : []) {
2477
- includedByButtons.add(fid);
2478
- }
2479
- }
2480
- for (const [key, arr] of Object.entries(excMap)) {
2481
- referencedKeys.add(key);
2482
- const owner = this.optionOwnerById.get(key);
2483
- if (owner) referencedOwnerFields.add(owner.fieldId);
2484
- for (const fid of arr != null ? arr : []) {
2485
- void fid;
2486
- }
2487
- }
2488
- const boundIds = /* @__PURE__ */ new Set();
2489
- for (const f of this.props.fields) {
2490
- const b = f.bind_id;
2491
- if (Array.isArray(b)) b.forEach((id) => boundIds.add(id));
2492
- else if (typeof b === "string") boundIds.add(b);
2493
- }
2494
- const fields = this.props.fields.filter((f) => {
2495
- var _a2;
2496
- const isUtility = ((_a2 = f.pricing_role) != null ? _a2 : "base") === "utility";
2497
- if (!isUtility) return true;
2498
- const bound = !!f.bind_id;
2499
- const included = includedByTag.has(f.id) || includedByButtons.has(f.id);
2500
- const referenced = referencedOwnerFields.has(f.id) || referencedKeys.has(f.id);
2501
- const excluded = excludedAnywhere.has(f.id);
2502
- return bound || included || referenced || !excluded;
2503
- });
2504
- const allowedTargets = new Set(fields.map((f) => f.id));
2505
- const pruneButtons = (src) => {
2506
- if (!src) return void 0;
2507
- const out2 = {};
2508
- for (const [key, arr] of Object.entries(src)) {
2509
- const keyIsValid = optionIds.has(key) || fieldIds.has(key);
2510
- if (!keyIsValid) continue;
2511
- const cleaned = (arr != null ? arr : []).filter(
2512
- (fid) => allowedTargets.has(fid)
2513
- );
2514
- if (cleaned.length) out2[key] = Array.from(new Set(cleaned));
2515
- }
2516
- return Object.keys(out2).length ? out2 : void 0;
2517
- };
2518
- const includes_for_buttons = pruneButtons(
2519
- this.props.includes_for_buttons
2520
- );
2521
- const excludes_for_buttons = pruneButtons(
2522
- this.props.excludes_for_buttons
2523
- );
2524
- const out = {
2525
- filters: this.props.filters.slice(),
2526
- fields,
2527
- ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
2528
- ...includes_for_buttons && { includes_for_buttons },
2529
- ...excludes_for_buttons && { excludes_for_buttons },
2530
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
2531
- // keep fallbacks & other maps as-is
2532
- ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
2533
- };
2534
- return out;
2535
- }
2536
- errors() {
2537
- return validate(this.props, mergeValidatorOptions({}, this.options));
2342
+ }
2343
+
2344
+ // src/core/validate/steps/fallbacks.ts
2345
+ function codeForReason(reason) {
2346
+ switch (reason) {
2347
+ case "unknown_service":
2348
+ return "fallback_unknown_service";
2349
+ case "no_primary":
2350
+ return "fallback_no_primary";
2351
+ case "rate_violation":
2352
+ return "fallback_rate_violation";
2353
+ case "constraint_mismatch":
2354
+ return "fallback_constraint_mismatch";
2355
+ case "cycle":
2356
+ return "fallback_cycle";
2357
+ default:
2358
+ return "fallback_bad_node";
2538
2359
  }
2539
- getOptions() {
2540
- return cloneDeep2(this.options);
2360
+ }
2361
+ function messageFor(code, d) {
2362
+ const n = d.nodeId ? `node "${String(d.nodeId)}"` : "node";
2363
+ switch (code) {
2364
+ case "fallback_unknown_service":
2365
+ return `Fallback candidate "${String(
2366
+ d.candidate
2367
+ )}" is unknown for ${n}.`;
2368
+ case "fallback_no_primary":
2369
+ return `Fallback rule has no primary service for ${n}.`;
2370
+ case "fallback_rate_violation":
2371
+ return `Fallback candidate "${String(
2372
+ d.candidate
2373
+ )}" violates the base-rate rules for ${n}.`;
2374
+ case "fallback_constraint_mismatch":
2375
+ return `Fallback candidate "${String(
2376
+ d.candidate
2377
+ )}" does not satisfy required constraints for ${n}.`;
2378
+ case "fallback_cycle":
2379
+ return `Fallback rules contain a cycle for ${n}.`;
2380
+ default:
2381
+ return `Fallback rule is invalid for ${n}.`;
2541
2382
  }
2542
- visibleFields(tagId, selectedKeys) {
2543
- var _a;
2544
- return visibleFieldIdsUnder(this.props, tagId, {
2545
- selectedKeys: new Set(
2546
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
2383
+ }
2384
+ function validateFallbacks(v) {
2385
+ var _a, _b, _c, _d;
2386
+ const mode = (_b = (_a = v.options.fallbackSettings) == null ? void 0 : _a.mode) != null ? _b : "strict";
2387
+ if (!v.props.fallbacks) return;
2388
+ const diags = collectFailedFallbacks(v.props, (_c = v.options.serviceMap) != null ? _c : {}, {
2389
+ ...v.options.fallbackSettings,
2390
+ mode: "dev"
2391
+ });
2392
+ if (mode !== "strict") return;
2393
+ for (const d of diags) {
2394
+ if (d.scope === "global") continue;
2395
+ const code = codeForReason(
2396
+ String((_d = d.reason) != null ? _d : "fallback_bad_node")
2397
+ );
2398
+ const nodeId = d.nodeId ? String(d.nodeId) : void 0;
2399
+ const tagContext = d.tagContext;
2400
+ const affectedIds = [];
2401
+ if (nodeId) affectedIds.push(nodeId);
2402
+ if (typeof tagContext === "string" && tagContext && tagContext !== nodeId)
2403
+ affectedIds.push(tagContext);
2404
+ v.errors.push({
2405
+ code,
2406
+ severity: "error",
2407
+ message: messageFor(code, {
2408
+ nodeId,
2409
+ primary: d.primary,
2410
+ candidate: d.candidate,
2411
+ tagContext,
2412
+ scope: d.scope
2413
+ }),
2414
+ nodeId,
2415
+ details: withAffected(
2416
+ {
2417
+ primary: d.primary,
2418
+ candidate: d.candidate,
2419
+ tagContext,
2420
+ scope: d.scope
2421
+ },
2422
+ affectedIds.length > 1 ? affectedIds : void 0
2547
2423
  )
2548
2424
  });
2549
2425
  }
2550
- getNodeMap() {
2551
- if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
2552
- return this._nodemap;
2426
+ }
2427
+
2428
+ // src/core/validate/policies/collect-service-items.ts
2429
+ function asArray(v) {
2430
+ if (v === void 0) return void 0;
2431
+ return Array.isArray(v) ? v : [v];
2432
+ }
2433
+ function isServiceIdRef2(v) {
2434
+ return typeof v === "string" || typeof v === "number" && Number.isFinite(v);
2435
+ }
2436
+ function svcSnapshot(serviceMap, sid) {
2437
+ const svc = serviceMap[sid];
2438
+ if (!svc) return { id: sid };
2439
+ const meta = svc.meta && typeof svc.meta === "object" ? svc.meta : {};
2440
+ return {
2441
+ ...svc,
2442
+ id: sid,
2443
+ ...meta
2444
+ };
2445
+ }
2446
+ function pushItem(out, next) {
2447
+ var _a;
2448
+ const key = `${String(next.serviceId)}|${next.role}`;
2449
+ const existing = out.get(key);
2450
+ if (!existing) {
2451
+ out.set(key, {
2452
+ tagId: next.tagId,
2453
+ fieldId: next.fieldId,
2454
+ optionId: next.optionId,
2455
+ nodeId: next.nodeId,
2456
+ serviceId: next.serviceId,
2457
+ role: next.role,
2458
+ service: next.service,
2459
+ affectedIds: Array.from(new Set(next.affectedIds))
2460
+ });
2461
+ return;
2553
2462
  }
2554
- /* ───── internals ──────────────────────────────────────────────────── */
2555
- rebuildIndexes() {
2556
- this.tagById.clear();
2557
- this.fieldById.clear();
2558
- this.optionOwnerById.clear();
2559
- this._nodemap = null;
2560
- for (const t of this.props.filters) this.tagById.set(t.id, t);
2561
- for (const f of this.props.fields) {
2562
- this.fieldById.set(f.id, f);
2563
- if (Array.isArray(f.options)) {
2564
- for (const o of f.options)
2565
- this.optionOwnerById.set(o.id, { fieldId: f.id });
2566
- }
2567
- }
2463
+ const mergedIds = Array.from(
2464
+ /* @__PURE__ */ new Set([...existing.affectedIds, ...next.affectedIds])
2465
+ );
2466
+ out.set(key, {
2467
+ ...existing,
2468
+ tagId: (_a = existing.tagId) != null ? _a : next.tagId,
2469
+ affectedIds: mergedIds
2470
+ });
2471
+ }
2472
+ function fieldRoleOf(f, o) {
2473
+ var _a, _b;
2474
+ const roleRaw = (_b = (_a = o == null ? void 0 : o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
2475
+ return roleRaw === "utility" ? "utility" : "base";
2476
+ }
2477
+ function applyFilterAllowLists(tagId, fieldId, filter) {
2478
+ const tagAllow = asArray(filter == null ? void 0 : filter.tag_id);
2479
+ const fieldAllow = asArray(filter == null ? void 0 : filter.field_id);
2480
+ if (tagAllow) {
2481
+ if (!tagId) return false;
2482
+ if (!tagAllow.includes(tagId)) return false;
2568
2483
  }
2569
- };
2570
- function toStringSet(v) {
2571
- if (!v) return /* @__PURE__ */ new Set();
2572
- if (v instanceof Set) return new Set(Array.from(v).map(String));
2573
- return new Set(v.map(String));
2484
+ if (fieldAllow) {
2485
+ if (!fieldId) return false;
2486
+ if (!fieldAllow.includes(fieldId)) return false;
2487
+ }
2488
+ return true;
2574
2489
  }
2575
-
2576
- // src/core/rate-coherence.ts
2577
- function validateRateCoherenceDeep(params) {
2578
- var _a, _b, _c;
2579
- const { builder, services, tagId } = params;
2580
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
2581
- const props = builder.getProps();
2582
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
2583
- const fields = (_b = props.fields) != null ? _b : [];
2584
- const fieldById = new Map(fields.map((f) => [f.id, f]));
2585
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
2586
- const tag = tagById.get(tagId);
2587
- const baselineFieldIds = builder.visibleFields(tagId, []);
2588
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
2589
- const anchors = collectAnchors(baselineFields);
2590
- const diagnostics = [];
2591
- const seen = /* @__PURE__ */ new Set();
2592
- for (const anchor of anchors) {
2593
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
2594
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
2595
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
2596
- for (const fieldId of visibleInvalidFieldIds) {
2597
- const key = `internal|${tagId}|${fieldId}`;
2598
- if (seen.has(key)) continue;
2599
- seen.add(key);
2600
- diagnostics.push({
2601
- kind: "internal_field",
2602
- scope: "visible_group",
2603
- tagId,
2604
- fieldId,
2605
- nodeId: fieldId,
2606
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
2607
- simulationAnchor: {
2608
- kind: anchor.kind,
2609
- id: anchor.id,
2610
- fieldId: anchor.fieldId,
2611
- label: anchor.label
2612
- },
2613
- invalidFieldIds: [fieldId]
2490
+ function collectServiceItems(args) {
2491
+ var _a, _b, _c, _d, _e;
2492
+ const filter = args.filter;
2493
+ const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
2494
+ const where = filter == null ? void 0 : filter.where;
2495
+ const out = /* @__PURE__ */ new Map();
2496
+ const addServiceRef = (ref) => {
2497
+ if (roleFilter !== "both" && ref.role !== roleFilter) return;
2498
+ if (!applyFilterAllowLists(ref.tagId, ref.fieldId, filter)) return;
2499
+ const svc = args.serviceMap[ref.serviceId];
2500
+ if (where && svc && !matchesWhere(svc, where)) return;
2501
+ pushItem(out, {
2502
+ ...ref,
2503
+ service: svcSnapshot(args.serviceMap, ref.serviceId)
2504
+ });
2505
+ };
2506
+ if (args.mode === "global") {
2507
+ for (const t of (_b = args.tags) != null ? _b : []) {
2508
+ const sid = t.service_id;
2509
+ if (!isServiceIdRef2(sid)) continue;
2510
+ addServiceRef({
2511
+ tagId: t.id,
2512
+ serviceId: sid,
2513
+ role: "base",
2514
+ affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
2614
2515
  });
2615
2516
  }
2616
- const references = visibleFields.flatMap(
2617
- (field) => collectFieldReferences(field, services)
2618
- );
2619
- if (references.length <= 1) continue;
2620
- const primary = references.reduce((best, current) => {
2621
- if (current.rate !== best.rate) {
2622
- return current.rate > best.rate ? current : best;
2517
+ } else if (args.mode === "visible_group") {
2518
+ const t = args.tag;
2519
+ const sid = t ? t.service_id : void 0;
2520
+ if (t && isServiceIdRef2(sid)) {
2521
+ addServiceRef({
2522
+ tagId: t.id,
2523
+ serviceId: sid,
2524
+ role: "base",
2525
+ affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
2526
+ });
2527
+ }
2528
+ }
2529
+ const fields = (_c = args.fields) != null ? _c : [];
2530
+ for (const f of fields) {
2531
+ const fSid = f.service_id;
2532
+ if (isServiceIdRef2(fSid)) {
2533
+ addServiceRef({
2534
+ tagId: args.tagId,
2535
+ fieldId: f.id,
2536
+ serviceId: fSid,
2537
+ role: "base",
2538
+ affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
2539
+ });
2540
+ }
2541
+ for (const o of (_d = f.options) != null ? _d : []) {
2542
+ const oSid = o.service_id;
2543
+ if (!isServiceIdRef2(oSid)) continue;
2544
+ const role = fieldRoleOf(f, o);
2545
+ addServiceRef({
2546
+ tagId: args.tagId,
2547
+ fieldId: f.id,
2548
+ optionId: o.id,
2549
+ serviceId: oSid,
2550
+ role,
2551
+ affectedIds: [
2552
+ `field:${f.id}`,
2553
+ `option:${o.id}`,
2554
+ `service:${String(oSid)}`
2555
+ ]
2556
+ });
2557
+ }
2558
+ }
2559
+ const fb = args.props.fallbacks;
2560
+ if (!fb) return Array.from(out.values());
2561
+ const addFallbackNode = (nodeId, list) => {
2562
+ const arr = Array.isArray(list) ? list : [];
2563
+ for (const cand of arr) {
2564
+ if (!isServiceIdRef2(cand)) continue;
2565
+ addServiceRef({
2566
+ tagId: args.tagId,
2567
+ nodeId,
2568
+ serviceId: cand,
2569
+ role: "base",
2570
+ affectedIds: [`fallback-node:${nodeId}`, `service:${String(cand)}`]
2571
+ });
2572
+ }
2573
+ };
2574
+ const addFallbackGlobal = (primaryKey, list) => {
2575
+ const primaryId = primaryKey;
2576
+ addServiceRef({
2577
+ tagId: args.tagId,
2578
+ nodeId: primaryKey,
2579
+ serviceId: primaryId,
2580
+ role: "base",
2581
+ affectedIds: [
2582
+ `fallback-global-primary:${primaryKey}`,
2583
+ `service:${String(primaryId)}`
2584
+ ]
2585
+ });
2586
+ const arr = Array.isArray(list) ? list : [];
2587
+ for (const cand of arr) {
2588
+ if (!isServiceIdRef2(cand)) continue;
2589
+ addServiceRef({
2590
+ tagId: args.tagId,
2591
+ nodeId: primaryKey,
2592
+ serviceId: cand,
2593
+ role: "base",
2594
+ affectedIds: [
2595
+ `fallback-global:${primaryKey}`,
2596
+ `service:${String(cand)}`
2597
+ ]
2598
+ });
2599
+ }
2600
+ };
2601
+ const includeAllFallbacks = args.mode === "global";
2602
+ const includeGroupFallbacks = args.mode === "visible_group";
2603
+ const nodes = fb.nodes && typeof fb.nodes === "object" ? fb.nodes : void 0;
2604
+ if (nodes) {
2605
+ if (includeAllFallbacks) {
2606
+ for (const [nodeId, list] of Object.entries(nodes)) {
2607
+ addFallbackNode(nodeId, list);
2608
+ }
2609
+ } else if (includeGroupFallbacks) {
2610
+ const allowNodes = new Set(
2611
+ Array.isArray(args.visibleNodeIds) ? args.visibleNodeIds : []
2612
+ );
2613
+ for (const nodeId of allowNodes) {
2614
+ addFallbackNode(nodeId, nodes[nodeId]);
2615
+ }
2616
+ }
2617
+ }
2618
+ const globalFb = fb.global && typeof fb.global === "object" ? fb.global : void 0;
2619
+ if (globalFb) {
2620
+ if (includeAllFallbacks) {
2621
+ for (const [primaryKey, list] of Object.entries(globalFb)) {
2622
+ addFallbackGlobal(primaryKey, list);
2623
2623
  }
2624
- const bestKey = `${best.fieldId}|${best.nodeId}`;
2625
- const currentKey = `${current.fieldId}|${current.nodeId}`;
2626
- return currentKey < bestKey ? current : best;
2627
- });
2628
- for (const candidate of references) {
2629
- if (candidate.nodeId === primary.nodeId) continue;
2630
- if (candidate.fieldId === primary.fieldId) continue;
2631
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
2632
- continue;
2624
+ } else if (includeGroupFallbacks) {
2625
+ const allowPrimaries = new Set(
2626
+ ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
2627
+ );
2628
+ for (const primaryKey of allowPrimaries) {
2629
+ const list = globalFb[primaryKey];
2630
+ if (list === void 0) continue;
2631
+ addFallbackGlobal(primaryKey, list);
2633
2632
  }
2634
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
2635
- if (seen.has(key)) continue;
2636
- seen.add(key);
2637
- diagnostics.push({
2638
- kind: "contextual",
2639
- scope: "visible_group",
2640
- tagId,
2641
- nodeId: candidate.nodeId,
2642
- primary: toDiagnosticRef(primary),
2643
- offender: toDiagnosticRef(candidate),
2644
- policy: ratePolicy.kind,
2645
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2646
- message: explainRateMismatch(
2647
- ratePolicy,
2648
- primary,
2649
- candidate,
2650
- describeLabel(tag)
2651
- ),
2652
- simulationAnchor: {
2653
- kind: anchor.kind,
2654
- id: anchor.id,
2655
- fieldId: anchor.fieldId,
2656
- label: anchor.label
2657
- },
2658
- invalidFieldIds: visibleInvalidFieldIds
2659
- });
2660
2633
  }
2661
2634
  }
2662
- return diagnostics;
2635
+ return Array.from(out.values());
2663
2636
  }
2664
- function collectAnchors(fields) {
2665
- var _a, _b;
2666
- const anchors = [];
2667
- for (const field of fields) {
2668
- if (!isButton(field)) continue;
2669
- if (Array.isArray(field.options) && field.options.length > 0) {
2670
- for (const option of field.options) {
2671
- anchors.push({
2672
- kind: "option",
2673
- id: option.id,
2674
- fieldId: field.id,
2675
- label: (_a = option.label) != null ? _a : option.id
2676
- });
2637
+
2638
+ // src/core/validate/policies/ops.ts
2639
+ function evalPolicyOp(op, values, rule) {
2640
+ switch (op) {
2641
+ case "all_equal": {
2642
+ const set = new Set(
2643
+ values.map((v) => JSON.stringify(v))
2644
+ );
2645
+ return set.size <= 1;
2646
+ }
2647
+ case "no_mix": {
2648
+ const set = new Set(
2649
+ values.map((v) => JSON.stringify(v))
2650
+ );
2651
+ return set.size <= 1;
2652
+ }
2653
+ case "unique": {
2654
+ const seen = /* @__PURE__ */ new Set();
2655
+ for (const v of values) {
2656
+ const k = JSON.stringify(v);
2657
+ if (seen.has(k)) return false;
2658
+ seen.add(k);
2677
2659
  }
2678
- continue;
2660
+ return true;
2679
2661
  }
2680
- anchors.push({
2681
- kind: "field",
2682
- id: field.id,
2683
- fieldId: field.id,
2684
- label: (_b = field.label) != null ? _b : field.id
2685
- });
2662
+ case "all_true": {
2663
+ return values.every((v) => v === true);
2664
+ }
2665
+ case "any_true": {
2666
+ return values.some((v) => v === true);
2667
+ }
2668
+ case "max_count": {
2669
+ const limit = typeof rule.value === "number" ? rule.value : Infinity;
2670
+ return values.length <= limit;
2671
+ }
2672
+ case "min_count": {
2673
+ const min = typeof rule.value === "number" ? rule.value : 0;
2674
+ return values.length >= min;
2675
+ }
2676
+ default:
2677
+ return true;
2686
2678
  }
2687
- return anchors;
2688
2679
  }
2689
- function collectFieldReferences(field, services) {
2680
+
2681
+ // src/core/validate/policies/apply-policies.ts
2682
+ function uniq(arr) {
2683
+ return Array.from(new Set(arr));
2684
+ }
2685
+ function stableSeverity(s) {
2686
+ if (s === "warning") return "warning";
2687
+ if (s === "error") return "error";
2688
+ return "error";
2689
+ }
2690
+ function defaultPolicyMessage(rule) {
2691
+ if (typeof rule.message === "string" && rule.message.trim())
2692
+ return rule.message;
2693
+ if (typeof rule.label === "string" && rule.label.trim())
2694
+ return rule.label.trim();
2695
+ return `Policy "${rule.id}" violated`;
2696
+ }
2697
+ function affectedFromItems(items) {
2690
2698
  var _a;
2691
- const members = collectBaseMembers(field, services);
2692
- if (members.length === 0) return [];
2693
- if (isMultiField(field)) {
2694
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
2695
- return [
2696
- {
2697
- refKind: "multi",
2698
- nodeId: field.id,
2699
- fieldId: field.id,
2700
- label: (_a = field.label) != null ? _a : field.id,
2701
- rate: averageRate,
2702
- members
2699
+ const ids = [];
2700
+ for (const it of items) {
2701
+ for (const x of (_a = it.affectedIds) != null ? _a : []) ids.push(x);
2702
+ ids.push(`service:${String(it.serviceId)}`);
2703
+ }
2704
+ return uniq(ids);
2705
+ }
2706
+ function visibleGroupNodeIds(tag, fields) {
2707
+ var _a;
2708
+ const ids = [tag.id];
2709
+ for (const f of fields) {
2710
+ for (const o of (_a = f.options) != null ? _a : []) {
2711
+ ids.push(o.id);
2712
+ }
2713
+ }
2714
+ return uniq(ids);
2715
+ }
2716
+ function visibleGroupPrimaries(tag, fields) {
2717
+ var _a;
2718
+ const prim = [];
2719
+ const tagSid = tag.service_id;
2720
+ if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
2721
+ prim.push(tagSid);
2722
+ }
2723
+ for (const f of fields) {
2724
+ const fsid = f.service_id;
2725
+ if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
2726
+ prim.push(fsid);
2727
+ }
2728
+ for (const o of (_a = f.options) != null ? _a : []) {
2729
+ const osid = o.service_id;
2730
+ if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
2731
+ prim.push(osid);
2703
2732
  }
2704
- ];
2733
+ }
2705
2734
  }
2706
- return members.map((member) => ({
2707
- refKind: "single",
2708
- nodeId: member.id,
2709
- fieldId: field.id,
2710
- label: member.label,
2711
- rate: member.rate,
2712
- service_id: member.service_id,
2713
- members: [member]
2714
- }));
2735
+ return uniq(prim);
2715
2736
  }
2716
- function collectBaseMembers(field, services) {
2717
- var _a, _b, _c;
2718
- const members = [];
2719
- if (Array.isArray(field.options) && field.options.length > 0) {
2720
- for (const option of field.options) {
2721
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
2722
- if (role2 !== "base") continue;
2723
- if (option.service_id === void 0 || option.service_id === null) {
2724
- continue;
2737
+ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder, tags) {
2738
+ var _a, _b, _c, _d, _e;
2739
+ if (!(policies == null ? void 0 : policies.length)) return;
2740
+ const tagById = /* @__PURE__ */ new Map();
2741
+ for (const t of tags) tagById.set(t.id, t);
2742
+ for (const rule of policies) {
2743
+ const projPath = (_a = rule.projection) != null ? _a : "service.id";
2744
+ const severity = stableSeverity(
2745
+ rule.severity
2746
+ );
2747
+ const message = defaultPolicyMessage(rule);
2748
+ if (rule.scope === "global") {
2749
+ const tagAllow = Array.isArray(
2750
+ (_b = rule.filter) == null ? void 0 : _b.tag_id
2751
+ ) ? (_c = rule.filter) == null ? void 0 : _c.tag_id : ((_d = rule.filter) == null ? void 0 : _d.tag_id) ? [rule.filter.tag_id] : void 0;
2752
+ let items = [];
2753
+ if (tagAllow && tagAllow.length) {
2754
+ const merged = /* @__PURE__ */ new Map();
2755
+ for (const id of tagAllow) {
2756
+ const t = tagById.get(id);
2757
+ if (!t) continue;
2758
+ const visibleFields = fieldsVisibleUnder(t.id);
2759
+ const nodeIds = visibleGroupNodeIds(
2760
+ t,
2761
+ visibleFields
2762
+ );
2763
+ const primaries = visibleGroupPrimaries(
2764
+ t,
2765
+ visibleFields
2766
+ );
2767
+ const sub = collectServiceItems({
2768
+ mode: "visible_group",
2769
+ props,
2770
+ serviceMap,
2771
+ tag: t,
2772
+ tagId: t.id,
2773
+ fields: visibleFields,
2774
+ filter: rule.filter,
2775
+ visibleNodeIds: nodeIds,
2776
+ visiblePrimaries: primaries
2777
+ });
2778
+ for (const it of sub) {
2779
+ const k = `${String(it.serviceId)}|${it.role}`;
2780
+ const existing = merged.get(k);
2781
+ if (!existing) {
2782
+ merged.set(k, it);
2783
+ } else {
2784
+ merged.set(k, {
2785
+ ...existing,
2786
+ affectedIds: uniq([
2787
+ ...existing.affectedIds,
2788
+ ...it.affectedIds
2789
+ ])
2790
+ });
2791
+ }
2792
+ }
2793
+ }
2794
+ items = Array.from(merged.values());
2795
+ } else {
2796
+ const allFields = (_e = props.fields) != null ? _e : [];
2797
+ items = collectServiceItems({
2798
+ mode: "global",
2799
+ props,
2800
+ serviceMap,
2801
+ tags,
2802
+ fields: allFields,
2803
+ filter: rule.filter
2804
+ });
2725
2805
  }
2726
- const cap2 = getServiceCapability(services, option.service_id);
2727
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
2728
- continue;
2806
+ const values = items.map(
2807
+ (it) => getByPath(it, projPath)
2808
+ );
2809
+ if (!evalPolicyOp(rule.op, values, rule)) {
2810
+ errors.push({
2811
+ code: "policy_violation",
2812
+ severity,
2813
+ message,
2814
+ nodeId: "global",
2815
+ details: {
2816
+ ruleId: rule.id,
2817
+ scope: "global",
2818
+ op: rule.op,
2819
+ projection: projPath,
2820
+ count: items.length,
2821
+ affectedIds: affectedFromItems(items)
2822
+ }
2823
+ });
2729
2824
  }
2730
- members.push({
2731
- kind: "option",
2732
- id: option.id,
2733
- fieldId: field.id,
2734
- label: (_b = option.label) != null ? _b : option.id,
2735
- service_id: option.service_id,
2736
- rate: cap2.rate
2825
+ continue;
2826
+ }
2827
+ for (const t of tags) {
2828
+ const visibleFields = fieldsVisibleUnder(t.id);
2829
+ const nodeIds = visibleGroupNodeIds(t, visibleFields);
2830
+ const primaries = visibleGroupPrimaries(t, visibleFields);
2831
+ const items = collectServiceItems({
2832
+ mode: "visible_group",
2833
+ props,
2834
+ serviceMap,
2835
+ tag: t,
2836
+ tagId: t.id,
2837
+ fields: visibleFields,
2838
+ filter: rule.filter,
2839
+ visibleNodeIds: nodeIds,
2840
+ visiblePrimaries: primaries
2737
2841
  });
2842
+ if (!items.length) continue;
2843
+ const values = items.map(
2844
+ (it) => getByPath(it, projPath)
2845
+ );
2846
+ if (!evalPolicyOp(rule.op, values, rule)) {
2847
+ errors.push({
2848
+ code: "policy_violation",
2849
+ severity,
2850
+ message,
2851
+ nodeId: t.id,
2852
+ details: {
2853
+ ruleId: rule.id,
2854
+ scope: "visible_group",
2855
+ op: rule.op,
2856
+ projection: projPath,
2857
+ count: items.length,
2858
+ affectedIds: affectedFromItems(items)
2859
+ }
2860
+ });
2861
+ }
2738
2862
  }
2739
- return members;
2740
2863
  }
2741
- const role = normalizeRole(field.pricing_role, "base");
2742
- if (role !== "base") return members;
2743
- if (field.service_id === void 0 || field.service_id === null) return members;
2744
- const cap = getServiceCapability(services, field.service_id);
2745
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
2746
- return members;
2747
- }
2748
- members.push({
2749
- kind: "field",
2750
- id: field.id,
2751
- fieldId: field.id,
2752
- label: (_c = field.label) != null ? _c : field.id,
2753
- service_id: field.service_id,
2754
- rate: cap.rate
2755
- });
2756
- return members;
2757
2864
  }
2758
- function isButton(field) {
2759
- if (field.button === true) return true;
2760
- return Array.isArray(field.options) && field.options.length > 0;
2761
- }
2762
- function normalizeRole(role, fallback) {
2763
- return role === "base" || role === "utility" ? role : fallback;
2865
+
2866
+ // src/core/governance.ts
2867
+ var DEFAULT_FALLBACK_SETTINGS = {
2868
+ requireConstraintFit: true,
2869
+ ratePolicy: { kind: "lte_primary", pct: 5 },
2870
+ selectionStrategy: "priority",
2871
+ mode: "strict"
2872
+ };
2873
+ function resolveGlobalRatePolicy(options) {
2874
+ return normalizeRatePolicy(options.ratePolicy);
2764
2875
  }
2765
- function toDiagnosticRef(reference) {
2876
+ function resolveFallbackSettings(options) {
2877
+ var _a;
2766
2878
  return {
2767
- nodeId: reference.nodeId,
2768
- fieldId: reference.fieldId,
2769
- label: reference.label,
2770
- refKind: reference.refKind,
2771
- service_id: reference.service_id,
2772
- rate: reference.rate
2879
+ ...DEFAULT_FALLBACK_SETTINGS,
2880
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2773
2881
  };
2774
2882
  }
2775
- function contextualKey(tagId, primary, candidate, ratePolicy) {
2776
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
2777
- return [
2778
- "contextual",
2779
- tagId,
2780
- primary.fieldId,
2781
- primary.nodeId,
2782
- candidate.fieldId,
2783
- candidate.nodeId,
2784
- `${ratePolicy.kind}${pctKey}`
2785
- ].join("|");
2786
- }
2787
- function describeLabel(tag) {
2788
- var _a, _b;
2789
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
2790
- }
2791
- function explainRateMismatch(policy, primary, candidate, where) {
2792
- var _a, _b;
2793
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
2794
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
2795
- switch (policy.kind) {
2796
- case "eq_primary":
2797
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
2798
- case "lte_primary":
2799
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
2800
- case "within_pct":
2801
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
2802
- case "at_least_pct_lower":
2803
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
2804
- }
2883
+ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2884
+ var _a, _b, _c, _d;
2885
+ const mergedFallbackSettings = {
2886
+ ...(_a = defaults.fallbackSettings) != null ? _a : {},
2887
+ ...(_b = overrides.fallbackSettings) != null ? _b : {}
2888
+ };
2889
+ return {
2890
+ ...defaults,
2891
+ ...overrides,
2892
+ policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2893
+ ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2894
+ fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2895
+ };
2805
2896
  }
2806
2897
 
2807
2898
  // src/core/validate/index.ts
@@ -2852,7 +2943,8 @@ function validate(props, ctx = {}) {
2852
2943
  invalidRateFieldIds: /* @__PURE__ */ new Set(),
2853
2944
  tagById,
2854
2945
  fieldById,
2855
- fieldsVisibleUnder: (_tagId) => []
2946
+ fieldsVisibleUnder: (_tagId) => [],
2947
+ simulatedVisibilityContexts: []
2856
2948
  };
2857
2949
  validateStructure(v);
2858
2950
  validateIdentity(v);
@@ -2872,54 +2964,306 @@ function validate(props, ctx = {}) {
2872
2964
  validateServiceVsUserInput(v);
2873
2965
  validateUtilityMarkers(v);
2874
2966
  validateRates(v);
2875
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
2876
- const builder = createBuilder({ serviceMap });
2877
- builder.load(props);
2878
- for (const tag of tags) {
2879
- const diags = validateRateCoherenceDeep({
2880
- builder,
2881
- services: serviceMap,
2882
- tagId: tag.id,
2883
- ratePolicy,
2884
- invalidFieldIds: v.invalidRateFieldIds
2967
+ validateRateCoherence(v);
2968
+ validateConstraints(v);
2969
+ validateCustomFields(v);
2970
+ validateGlobalUtilityGuard(v);
2971
+ validateUnboundFields(v);
2972
+ validateFallbacks(v);
2973
+ return v.errors;
2974
+ }
2975
+ async function validateAsync(props, ctx = {}) {
2976
+ await Promise.resolve();
2977
+ if (typeof requestAnimationFrame === "function") {
2978
+ await new Promise(
2979
+ (resolve) => requestAnimationFrame(() => resolve())
2980
+ );
2981
+ } else {
2982
+ await new Promise((resolve) => setTimeout(resolve, 0));
2983
+ }
2984
+ return validate(props, ctx);
2985
+ }
2986
+
2987
+ // src/core/builder.ts
2988
+ import { cloneDeep as cloneDeep2 } from "lodash-es";
2989
+ function createBuilder(opts = {}) {
2990
+ return new BuilderImpl(opts);
2991
+ }
2992
+ var BuilderImpl = class {
2993
+ constructor(opts = {}) {
2994
+ this.props = {
2995
+ filters: [],
2996
+ fields: [],
2997
+ schema_version: "1.0"
2998
+ };
2999
+ this.tagById = /* @__PURE__ */ new Map();
3000
+ this.fieldById = /* @__PURE__ */ new Map();
3001
+ this.optionOwnerById = /* @__PURE__ */ new Map();
3002
+ this._nodemap = null;
3003
+ this.options = { ...opts };
3004
+ }
3005
+ /* ───── lifecycle ─────────────────────────────────────────────────────── */
3006
+ isTagId(id) {
3007
+ return this.tagById.has(id);
3008
+ }
3009
+ isFieldId(id) {
3010
+ return this.fieldById.has(id);
3011
+ }
3012
+ isOptionId(id) {
3013
+ return this.optionOwnerById.has(id);
3014
+ }
3015
+ load(raw) {
3016
+ const next = normalise(raw, {
3017
+ defaultPricingRole: "base",
3018
+ constraints: this.getConstraints().map((item) => item.label)
3019
+ });
3020
+ this.props = next;
3021
+ this.rebuildIndexes();
3022
+ }
3023
+ getProps() {
3024
+ return this.props;
3025
+ }
3026
+ setOptions(patch) {
3027
+ this.options = { ...this.options, ...patch };
3028
+ }
3029
+ getServiceMap() {
3030
+ var _a;
3031
+ return (_a = this.options.serviceMap) != null ? _a : {};
3032
+ }
3033
+ getConstraints() {
3034
+ var _a;
3035
+ const serviceMap = this.getServiceMap();
3036
+ const out = /* @__PURE__ */ new Set();
3037
+ const guard = /* @__PURE__ */ new Set();
3038
+ for (const svc of Object.values(serviceMap)) {
3039
+ const flags = (_a = svc.flags) != null ? _a : {};
3040
+ for (const flagId of Object.keys(flags)) {
3041
+ if (guard.has(flagId)) continue;
3042
+ guard.add(flagId);
3043
+ out.add({
3044
+ id: flagId,
3045
+ value: flagId,
3046
+ label: flagId,
3047
+ description: flags[flagId].description
3048
+ });
3049
+ }
3050
+ }
3051
+ return Array.from(out);
3052
+ }
3053
+ /* ───── querying ─────────────────────────────────────────────────────── */
3054
+ tree() {
3055
+ var _a, _b, _c, _d;
3056
+ const nodes = [];
3057
+ const edges = [];
3058
+ const showSet = toStringSet(this.options.showOptionNodes);
3059
+ for (const t of this.props.filters) {
3060
+ nodes.push({ id: t.id, kind: "tag", label: t.label });
3061
+ }
3062
+ for (const t of this.props.filters) {
3063
+ if (t.bind_id) {
3064
+ edges.push({
3065
+ from: t.bind_id,
3066
+ to: t.id,
3067
+ kind: "child"
3068
+ });
3069
+ }
3070
+ }
3071
+ for (const f of this.props.fields) {
3072
+ nodes.push({
3073
+ id: f.id,
3074
+ kind: "field",
3075
+ label: f.label,
3076
+ bind_type: f.pricing_role === "utility" ? "utility" : f.bind_id ? "bound" : null
2885
3077
  });
2886
- for (const diag of diags) {
2887
- if (diag.kind !== "contextual") continue;
2888
- errors.push({
2889
- code: "rate_coherence_violation",
2890
- severity: "error",
2891
- message: diag.message,
2892
- nodeId: diag.nodeId,
2893
- details: {
2894
- tagId: diag.tagId,
2895
- simulationAnchor: diag.simulationAnchor,
2896
- primary: diag.primary,
2897
- offender: diag.offender,
2898
- policy: diag.policy,
2899
- policyPct: diag.policyPct,
2900
- invalidFieldIds: diag.invalidFieldIds
2901
- }
3078
+ }
3079
+ for (const f of this.props.fields) {
3080
+ const b = f.bind_id;
3081
+ if (Array.isArray(b)) {
3082
+ for (const tagId of b)
3083
+ edges.push({
3084
+ from: tagId,
3085
+ to: f.id,
3086
+ kind: "bind"
3087
+ });
3088
+ } else if (typeof b === "string") {
3089
+ edges.push({ from: b, to: f.id, kind: "bind" });
3090
+ }
3091
+ }
3092
+ for (const f of this.props.fields) {
3093
+ const showOptions = showSet.has(f.id);
3094
+ if (!showOptions) continue;
3095
+ if (!Array.isArray(f.options)) continue;
3096
+ for (const o of f.options) {
3097
+ nodes.push({
3098
+ id: o.id,
3099
+ kind: "option",
3100
+ label: o.label
2902
3101
  });
3102
+ const e = {
3103
+ from: f.id,
3104
+ to: o.id,
3105
+ kind: "option",
3106
+ meta: { ownerField: f.id }
3107
+ };
3108
+ edges.push(e);
3109
+ }
3110
+ }
3111
+ for (const t of this.props.filters) {
3112
+ for (const id of (_a = t.includes) != null ? _a : []) {
3113
+ edges.push({ from: t.id, to: id, kind: "include" });
2903
3114
  }
3115
+ for (const id of (_b = t.excludes) != null ? _b : []) {
3116
+ edges.push({ from: t.id, to: id, kind: "exclude" });
3117
+ }
3118
+ }
3119
+ const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
3120
+ const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
3121
+ const pushButtonEdge = (keyId, targetFieldId, kind) => {
3122
+ var _a2;
3123
+ const owner = this.optionOwnerById.get(keyId);
3124
+ const ownerFieldId = (_a2 = owner == null ? void 0 : owner.fieldId) != null ? _a2 : this.fieldById.has(keyId) ? keyId : void 0;
3125
+ if (!ownerFieldId) return;
3126
+ const fromNode = owner && showSet.has(owner.fieldId) ? keyId : ownerFieldId;
3127
+ const meta = owner ? showSet.has(owner.fieldId) ? {
3128
+ via: "option-visible",
3129
+ ownerField: owner.fieldId,
3130
+ sourceOption: keyId
3131
+ } : {
3132
+ via: "option-hidden",
3133
+ ownerField: owner.fieldId,
3134
+ sourceOption: keyId
3135
+ } : { via: "field-button" };
3136
+ const e = { from: fromNode, to: targetFieldId, kind, meta };
3137
+ edges.push(e);
3138
+ };
3139
+ for (const [keyId, arr] of Object.entries(incMap)) {
3140
+ for (const fid of arr != null ? arr : [])
3141
+ pushButtonEdge(keyId, fid, "include");
3142
+ }
3143
+ for (const [keyId, arr] of Object.entries(excMap)) {
3144
+ for (const fid of arr != null ? arr : [])
3145
+ pushButtonEdge(keyId, fid, "exclude");
2904
3146
  }
3147
+ return { nodes, edges };
2905
3148
  }
2906
- validateConstraints(v);
2907
- validateCustomFields(v);
2908
- validateGlobalUtilityGuard(v);
2909
- validateUnboundFields(v);
2910
- validateFallbacks(v);
2911
- return v.errors;
2912
- }
2913
- async function validateAsync(props, ctx = {}) {
2914
- await Promise.resolve();
2915
- if (typeof requestAnimationFrame === "function") {
2916
- await new Promise(
2917
- (resolve) => requestAnimationFrame(() => resolve())
3149
+ cleanedProps() {
3150
+ var _a, _b, _c, _d, _e;
3151
+ const fieldIds = new Set(this.props.fields.map((f) => f.id));
3152
+ const optionIds = /* @__PURE__ */ new Set();
3153
+ this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
3154
+ const includedByTag = /* @__PURE__ */ new Set();
3155
+ const excludedAnywhere = /* @__PURE__ */ new Set();
3156
+ for (const t of this.props.filters) {
3157
+ for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
3158
+ for (const id of (_b = t.excludes) != null ? _b : []) excludedAnywhere.add(id);
3159
+ }
3160
+ const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
3161
+ const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
3162
+ const includedByButtons = /* @__PURE__ */ new Set();
3163
+ const referencedKeys = /* @__PURE__ */ new Set();
3164
+ const referencedOwnerFields = /* @__PURE__ */ new Set();
3165
+ for (const [key, arr] of Object.entries(incMap)) {
3166
+ referencedKeys.add(key);
3167
+ const owner = this.optionOwnerById.get(key);
3168
+ if (owner) referencedOwnerFields.add(owner.fieldId);
3169
+ for (const fid of arr != null ? arr : []) {
3170
+ includedByButtons.add(fid);
3171
+ }
3172
+ }
3173
+ for (const [key, arr] of Object.entries(excMap)) {
3174
+ referencedKeys.add(key);
3175
+ const owner = this.optionOwnerById.get(key);
3176
+ if (owner) referencedOwnerFields.add(owner.fieldId);
3177
+ for (const fid of arr != null ? arr : []) {
3178
+ void fid;
3179
+ }
3180
+ }
3181
+ const boundIds = /* @__PURE__ */ new Set();
3182
+ for (const f of this.props.fields) {
3183
+ const b = f.bind_id;
3184
+ if (Array.isArray(b)) b.forEach((id) => boundIds.add(id));
3185
+ else if (typeof b === "string") boundIds.add(b);
3186
+ }
3187
+ const fields = this.props.fields.filter((f) => {
3188
+ var _a2;
3189
+ const isUtility = ((_a2 = f.pricing_role) != null ? _a2 : "base") === "utility";
3190
+ if (!isUtility) return true;
3191
+ const bound = !!f.bind_id;
3192
+ const included = includedByTag.has(f.id) || includedByButtons.has(f.id);
3193
+ const referenced = referencedOwnerFields.has(f.id) || referencedKeys.has(f.id);
3194
+ const excluded = excludedAnywhere.has(f.id);
3195
+ return bound || included || referenced || !excluded;
3196
+ });
3197
+ const allowedTargets = new Set(fields.map((f) => f.id));
3198
+ const pruneButtons = (src) => {
3199
+ if (!src) return void 0;
3200
+ const out2 = {};
3201
+ for (const [key, arr] of Object.entries(src)) {
3202
+ const keyIsValid = optionIds.has(key) || fieldIds.has(key);
3203
+ if (!keyIsValid) continue;
3204
+ const cleaned = (arr != null ? arr : []).filter(
3205
+ (fid) => allowedTargets.has(fid)
3206
+ );
3207
+ if (cleaned.length) out2[key] = Array.from(new Set(cleaned));
3208
+ }
3209
+ return Object.keys(out2).length ? out2 : void 0;
3210
+ };
3211
+ const includes_for_buttons = pruneButtons(
3212
+ this.props.includes_for_buttons
2918
3213
  );
2919
- } else {
2920
- await new Promise((resolve) => setTimeout(resolve, 0));
3214
+ const excludes_for_buttons = pruneButtons(
3215
+ this.props.excludes_for_buttons
3216
+ );
3217
+ const out = {
3218
+ filters: this.props.filters.slice(),
3219
+ fields,
3220
+ ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
3221
+ ...includes_for_buttons && { includes_for_buttons },
3222
+ ...excludes_for_buttons && { excludes_for_buttons },
3223
+ schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
3224
+ // keep fallbacks & other maps as-is
3225
+ ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
3226
+ };
3227
+ return out;
2921
3228
  }
2922
- return validate(props, ctx);
3229
+ errors() {
3230
+ return validate(this.props, mergeValidatorOptions({}, this.options));
3231
+ }
3232
+ getOptions() {
3233
+ return cloneDeep2(this.options);
3234
+ }
3235
+ visibleFields(tagId, selectedKeys) {
3236
+ var _a;
3237
+ return visibleFieldIdsUnder(this.props, tagId, {
3238
+ selectedKeys: new Set(
3239
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
3240
+ )
3241
+ });
3242
+ }
3243
+ getNodeMap() {
3244
+ if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
3245
+ return this._nodemap;
3246
+ }
3247
+ /* ───── internals ──────────────────────────────────────────────────── */
3248
+ rebuildIndexes() {
3249
+ this.tagById.clear();
3250
+ this.fieldById.clear();
3251
+ this.optionOwnerById.clear();
3252
+ this._nodemap = null;
3253
+ for (const t of this.props.filters) this.tagById.set(t.id, t);
3254
+ for (const f of this.props.fields) {
3255
+ this.fieldById.set(f.id, f);
3256
+ if (Array.isArray(f.options)) {
3257
+ for (const o of f.options)
3258
+ this.optionOwnerById.set(o.id, { fieldId: f.id });
3259
+ }
3260
+ }
3261
+ }
3262
+ };
3263
+ function toStringSet(v) {
3264
+ if (!v) return /* @__PURE__ */ new Set();
3265
+ if (v instanceof Set) return new Set(Array.from(v).map(String));
3266
+ return new Set(v.map(String));
2923
3267
  }
2924
3268
 
2925
3269
  // src/core/fallback.ts
@@ -3917,23 +4261,20 @@ function compilePolicies(raw) {
3917
4261
 
3918
4262
  // src/core/service-filter.ts
3919
4263
  function filterServicesForVisibleGroup(input, deps) {
3920
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4264
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
3921
4265
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
3922
4266
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
3923
4267
  const { context } = input;
3924
4268
  const usedSet = new Set(context.usedServiceIds.map(String));
3925
- const primary = context.usedServiceIds[0];
3926
4269
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
3927
4270
  const resolvedRatePolicy = normalizeRatePolicy(
3928
4271
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
3929
4272
  );
3930
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
3931
- const fb = {
3932
- ...DEFAULT_FALLBACK_SETTINGS,
3933
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
3934
- ratePolicy: resolvedRatePolicy
3935
- };
3936
4273
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
4274
+ const resolvedCustomPrimaryRate = resolveCustomPrimaryRate(
4275
+ context.rateContext,
4276
+ svcMap
4277
+ );
3937
4278
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
3938
4279
  deps.builder,
3939
4280
  context.tagId,
@@ -3960,7 +4301,19 @@ function filterServicesForVisibleGroup(input, deps) {
3960
4301
  cap.id,
3961
4302
  (_k = context.effectiveConstraints) != null ? _k : {}
3962
4303
  );
3963
- const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
4304
+ const passesRate2 = resolvedCustomPrimaryRate != null ? passesRatePolicy(
4305
+ resolvedRatePolicy,
4306
+ resolvedCustomPrimaryRate,
4307
+ toFiniteNumber(cap.rate)
4308
+ ) : candidatePassesRateCoherence(
4309
+ deps.builder,
4310
+ svcMap,
4311
+ context.tagId,
4312
+ (_l = context.selectedButtons) != null ? _l : [],
4313
+ context.usedServiceIds,
4314
+ id,
4315
+ resolvedRatePolicy
4316
+ );
3964
4317
  const polRes = evaluatePoliciesRaw(
3965
4318
  policySource,
3966
4319
  [...context.usedServiceIds, id],
@@ -3992,6 +4345,17 @@ function filterServicesForVisibleGroup(input, deps) {
3992
4345
  diagnostics: lastDiagnostics && lastDiagnostics.length ? lastDiagnostics : void 0
3993
4346
  };
3994
4347
  }
4348
+ function resolveCustomPrimaryRate(rateContext, serviceMap) {
4349
+ if (!rateContext || rateContext.mode !== "custom_primary_rate") {
4350
+ return void 0;
4351
+ }
4352
+ if (rateContext.source === "manual") {
4353
+ return toFiniteNumber(rateContext.primaryRate);
4354
+ }
4355
+ if (rateContext.primaryServiceId == null) return void 0;
4356
+ const cap = getServiceCapability(serviceMap, rateContext.primaryServiceId);
4357
+ return toFiniteNumber(cap == null ? void 0 : cap.rate);
4358
+ }
3995
4359
  function evaluatePoliciesRaw(raw, serviceIds, svcMap, tagId, visibleServiceIds) {
3996
4360
  const compiled = compilePolicies(raw);
3997
4361
  const evaluated = evaluateServicePolicies(
@@ -4079,7 +4443,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
4079
4443
  const fields = (_b = props.fields) != null ? _b : [];
4080
4444
  const tag = tags.find((t) => t.id === tagId);
4081
4445
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
4082
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
4446
+ const visibleFieldIds = new Set(
4447
+ builder.visibleFields(tagId, selectedButtons)
4448
+ );
4083
4449
  for (const field of fields) {
4084
4450
  if (!visibleFieldIds.has(field.id)) continue;
4085
4451
  if (field.service_id != null) {
@@ -4102,8 +4468,7 @@ function matchesRuleFilter(cap, rule, tagId) {
4102
4468
  if (!cap) return false;
4103
4469
  const f = rule.filter;
4104
4470
  if (!f) return true;
4105
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
4106
- return true;
4471
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
4107
4472
  }
4108
4473
  function toStrSet(v) {
4109
4474
  const arr = Array.isArray(v) ? v : [v];
@@ -4111,6 +4476,107 @@ function toStrSet(v) {
4111
4476
  for (const x of arr) s.add(String(x));
4112
4477
  return s;
4113
4478
  }
4479
+ function candidatePassesRateCoherence(builder, serviceMap, tagId, selectedKeys, usedServiceIds, candidateId, ratePolicy) {
4480
+ var _a, _b, _c, _d;
4481
+ if (usedServiceIds.length === 0) return true;
4482
+ const props = builder.getProps();
4483
+ const baseFields = (_a = props.fields) != null ? _a : [];
4484
+ const candidateFieldId = syntheticServiceFieldId("candidate", candidateId, 0);
4485
+ const syntheticFields = [
4486
+ ...usedServiceIds.map((serviceId, index) => ({
4487
+ id: syntheticServiceFieldId("used", serviceId, index),
4488
+ label: `Used service ${String(serviceId)}`,
4489
+ type: "custom",
4490
+ button: true,
4491
+ service_id: serviceId,
4492
+ pricing_role: "base"
4493
+ })),
4494
+ {
4495
+ id: candidateFieldId,
4496
+ label: `Candidate ${String(candidateId)}`,
4497
+ type: "custom",
4498
+ button: true,
4499
+ service_id: candidateId,
4500
+ pricing_role: "base"
4501
+ }
4502
+ ];
4503
+ const fields = [...baseFields, ...syntheticFields];
4504
+ const visibleFieldIds = [
4505
+ ...builder.visibleFields(tagId, selectedKeys),
4506
+ ...syntheticFields.map((field) => field.id)
4507
+ ];
4508
+ const anchoredFilters = ((_b = props.filters) != null ? _b : []).map(
4509
+ (tag) => tag.id === tagId && usedServiceIds[0] != null ? { ...tag, service_id: usedServiceIds[0] } : tag
4510
+ );
4511
+ const validationProps = {
4512
+ ...props,
4513
+ filters: anchoredFilters,
4514
+ fields
4515
+ };
4516
+ const errors = [];
4517
+ const tags = (_c = validationProps.filters) != null ? _c : [];
4518
+ const fieldById = new Map(fields.map((field) => [field.id, field]));
4519
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
4520
+ const v = {
4521
+ props: validationProps,
4522
+ nodeMap: buildNodeMap(validationProps),
4523
+ options: {
4524
+ ...(_d = builder.getOptions) == null ? void 0 : _d.call(builder),
4525
+ serviceMap,
4526
+ ratePolicy
4527
+ },
4528
+ errors,
4529
+ serviceMap,
4530
+ selectedKeys: new Set(selectedKeys),
4531
+ tags,
4532
+ fields,
4533
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
4534
+ tagById,
4535
+ fieldById,
4536
+ fieldsVisibleUnder: () => [],
4537
+ simulatedVisibilityContexts: []
4538
+ };
4539
+ validateRateCoherenceForVisibleContext({
4540
+ v,
4541
+ tagId,
4542
+ selectedKeys,
4543
+ visibleFieldIds,
4544
+ effectMap: buildTriggerEffectMap(validationProps),
4545
+ seen: /* @__PURE__ */ new Set()
4546
+ });
4547
+ return !errors.some(
4548
+ (error) => rateIssueAffectsCandidate(
4549
+ error,
4550
+ candidateId,
4551
+ candidateFieldId,
4552
+ usedServiceIds[0]
4553
+ )
4554
+ );
4555
+ }
4556
+ function syntheticServiceFieldId(kind, serviceId, index) {
4557
+ return `__service_filter_${kind}__:${index}:${String(serviceId)}`;
4558
+ }
4559
+ function rateIssueAffectsCandidate(error, candidateId, candidateFieldId, primaryAnchorId) {
4560
+ var _a, _b, _c, _d;
4561
+ if (error.code !== "rate_coherence_violation") return false;
4562
+ const candidateKey = String(candidateId);
4563
+ const details = (_a = error.details) != null ? _a : {};
4564
+ const anchorKey = primaryAnchorId == null ? void 0 : String(primaryAnchorId);
4565
+ const primaryMatchesAnchor = anchorKey == null || String((_b = details.primary) == null ? void 0 : _b.serviceId) === anchorKey || String((_c = details.primary) == null ? void 0 : _c.service_id) === anchorKey;
4566
+ if (primaryMatchesAnchor && ((_d = details.affectedServiceIds) == null ? void 0 : _d.some(
4567
+ (serviceId) => String(serviceId) === candidateKey
4568
+ ))) {
4569
+ return true;
4570
+ }
4571
+ if (primaryMatchesAnchor && String(error.nodeId) === candidateFieldId) {
4572
+ return true;
4573
+ }
4574
+ return [details.primary, details.candidate].some((ref) => {
4575
+ if (!ref) return false;
4576
+ if (!primaryMatchesAnchor) return false;
4577
+ return String(ref.serviceId) === candidateKey || String(ref.service_id) === candidateKey || String(ref.fieldId) === candidateFieldId || String(ref.nodeId) === candidateFieldId;
4578
+ });
4579
+ }
4114
4580
 
4115
4581
  // src/utils/prune-fallbacks.ts
4116
4582
  function pruneInvalidNodeFallbacks(props, services, settings) {
@@ -5214,6 +5680,7 @@ function mapDiagReason(reason) {
5214
5680
  }
5215
5681
  export {
5216
5682
  buildOrderSnapshot,
5683
+ buildTriggerEffectMap,
5217
5684
  collectFailedFallbacks,
5218
5685
  createBuilder,
5219
5686
  createFallbackEditor,
@@ -5222,6 +5689,7 @@ export {
5222
5689
  getAssignedServiceIds,
5223
5690
  getEligibleFallbacks,
5224
5691
  getFallbackRegistrationInfo,
5692
+ isRefExcludedBySelectedKeys,
5225
5693
  normalise,
5226
5694
  normalizeFieldValidation,
5227
5695
  resolveServiceFallback,