@timeax/digital-service-engine 0.2.7 → 0.2.8

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