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