@timeax/digital-service-engine 0.2.6 → 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];
@@ -1227,10 +1258,19 @@ function validateOrderKinds(v) {
1227
1258
  }
1228
1259
 
1229
1260
  // src/core/validate/steps/service-vs-input.ts
1261
+ function hasButtonTriggerMap(v, fieldId) {
1262
+ var _a, _b;
1263
+ const includes = (_a = v.props.includes_for_buttons) == null ? void 0 : _a[fieldId];
1264
+ const excludes = (_b = v.props.excludes_for_buttons) == null ? void 0 : _b[fieldId];
1265
+ return Array.isArray(includes) && includes.length > 0 || Array.isArray(excludes) && excludes.length > 0;
1266
+ }
1230
1267
  function validateServiceVsUserInput(v) {
1231
1268
  for (const f of v.fields) {
1232
1269
  const anySvc = hasAnyServiceOption(f);
1233
1270
  const hasName = !!(f.name && f.name.trim());
1271
+ const isButton2 = f.button === true;
1272
+ const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
1273
+ const hasTriggerMap = isButton2 && hasButtonTriggerMap(v, f.id);
1234
1274
  if (f.type === "custom" && anySvc) {
1235
1275
  v.errors.push({
1236
1276
  code: "user_input_field_has_service_option",
@@ -1241,14 +1281,15 @@ function validateServiceVsUserInput(v) {
1241
1281
  });
1242
1282
  }
1243
1283
  if (!hasName) {
1244
- if (!anySvc) {
1245
- v.errors.push({
1246
- code: "service_field_missing_service_id",
1247
- severity: "error",
1248
- message: `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
1249
- nodeId: f.id
1250
- });
1284
+ if (hasFieldService || anySvc || hasTriggerMap) {
1285
+ continue;
1251
1286
  }
1287
+ v.errors.push({
1288
+ code: "service_field_missing_service_id",
1289
+ severity: "error",
1290
+ message: isButton2 ? `Button field "${f.id}" has no "name", no "service_id", and no includes/excludes trigger map. Add a name, attach a service_id, or configure includes_for_buttons/excludes_for_buttons.` : `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
1291
+ nodeId: f.id
1292
+ });
1252
1293
  } else {
1253
1294
  if (anySvc) {
1254
1295
  v.errors.push({
@@ -1565,227 +1606,802 @@ function validateRates(v) {
1565
1606
  }
1566
1607
  }
1567
1608
 
1568
- // src/core/validate/steps/constraints.ts
1569
- function constraintKeysInChain(v, tagId) {
1570
- const keys = [];
1571
- const seenKeys = /* @__PURE__ */ new Set();
1572
- let cur = tagId;
1573
- const seenTags = /* @__PURE__ */ new Set();
1574
- while (cur && !seenTags.has(cur)) {
1575
- seenTags.add(cur);
1576
- const t = v.tagById.get(cur);
1577
- const c = t == null ? void 0 : t.constraints;
1578
- if (c && typeof c === "object") {
1579
- for (const k of Object.keys(c)) {
1580
- if (!seenKeys.has(k)) {
1581
- seenKeys.add(k);
1582
- keys.push(k);
1583
- }
1584
- }
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);
1615
+ }
1616
+ return Array.from(out);
1617
+ }
1618
+ function buildTriggerEffectMap(props) {
1619
+ var _a, _b;
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);
1585
1626
  }
1586
- cur = t == null ? void 0 : t.bind_id;
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);
1587
1632
  }
1588
- return keys;
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);
1636
+ }
1637
+ return map;
1589
1638
  }
1590
- function effectiveConstraints(v, tagId) {
1591
- var _a;
1592
- const out = {};
1593
- const keys = constraintKeysInChain(v, tagId);
1594
- for (const key of keys) {
1595
- let cur = tagId;
1596
- const seen = /* @__PURE__ */ new Set();
1597
- while (cur && !seen.has(cur)) {
1598
- seen.add(cur);
1599
- const t = v.tagById.get(cur);
1600
- const val = (_a = t == null ? void 0 : t.constraints) == null ? void 0 : _a[key];
1601
- if (val === true || val === false) {
1602
- out[key] = val;
1603
- break;
1604
- }
1605
- cur = t == null ? void 0 : t.bind_id;
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;
1606
1645
  }
1607
1646
  }
1608
- return out;
1647
+ return false;
1609
1648
  }
1610
- function validateConstraints(v) {
1611
- var _a, _b;
1612
- for (const t of v.tags) {
1613
- const eff = effectiveConstraints(v, t.id);
1614
- const hasAnyRequired = Object.values(eff).some(
1615
- (x) => x === true
1649
+ function validateRateCoherenceDeep(params) {
1650
+ var _a, _b, _c;
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
+ });
1693
+ }
1694
+ const references = visibleFields.flatMap(
1695
+ (field) => collectFieldReferences(field, services)
1616
1696
  );
1617
- if (!hasAnyRequired) continue;
1618
- const visible = v.fieldsVisibleUnder(t.id);
1619
- for (const f of visible) {
1620
- for (const o of (_a = f.options) != null ? _a : []) {
1621
- if (!isServiceIdRef(o.service_id)) continue;
1622
- const svc = getServiceCapability(v.serviceMap, o.service_id);
1623
- if (!svc || typeof svc !== "object") continue;
1624
- for (const [k, val] of Object.entries(eff)) {
1625
- if (val === true && !isServiceFlagEnabled(svc, k)) {
1626
- v.errors.push({
1627
- code: "unsupported_constraint",
1628
- severity: "error",
1629
- message: `Service option "${o.id}" under tag "${t.id}" does not support required constraint "${k}".`,
1630
- nodeId: t.id,
1631
- details: withAffected(
1632
- {
1633
- flag: k,
1634
- serviceId: o.service_id,
1635
- fieldId: f.id,
1636
- optionId: o.id
1637
- },
1638
- [t.id, f.id, o.id]
1639
- )
1640
- });
1641
- }
1642
- }
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;
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;
1643
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
+ });
1644
1752
  }
1645
1753
  }
