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