@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.
- package/dist/core/index.cjs +1327 -866
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +13 -1
- package/dist/core/index.d.ts +13 -1
- package/dist/core/index.js +1325 -866
- package/dist/core/index.js.map +1 -1
- package/dist/react/index.cjs +579 -379
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +579 -379
- package/dist/react/index.js.map +1 -1
- package/dist/workspace/index.cjs +588 -392
- package/dist/workspace/index.cjs.map +1 -1
- package/dist/workspace/index.js +588 -392
- package/dist/workspace/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.cjs
CHANGED
|
@@ -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
|
|
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 (
|
|
661
|
+
if (effectfulKeys.has(t)) triggers.push(t);
|
|
666
662
|
}
|
|
667
663
|
for (const o of (_a = f.options) != null ? _a : []) {
|
|
668
|
-
const t =
|
|
669
|
-
if (
|
|
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((
|
|
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
|
-
|
|
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 (
|
|
1245
|
-
|
|
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/
|
|
1569
|
-
function
|
|
1570
|
-
const
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
|
1647
|
+
return false;
|
|
1609
1648
|
}
|
|
1610
|
-
function
|
|
1611
|
-
var _a, _b;
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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 (
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
if (
|
|
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
|
-
|
|
1831
|
+
return members;
|
|
1721
1832
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
|
1749
|
-
|
|
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: "
|
|
2127
|
+
code: "rate_coherence_violation",
|
|
1752
2128
|
severity: "error",
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
details:
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
-
|
|
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/
|
|
1771
|
-
function
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
|
1788
|
-
|
|
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
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
-
|
|
2793
|
-
|
|
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
|
|
2918
|
+
function resolveFallbackSettings(options) {
|
|
2919
|
+
var _a;
|
|
2796
2920
|
return {
|
|
2797
|
-
|
|
2798
|
-
|
|
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
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
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
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
-
|
|
2950
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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,
|