1646
- for (const t of v.tags) {
1647
- const sid = t.service_id;
1648
- if (!isServiceIdRef(sid)) continue;
1649
- const svc = getServiceCapability(v.serviceMap, sid);
1650
- if (!svc || typeof svc !== "object") continue;
1651
- const eff = effectiveConstraints(v, t.id);
1652
- for (const [k, val] of Object.entries(eff)) {
1653
- if (val === true && !isServiceFlagEnabled(svc, k)) {
1654
- v.errors.push({
1655
- code: "unsupported_constraint",
1656
- severity: "error",
1657
- message: `Tag "${t.id}" maps to service "${String(
1658
- sid
1659
- )}" which does not support required constraint "${k}".`,
1660
- nodeId: t.id,
1661
- details: { flag: k, serviceId: sid }
1754
+ return diagnostics;
1755
+ }
1756
+ function collectAnchors(fields) {
1757
+ var _a, _b;
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
1662
1768
  });
1663
1769
  }
1770
+ continue;
1664
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
+ });
1665
1778
  }
1666
- for (const t of v.tags) {
1667
- const ov = t.constraints_overrides;
1668
- if (!ov || typeof ov !== "object") continue;
1669
- for (const k of Object.keys(ov)) {
1670
- const row = ov[k];
1671
- if (!row) continue;
1672
- const from = row.from === true;
1673
- const to = row.to === true;
1674
- const origin = String((_b = row.origin) != null ? _b : "");
1675
- v.errors.push({
1676
- code: "constraint_overridden",
1677
- severity: "warning",
1678
- message: origin ? `Constraint "${k}" on tag "${t.id}" was overridden by ancestor "${origin}" (${String(from)} \u2192 ${String(
1679
- to
1680
- )}).` : `Constraint "${k}" on tag "${t.id}" was overridden by an ancestor (${String(from)} \u2192 ${String(to)}).`,
1681
- nodeId: t.id,
1682
- details: withAffected(
1683
- { flag: k, from, to, origin },
1684
- origin ? [t.id, origin] : void 0
1685
- )
1686
- });
1687
- }
1688
- }
1779
+ return anchors;
1689
1780
  }
1690
-
1691
- // src/core/validate/steps/custom.ts
1692
- function validateCustomFields(v) {
1693
- for (const f of v.fields) {
1694
- if (f.type !== "custom") continue;
1695
- if (!f.component || !String(f.component).trim()) {
1696
- v.errors.push({
1697
- code: "custom_component_missing",
1698
- severity: "error",
1699
- message: `Custom field "${f.id}" is missing a valid component reference.`,
1700
- nodeId: f.id
1701
- });
1702
- }
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
+ ];
1703
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
+ }));
1704
1807
  }
1705
-
1706
- // src/core/validate/steps/global-utility-guard.ts
1707
- function validateGlobalUtilityGuard(v) {
1808
+ function collectBaseMembers(field, services) {
1708
1809
  var _a, _b, _c;
1709
- if (!v.options.globalUtilityGuard) return;
1710
- let hasUtility = false;
1711
- let hasBase = false;
1712
- for (const f of v.fields) {
1713
- for (const o of (_a = f.options) != null ? _a : []) {
1714
- if (!isServiceIdRef(o.service_id)) continue;
1715
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1716
- if (role === "base") hasBase = true;
1717
- else if (role === "utility") hasUtility = true;
1718
- if (hasUtility && hasBase) break;
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
+ });
1719
1830
  }
1720
- if (hasUtility && hasBase) break;
1831
+ return members;
1721
1832
  }
1722
- if (hasUtility && !hasBase) {
1723
- v.errors.push({
1724
- code: "utility_without_base",
1725
- severity: "warning",
1726
- message: "Global utility guard: utility-priced options exist but no base-priced options were found.",
1727
- nodeId: "global",
1728
- details: { scope: "global" }
1729
- });
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;
1730
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;
1731
1849
  }
1732
-
1733
- // src/core/validate/steps/unbound.ts
1734
- function validateUnboundFields(v) {
1850
+ function isButton(field) {
1851
+ if (field.button === true) return true;
1852
+ return Array.isArray(field.options) && field.options.length > 0;
1853
+ }
1854
+ function normalizeRole(role, fallback) {
1855
+ return role === "base" || role === "utility" ? role : fallback;
1856
+ }
1857
+ function toDiagnosticRef(reference) {
1858
+ return {
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
1865
+ };
1866
+ }
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("|");
1878
+ }
1879
+ function describeLabel(tag) {
1735
1880
  var _a, _b;
1736
- const boundFieldIds = /* @__PURE__ */ new Set();
1737
- for (const f of v.fields) {
1738
- if (f.bind_id) boundFieldIds.add(f.id);
1881
+ return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
1882
+ }
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}.`;
1739
1896
  }
1740
- const includedByTag = /* @__PURE__ */ new Set();
1741
- for (const t of v.tags) {
1742
- for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
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);
1743
1908
  }
1744
- const includedByOption = /* @__PURE__ */ new Set();
1745
- for (const arr of Object.values((_b = v.props.includes_for_buttons) != null ? _b : {})) {
1746
- for (const id of arr != null ? arr : []) includedByOption.add(id);
1909
+ return Array.from(out);
1910
+ }
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) {
1918
+ var _a, _b, _c, _d, _e;
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
+ };
1933
+ }
1747
1934
  }
1748
- for (const f of v.fields) {
1749
- if (!boundFieldIds.has(f.id) && !includedByTag.has(f.id) && !includedByOption.has(f.id)) {
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
+ }
1951
+ }
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")
1965
+ });
1966
+ }
1967
+ }
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
+ }
2017
+ });
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;
2037
+ }
2038
+ if (survivingRefs.length <= 1) continue;
2039
+ const survivingSelected = survivingRefs.filter(
2040
+ (ref) => selectedSet.has(ref.key)
2041
+ );
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
+ });
2102
+ }
2103
+ }
2104
+ }
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);
1750
2126
  v.errors.push({
1751
- code: "field_unbound",
2127
+ code: "rate_coherence_violation",
1752
2128
  severity: "error",
1753
- message: `Field "${f.id}" is unbound: it is not bound to any tag and not included by tags or option maps.`,
1754
- nodeId: f.id,
1755
- details: withAffected(
1756
- {
1757
- fieldId: f.id,
1758
- bound: false,
1759
- // exposing these helps editors explain "why"
1760
- includedByTag: includedByTag.has(f.id),
1761
- includedByOption: includedByOption.has(f.id)
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
1762
2141
  },
1763
- [f.id]
1764
- )
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
+ }
1765
2164
  });
1766
2165
  }
1767
2166
  }
1768
2167
  }
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
+ });
2181
+ }
2182
+ }
1769
2183
 
1770
- // src/core/validate/steps/fallbacks.ts
1771
- function codeForReason(reason) {
1772
- switch (reason) {
1773
- case "unknown_service":
1774
- return "fallback_unknown_service";
1775
- case "no_primary":
1776
- return "fallback_no_primary";
1777
- case "rate_violation":
1778
- return "fallback_rate_violation";
1779
- case "constraint_mismatch":
1780
- return "fallback_constraint_mismatch";
1781
- case "cycle":
1782
- return "fallback_cycle";
1783
- default:
1784
- return "fallback_bad_node";
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
+ }
2201
+ }
2202
+ cur = t == null ? void 0 : t.bind_id;
1785
2203
  }
2204
+ return keys;
1786
2205
  }
1787
- function messageFor(code, d) {
1788
- const n = d.nodeId ? `node "${String(d.nodeId)}"` : "node";
2206
+ function effectiveConstraints(v, tagId) {
2207
+ var _a;
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;
2220
+ }
2221
+ cur = t == null ? void 0 : t.bind_id;
2222
+ }
2223
+ }
2224
+ return out;
2225
+ }
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
2232
+ );
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
+ });
2257
+ }
2258
+ }
2259
+ }
2260
+ }
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}".`,
2276
+ nodeId: t.id,
2277
+ details: { flag: k, serviceId: sid }
2278
+ });
2279
+ }
2280
+ }
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
+ }
2305
+ }
2306
+
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
+ }
2319
+ }
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;
2337
+ }
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" }
2345
+ });
2346
+ }
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);
2355
+ }
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);
2359
+ }
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);
2363
+ }
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
+ )
2381
+ });
2382
+ }
2383
+ }
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";
2401
+ }
2402
+ }
2403
+ function messageFor(code, d) {
2404
+ const n = d.nodeId ? `node "${String(d.nodeId)}"` : "node";
1789
2405
  switch (code) {
1790
2406
  case "fallback_unknown_service":
1791
2407
  return `Fallback candidate "${String(
@@ -2216,622 +2832,109 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
2216
2832
  });
2217
2833
  }
2218
2834
  }
2219
- }
2220
- items = Array.from(merged.values());
2221
- } else {
2222
- const allFields = (_e = props.fields) != null ? _e : [];
2223
- items = collectServiceItems({
2224
- mode: "global",
2225
- props,
2226
- serviceMap,
2227
- tags,
2228
- fields: allFields,
2229
- filter: rule.filter
2230
- });
2231
- }
2232
- const values = items.map(
2233
- (it) => getByPath(it, projPath)
2234
- );
2235
- if (!evalPolicyOp(rule.op, values, rule)) {
2236
- errors.push({
2237
- code: "policy_violation",
2238
- severity,
2239
- message,
2240
- nodeId: "global",
2241
- details: {
2242
- ruleId: rule.id,
2243
- scope: "global",
2244
- op: rule.op,
2245
- projection: projPath,
2246
- count: items.length,
2247
- affectedIds: affectedFromItems(items)
2248
- }
2249
- });
2250
- }
2251
- continue;
2252
- }
2253
- for (const t of tags) {
2254
- const visibleFields = fieldsVisibleUnder(t.id);
2255
- const nodeIds = visibleGroupNodeIds(t, visibleFields);
2256
- const primaries = visibleGroupPrimaries(t, visibleFields);
2257
- const items = collectServiceItems({
2258
- mode: "visible_group",
2259
- props,
2260
- serviceMap,
2261
- tag: t,
2262
- tagId: t.id,
2263
- fields: visibleFields,
2264
- filter: rule.filter,
2265
- visibleNodeIds: nodeIds,
2266
- visiblePrimaries: primaries
2267
- });
2268
- if (!items.length) continue;
2269
- const values = items.map(
2270
- (it) => getByPath(it, projPath)
2271
- );
2272
- if (!evalPolicyOp(rule.op, values, rule)) {
2273
- errors.push({
2274
- code: "policy_violation",
2275
- severity,
2276
- message,
2277
- nodeId: t.id,
2278
- details: {
2279
- ruleId: rule.id,
2280
- scope: "visible_group",
2281
- op: rule.op,
2282
- projection: projPath,
2283
- count: items.length,
2284
- affectedIds: affectedFromItems(items)
2285
- }
2286
- });
2287
- }
2288
- }
2289
- }
2290
- }
2291
-
2292
- // src/core/governance.ts
2293
- var DEFAULT_FALLBACK_SETTINGS = {
2294
- requireConstraintFit: true,
2295
- ratePolicy: { kind: "lte_primary", pct: 5 },
2296
- selectionStrategy: "priority",
2297
- mode: "strict"
2298
- };
2299
- function resolveGlobalRatePolicy(options) {
2300
- return normalizeRatePolicy(options.ratePolicy);
2301
- }
2302
- function resolveFallbackSettings(options) {
2303
- var _a;
2304
- return {
2305
- ...DEFAULT_FALLBACK_SETTINGS,
2306
- ...(_a = options.fallbackSettings) != null ? _a : {}
2307
- };
2308
- }
2309
- function mergeValidatorOptions(defaults = {}, overrides = {}) {
2310
- var _a, _b, _c, _d;
2311
- const mergedFallbackSettings = {
2312
- ...(_a = defaults.fallbackSettings) != null ? _a : {},
2313
- ...(_b = overrides.fallbackSettings) != null ? _b : {}
2314
- };
2315
- return {
2316
- ...defaults,
2317
- ...overrides,
2318
- policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2319
- ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2320
- fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2321
- };
2322
- }
2323
-
2324
- // src/core/builder.ts
2325
- var import_lodash_es2 = require("lodash-es");
2326
- function createBuilder(opts = {}) {
2327
- return new BuilderImpl(opts);
2328
- }
2329
- var BuilderImpl = class {
2330
- constructor(opts = {}) {
2331
- this.props = {
2332
- filters: [],
2333
- fields: [],
2334
- schema_version: "1.0"
2335
- };
2336
- this.tagById = /* @__PURE__ */ new Map();
2337
- this.fieldById = /* @__PURE__ */ new Map();
2338
- this.optionOwnerById = /* @__PURE__ */ new Map();
2339
- this._nodemap = null;
2340
- this.options = { ...opts };
2341
- }
2342
- /* ───── lifecycle ─────────────────────────────────────────────────────── */
2343
- isTagId(id) {
2344
- return this.tagById.has(id);
2345
- }
2346
- isFieldId(id) {
2347
- return this.fieldById.has(id);
2348
- }
2349
- isOptionId(id) {
2350
- return this.optionOwnerById.has(id);
2351
- }
2352
- load(raw) {
2353
- const next = normalise(raw, {
2354
- defaultPricingRole: "base",
2355
- constraints: this.getConstraints().map((item) => item.label)
2356
- });
2357
- this.props = next;
2358
- this.rebuildIndexes();
2359
- }
2360
- getProps() {
2361
- return this.props;
2362
- }
2363
- setOptions(patch) {
2364
- this.options = { ...this.options, ...patch };
2365
- }
2366
- getServiceMap() {
2367
- var _a;
2368
- return (_a = this.options.serviceMap) != null ? _a : {};
2369
- }
2370
- getConstraints() {
2371
- var _a;
2372
- const serviceMap = this.getServiceMap();
2373
- const out = /* @__PURE__ */ new Set();
2374
- const guard = /* @__PURE__ */ new Set();
2375
- for (const svc of Object.values(serviceMap)) {
2376
- const flags = (_a = svc.flags) != null ? _a : {};
2377
- for (const flagId of Object.keys(flags)) {
2378
- if (guard.has(flagId)) continue;
2379
- guard.add(flagId);
2380
- out.add({
2381
- id: flagId,
2382
- value: flagId,
2383
- label: flagId,
2384
- description: flags[flagId].description
2385
- });
2386
- }
2387
- }
2388
- return Array.from(out);
2389
- }
2390
- /* ───── querying ─────────────────────────────────────────────────────── */
2391
- tree() {
2392
- var _a, _b, _c, _d;
2393
- const nodes = [];
2394
- const edges = [];
2395
- const showSet = toStringSet(this.options.showOptionNodes);
2396
- for (const t of this.props.filters) {
2397
- nodes.push({ id: t.id, kind: "tag", label: t.label });
2398
- }
2399
- for (const t of this.props.filters) {
2400
- if (t.bind_id) {
2401
- edges.push({
2402
- from: t.bind_id,
2403
- to: t.id,
2404
- kind: "child"
2405
- });
2406
- }
2407
- }
2408
- for (const f of this.props.fields) {
2409
- nodes.push({
2410
- id: f.id,
2411
- kind: "field",
2412
- label: f.label,
2413
- bind_type: f.pricing_role === "utility" ? "utility" : f.bind_id ? "bound" : null
2414
- });
2415
- }
2416
- for (const f of this.props.fields) {
2417
- const b = f.bind_id;
2418
- if (Array.isArray(b)) {
2419
- for (const tagId of b)
2420
- edges.push({
2421
- from: tagId,
2422
- to: f.id,
2423
- kind: "bind"
2424
- });
2425
- } else if (typeof b === "string") {
2426
- edges.push({ from: b, to: f.id, kind: "bind" });
2427
- }
2428
- }
2429
- for (const f of this.props.fields) {
2430
- const showOptions = showSet.has(f.id);
2431
- if (!showOptions) continue;
2432
- if (!Array.isArray(f.options)) continue;
2433
- for (const o of f.options) {
2434
- nodes.push({
2435
- id: o.id,
2436
- kind: "option",
2437
- label: o.label
2438
- });
2439
- const e = {
2440
- from: f.id,
2441
- to: o.id,
2442
- kind: "option",
2443
- meta: { ownerField: f.id }
2444
- };
2445
- edges.push(e);
2446
- }
2447
- }
2448
- for (const t of this.props.filters) {
2449
- for (const id of (_a = t.includes) != null ? _a : []) {
2450
- edges.push({ from: t.id, to: id, kind: "include" });
2451
- }
2452
- for (const id of (_b = t.excludes) != null ? _b : []) {
2453
- edges.push({ from: t.id, to: id, kind: "exclude" });
2454
- }
2455
- }
2456
- const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
2457
- const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
2458
- const pushButtonEdge = (keyId, targetFieldId, kind) => {
2459
- var _a2;
2460
- const owner = this.optionOwnerById.get(keyId);
2461
- const ownerFieldId = (_a2 = owner == null ? void 0 : owner.fieldId) != null ? _a2 : this.fieldById.has(keyId) ? keyId : void 0;
2462
- if (!ownerFieldId) return;
2463
- const fromNode = owner && showSet.has(owner.fieldId) ? keyId : ownerFieldId;
2464
- const meta = owner ? showSet.has(owner.fieldId) ? {
2465
- via: "option-visible",
2466
- ownerField: owner.fieldId,
2467
- sourceOption: keyId
2468
- } : {
2469
- via: "option-hidden",
2470
- ownerField: owner.fieldId,
2471
- sourceOption: keyId
2472
- } : { via: "field-button" };
2473
- const e = { from: fromNode, to: targetFieldId, kind, meta };
2474
- edges.push(e);
2475
- };
2476
- for (const [keyId, arr] of Object.entries(incMap)) {
2477
- for (const fid of arr != null ? arr : [])
2478
- pushButtonEdge(keyId, fid, "include");
2479
- }
2480
- for (const [keyId, arr] of Object.entries(excMap)) {
2481
- for (const fid of arr != null ? arr : [])
2482
- pushButtonEdge(keyId, fid, "exclude");
2483
- }
2484
- return { nodes, edges };
2485
- }
2486
- cleanedProps() {
2487
- var _a, _b, _c, _d, _e;
2488
- const fieldIds = new Set(this.props.fields.map((f) => f.id));
2489
- const optionIds = /* @__PURE__ */ new Set();
2490
- this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
2491
- const includedByTag = /* @__PURE__ */ new Set();
2492
- const excludedAnywhere = /* @__PURE__ */ new Set();
2493
- for (const t of this.props.filters) {
2494
- for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
2495
- for (const id of (_b = t.excludes) != null ? _b : []) excludedAnywhere.add(id);
2496
- }
2497
- const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
2498
- const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
2499
- const includedByButtons = /* @__PURE__ */ new Set();
2500
- const referencedKeys = /* @__PURE__ */ new Set();
2501
- const referencedOwnerFields = /* @__PURE__ */ new Set();
2502
- for (const [key, arr] of Object.entries(incMap)) {
2503
- referencedKeys.add(key);
2504
- const owner = this.optionOwnerById.get(key);
2505
- if (owner) referencedOwnerFields.add(owner.fieldId);
2506
- for (const fid of arr != null ? arr : []) {
2507
- includedByButtons.add(fid);
2508
- }
2509
- }
2510
- for (const [key, arr] of Object.entries(excMap)) {
2511
- referencedKeys.add(key);
2512
- const owner = this.optionOwnerById.get(key);
2513
- if (owner) referencedOwnerFields.add(owner.fieldId);
2514
- for (const fid of arr != null ? arr : []) {
2515
- void fid;
2516
- }
2517
- }
2518
- const boundIds = /* @__PURE__ */ new Set();
2519
- for (const f of this.props.fields) {
2520
- const b = f.bind_id;
2521
- if (Array.isArray(b)) b.forEach((id) => boundIds.add(id));
2522
- else if (typeof b === "string") boundIds.add(b);
2523
- }
2524
- const fields = this.props.fields.filter((f) => {
2525
- var _a2;
2526
- const isUtility = ((_a2 = f.pricing_role) != null ? _a2 : "base") === "utility";
2527
- if (!isUtility) return true;
2528
- const bound = !!f.bind_id;
2529
- const included = includedByTag.has(f.id) || includedByButtons.has(f.id);
2530
- const referenced = referencedOwnerFields.has(f.id) || referencedKeys.has(f.id);
2531
- const excluded = excludedAnywhere.has(f.id);
2532
- return bound || included || referenced || !excluded;
2533
- });
2534
- const allowedTargets = new Set(fields.map((f) => f.id));
2535
- const pruneButtons = (src) => {
2536
- if (!src) return void 0;
2537
- const out2 = {};
2538
- for (const [key, arr] of Object.entries(src)) {
2539
- const keyIsValid = optionIds.has(key) || fieldIds.has(key);
2540
- if (!keyIsValid) continue;
2541
- const cleaned = (arr != null ? arr : []).filter(
2542
- (fid) => allowedTargets.has(fid)
2543
- );
2544
- if (cleaned.length) out2[key] = Array.from(new Set(cleaned));
2545
- }
2546
- return Object.keys(out2).length ? out2 : void 0;
2547
- };
2548
- const includes_for_buttons = pruneButtons(
2549
- this.props.includes_for_buttons
2550
- );
2551
- const excludes_for_buttons = pruneButtons(
2552
- this.props.excludes_for_buttons
2553
- );
2554
- const out = {
2555
- filters: this.props.filters.slice(),
2556
- fields,
2557
- ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
2558
- ...includes_for_buttons && { includes_for_buttons },
2559
- ...excludes_for_buttons && { excludes_for_buttons },
2560
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
2561
- // keep fallbacks & other maps as-is
2562
- ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
2563
- };
2564
- return out;
2565
- }
2566
- errors() {
2567
- return validate(this.props, mergeValidatorOptions({}, this.options));
2568
- }
2569
- getOptions() {
2570
- return (0, import_lodash_es2.cloneDeep)(this.options);
2571
- }
2572
- visibleFields(tagId, selectedKeys) {
2573
- var _a;
2574
- return visibleFieldIdsUnder(this.props, tagId, {
2575
- selectedKeys: new Set(
2576
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
2577
- )
2578
- });
2579
- }
2580
- getNodeMap() {
2581
- if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
2582
- return this._nodemap;
2583
- }
2584
- /* ───── internals ──────────────────────────────────────────────────── */
2585
- rebuildIndexes() {
2586
- this.tagById.clear();
2587
- this.fieldById.clear();
2588
- this.optionOwnerById.clear();
2589
- this._nodemap = null;
2590
- for (const t of this.props.filters) this.tagById.set(t.id, t);
2591
- for (const f of this.props.fields) {
2592
- this.fieldById.set(f.id, f);
2593
- if (Array.isArray(f.options)) {
2594
- for (const o of f.options)
2595
- this.optionOwnerById.set(o.id, { fieldId: f.id });
2596
- }
2597
- }
2598
- }
2599
- };
2600
- function toStringSet(v) {
2601
- if (!v) return /* @__PURE__ */ new Set();
2602
- if (v instanceof Set) return new Set(Array.from(v).map(String));
2603
- return new Set(v.map(String));
2604
- }
2605
-
2606
- // src/core/rate-coherence.ts
2607
- function validateRateCoherenceDeep(params) {
2608
- var _a, _b, _c;
2609
- const { builder, services, tagId } = params;
2610
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
2611
- const props = builder.getProps();
2612
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
2613
- const fields = (_b = props.fields) != null ? _b : [];
2614
- const fieldById = new Map(fields.map((f) => [f.id, f]));
2615
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
2616
- const tag = tagById.get(tagId);
2617
- const baselineFieldIds = builder.visibleFields(tagId, []);
2618
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
2619
- const anchors = collectAnchors(baselineFields);
2620
- const diagnostics = [];
2621
- const seen = /* @__PURE__ */ new Set();
2622
- for (const anchor of anchors) {
2623
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
2624
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
2625
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
2626
- for (const fieldId of visibleInvalidFieldIds) {
2627
- const key = `internal|${tagId}|${fieldId}`;
2628
- if (seen.has(key)) continue;
2629
- seen.add(key);
2630
- diagnostics.push({
2631
- kind: "internal_field",
2632
- scope: "visible_group",
2633
- tagId,
2634
- fieldId,
2635
- nodeId: fieldId,
2636
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
2637
- simulationAnchor: {
2638
- kind: anchor.kind,
2639
- id: anchor.id,
2640
- fieldId: anchor.fieldId,
2641
- label: anchor.label
2642
- },
2643
- invalidFieldIds: [fieldId]
2644
- });
2645
- }
2646
- const references = visibleFields.flatMap(
2647
- (field) => collectFieldReferences(field, services)
2648
- );
2649
- if (references.length <= 1) continue;
2650
- const primary = references.reduce((best, current) => {
2651
- if (current.rate !== best.rate) {
2652
- return current.rate > best.rate ? current : best;
2653
- }
2654
- const bestKey = `${best.fieldId}|${best.nodeId}`;
2655
- const currentKey = `${current.fieldId}|${current.nodeId}`;
2656
- return currentKey < bestKey ? current : best;
2657
- });
2658
- for (const candidate of references) {
2659
- if (candidate.nodeId === primary.nodeId) continue;
2660
- if (candidate.fieldId === primary.fieldId) continue;
2661
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
2662
- continue;
2663
- }
2664
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
2665
- if (seen.has(key)) continue;
2666
- seen.add(key);
2667
- diagnostics.push({
2668
- kind: "contextual",
2669
- scope: "visible_group",
2670
- tagId,
2671
- nodeId: candidate.nodeId,
2672
- primary: toDiagnosticRef(primary),
2673
- offender: toDiagnosticRef(candidate),
2674
- policy: ratePolicy.kind,
2675
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2676
- message: explainRateMismatch(
2677
- ratePolicy,
2678
- primary,
2679
- candidate,
2680
- describeLabel(tag)
2681
- ),
2682
- simulationAnchor: {
2683
- kind: anchor.kind,
2684
- id: anchor.id,
2685
- fieldId: anchor.fieldId,
2686
- label: anchor.label
2687
- },
2688
- invalidFieldIds: visibleInvalidFieldIds
2689
- });
2690
- }
2691
- }
2692
- return diagnostics;
2693
- }
2694
- function collectAnchors(fields) {
2695
- var _a, _b;
2696
- const anchors = [];
2697
- for (const field of fields) {
2698
- if (!isButton(field)) continue;
2699
- if (Array.isArray(field.options) && field.options.length > 0) {
2700
- for (const option of field.options) {
2701
- anchors.push({
2702
- kind: "option",
2703
- id: option.id,
2704
- fieldId: field.id,
2705
- label: (_a = option.label) != null ? _a : option.id
2706
- });
2707
- }
2708
- continue;
2709
- }
2710
- anchors.push({
2711
- kind: "field",
2712
- id: field.id,
2713
- fieldId: field.id,
2714
- label: (_b = field.label) != null ? _b : field.id
2715
- });
2716
- }
2717
- return anchors;
2718
- }
2719
- function collectFieldReferences(field, services) {
2720
- var _a;
2721
- const members = collectBaseMembers(field, services);
2722
- if (members.length === 0) return [];
2723
- if (isMultiField(field)) {
2724
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
2725
- return [
2726
- {
2727
- refKind: "multi",
2728
- nodeId: field.id,
2729
- fieldId: field.id,
2730
- label: (_a = field.label) != null ? _a : field.id,
2731
- rate: averageRate,
2732
- members
2733
- }
2734
- ];
2735
- }
2736
- return members.map((member) => ({
2737
- refKind: "single",
2738
- nodeId: member.id,
2739
- fieldId: field.id,
2740
- label: member.label,
2741
- rate: member.rate,
2742
- service_id: member.service_id,
2743
- members: [member]
2744
- }));
2745
- }
2746
- function collectBaseMembers(field, services) {
2747
- var _a, _b, _c;
2748
- const members = [];
2749
- if (Array.isArray(field.options) && field.options.length > 0) {
2750
- for (const option of field.options) {
2751
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
2752
- if (role2 !== "base") continue;
2753
- if (option.service_id === void 0 || option.service_id === null) {
2754
- continue;
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
+ });
2755
2847
  }
2756
- const cap2 = getServiceCapability(services, option.service_id);
2757
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
2758
- 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
+ });
2759
2866
  }
2760
- members.push({
2761
- kind: "option",
2762
- id: option.id,
2763
- fieldId: field.id,
2764
- label: (_b = option.label) != null ? _b : option.id,
2765
- service_id: option.service_id,
2766
- 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
2767
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
+ }
2768
2904
  }
2769
- return members;
2770
- }
2771
- const role = normalizeRole(field.pricing_role, "base");
2772
- if (role !== "base") return members;
2773
- if (field.service_id === void 0 || field.service_id === null) return members;
2774
- const cap = getServiceCapability(services, field.service_id);
2775
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
2776
- return members;
2777
2905
  }
2778
- members.push({
2779
- kind: "field",
2780
- id: field.id,
2781
- fieldId: field.id,
2782
- label: (_c = field.label) != null ? _c : field.id,
2783
- service_id: field.service_id,
2784
- rate: cap.rate
2785
- });
2786
- return members;
2787
- }
2788
- function isButton(field) {
2789
- if (field.button === true) return true;
2790
- return Array.isArray(field.options) && field.options.length > 0;
2791
2906
  }
2792
- function normalizeRole(role, fallback) {
2793
- 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);
2794
2917
  }
2795
- function toDiagnosticRef(reference) {
2918
+ function resolveFallbackSettings(options) {
2919
+ var _a;
2796
2920
  return {
2797
- nodeId: reference.nodeId,
2798
- fieldId: reference.fieldId,
2799
- label: reference.label,
2800
- refKind: reference.refKind,
2801
- service_id: reference.service_id,
2802
- rate: reference.rate
2921
+ ...DEFAULT_FALLBACK_SETTINGS,
2922
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2803
2923
  };
2804
2924
  }
2805
- function contextualKey(tagId, primary, candidate, ratePolicy) {
2806
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
2807
- return [
2808
- "contextual",
2809
- tagId,
2810
- primary.fieldId,
2811
- primary.nodeId,
2812
- candidate.fieldId,
2813
- candidate.nodeId,
2814
- `${ratePolicy.kind}${pctKey}`
2815
- ].join("|");
2816
- }
2817
- function describeLabel(tag) {
2818
- var _a, _b;
2819
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
2820
- }
2821
- function explainRateMismatch(policy, primary, candidate, where) {
2822
- var _a, _b;
2823
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
2824
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
2825
- switch (policy.kind) {
2826
- case "eq_primary":
2827
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
2828
- case "lte_primary":
2829
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
2830
- case "within_pct":
2831
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
2832
- case "at_least_pct_lower":
2833
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
2834
- }
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
+ };
2835
2938
  }
2836
2939
 
2837
2940
  // src/core/validate/index.ts
@@ -2882,7 +2985,8 @@ function validate(props, ctx = {}) {
2882
2985
  invalidRateFieldIds: /* @__PURE__ */ new Set(),
2883
2986
  tagById,
2884
2987
  fieldById,
2885
- fieldsVisibleUnder: (_tagId) => []
2988
+ fieldsVisibleUnder: (_tagId) => [],
2989
+ simulatedVisibilityContexts: []
2886
2990
  };
2887
2991
  validateStructure(v);
2888
2992
  validateIdentity(v);
@@ -2902,54 +3006,306 @@ function validate(props, ctx = {}) {
2902
3006
  validateServiceVsUserInput(v);
2903
3007
  validateUtilityMarkers(v);
2904
3008
  validateRates(v);
2905
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
2906
- const builder = createBuilder({ serviceMap });
2907
- builder.load(props);
2908
- for (const tag of tags) {
2909
- const diags = validateRateCoherenceDeep({
2910
- builder,
2911
- services: serviceMap,
2912
- tagId: tag.id,
2913
- ratePolicy,
2914
- 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
2915
3119
  });
2916
- for (const diag of diags) {
2917
- if (diag.kind !== "contextual") continue;
2918
- errors.push({
2919
- code: "rate_coherence_violation",
2920
- severity: "error",
2921
- message: diag.message,
2922
- nodeId: diag.nodeId,
2923
- details: {
2924
- tagId: diag.tagId,
2925
- simulationAnchor: diag.simulationAnchor,
2926
- primary: diag.primary,
2927
- offender: diag.offender,
2928
- policy: diag.policy,
2929
- policyPct: diag.policyPct,
2930
- invalidFieldIds: diag.invalidFieldIds
2931
- }
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
2932
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" });
2933
3159
  }
2934
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 };
2935
3190
  }
2936
- validateConstraints(v);
2937
- validateCustomFields(v);
2938
- validateGlobalUtilityGuard(v);
2939
- validateUnboundFields(v);
2940
- validateFallbacks(v);
2941
- return v.errors;
2942
- }
2943
- async function validateAsync(props, ctx = {}) {
2944
- await Promise.resolve();
2945
- if (typeof requestAnimationFrame === "function") {
2946
- await new Promise(
2947
- (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
2948
3255
  );
2949
- } else {
2950
- 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;
2951
3270
  }
2952
- 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));
2953
3309
  }
2954
3310
 
2955
3311
  // src/core/fallback.ts
@@ -3947,22 +4303,15 @@ function compilePolicies(raw) {
3947
4303
 
3948
4304
  // src/core/service-filter.ts
3949
4305
  function filterServicesForVisibleGroup(input, deps) {
3950
- 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;
3951
4307
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
3952
4308
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
3953
4309
  const { context } = input;
3954
4310
  const usedSet = new Set(context.usedServiceIds.map(String));
3955
- const primary = context.usedServiceIds[0];
3956
4311
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
3957
4312
  const resolvedRatePolicy = normalizeRatePolicy(
3958
4313
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
3959
4314
  );
3960
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
3961
- const fb = {
3962
- ...DEFAULT_FALLBACK_SETTINGS,
3963
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
3964
- ratePolicy: resolvedRatePolicy
3965
- };
3966
4315
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
3967
4316
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
3968
4317
  deps.builder,
@@ -3990,7 +4339,15 @@ function filterServicesForVisibleGroup(input, deps) {
3990
4339
  cap.id,
3991
4340
  (_k = context.effectiveConstraints) != null ? _k : {}
3992
4341
  );
3993
- 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
+ );
3994
4351
  const polRes = evaluatePoliciesRaw(
3995
4352
  policySource,
3996
4353
  [...context.usedServiceIds, id],
@@ -4109,7 +4466,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
4109
4466
  const fields = (_b = props.fields) != null ? _b : [];
4110
4467
  const tag = tags.find((t) => t.id === tagId);
4111
4468
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
4112
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
4469
+ const visibleFieldIds = new Set(
4470
+ builder.visibleFields(tagId, selectedButtons)
4471
+ );
4113
4472
  for (const field of fields) {
4114
4473
  if (!visibleFieldIds.has(field.id)) continue;
4115
4474
  if (field.service_id != null) {
@@ -4132,8 +4491,7 @@ function matchesRuleFilter(cap, rule, tagId) {
4132
4491
  if (!cap) return false;
4133
4492
  const f = rule.filter;
4134
4493
  if (!f) return true;
4135
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
4136
- return true;
4494
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
4137
4495
  }
4138
4496
  function toStrSet(v) {
4139
4497
  const arr = Array.isArray(v) ? v : [v];
@@ -4141,6 +4499,107 @@ function toStrSet(v) {
4141
4499
  for (const x of arr) s.add(String(x));
4142
4500
  return s;
4143
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
+ }
4144
4603
 
4145
4604
  // src/utils/prune-fallbacks.ts
4146
4605
  function pruneInvalidNodeFallbacks(props, services, settings) {
@@ -5245,6 +5704,7 @@ function mapDiagReason(reason) {
5245
5704
  // Annotate the CommonJS export names for ESM import in node:
5246
5705
  0 && (module.exports = {
5247
5706
  buildOrderSnapshot,
5707
+ buildTriggerEffectMap,
5248
5708
  collectFailedFallbacks,
5249
5709
  createBuilder,
5250
5710
  createFallbackEditor,
@@ -5253,6 +5713,7 @@ function mapDiagReason(reason) {
5253
5713
  getAssignedServiceIds,
5254
5714
  getEligibleFallbacks,
5255
5715
  getFallbackRegistrationInfo,
5716
+ isRefExcludedBySelectedKeys,
5256
5717
  normalise,
5257
5718
  normalizeFieldValidation,
5258
5719
  resolveServiceFallback,