@timeax/digital-service-engine 0.0.5 → 0.0.7
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 +1149 -504
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +114 -42
- package/dist/core/index.d.ts +114 -42
- package/dist/core/index.js +1147 -504
- package/dist/core/index.js.map +1 -1
- package/dist/react/index.cjs +4763 -3239
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +400 -244
- package/dist/react/index.d.ts +400 -244
- package/dist/react/index.js +4763 -3239
- package/dist/react/index.js.map +1 -1
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +163 -66
- package/dist/schema/index.d.ts +163 -66
- package/dist/workspace/index.cjs +5622 -2576
- package/dist/workspace/index.cjs.map +1 -1
- package/dist/workspace/index.d.cts +494 -263
- package/dist/workspace/index.d.ts +494 -263
- package/dist/workspace/index.js +5642 -2598
- package/dist/workspace/index.js.map +1 -1
- package/package.json +9 -9
- package/schema/editor-snapshot.schema.json +373 -9
- package/schema/service-props.schema.json +86 -9
package/dist/core/index.js
CHANGED
|
@@ -13,6 +13,7 @@ function normalise(input, opts = {}) {
|
|
|
13
13
|
const excludes_for_buttons = toStringArrayMap(
|
|
14
14
|
obj.excludes_for_buttons
|
|
15
15
|
);
|
|
16
|
+
const notices = toNoticeArray(obj.notices);
|
|
16
17
|
let filters = rawFilters.map((t) => coerceTag(t, constraints));
|
|
17
18
|
const fields = rawFields.map((f) => coerceField(f, defRole));
|
|
18
19
|
if (!filters.some((t) => t.id === "t:root")) {
|
|
@@ -28,6 +29,7 @@ function normalise(input, opts = {}) {
|
|
|
28
29
|
...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
|
|
29
30
|
fallbacks
|
|
30
31
|
},
|
|
32
|
+
...notices.length > 0 && { notices },
|
|
31
33
|
schema_version: typeof obj.schema_version === "string" ? obj.schema_version : "1.0"
|
|
32
34
|
};
|
|
33
35
|
propagateConstraints(out, constraints);
|
|
@@ -99,7 +101,7 @@ function coerceTag(src, flagKeys) {
|
|
|
99
101
|
const id = str(src.id);
|
|
100
102
|
const label = str(src.label);
|
|
101
103
|
const bind_id = str(src.bind_id) || (id == "t:root" ? void 0 : "t:root");
|
|
102
|
-
const service_id =
|
|
104
|
+
const service_id = toServiceIdOrUndefined(src.service_id);
|
|
103
105
|
const includes = toStringArray(src.includes);
|
|
104
106
|
const excludes = toStringArray(src.excludes);
|
|
105
107
|
let constraints = void 0;
|
|
@@ -148,7 +150,8 @@ function coerceField(src, defRole) {
|
|
|
148
150
|
const component = type === "custom" ? str(src.component) || void 0 : void 0;
|
|
149
151
|
const meta = src.meta && typeof src.meta === "object" ? { ...src.meta } : void 0;
|
|
150
152
|
const button = srcHasOptions ? true : src.button === true;
|
|
151
|
-
const
|
|
153
|
+
const validation = normalizeFieldValidation(src.validation);
|
|
154
|
+
const field_service_id_raw = toServiceIdOrUndefined(src.service_id);
|
|
152
155
|
const field_service_id = button && pricing_role !== "utility" && field_service_id_raw !== void 0 ? field_service_id_raw : void 0;
|
|
153
156
|
const field = {
|
|
154
157
|
id,
|
|
@@ -163,6 +166,7 @@ function coerceField(src, defRole) {
|
|
|
163
166
|
...ui && { ui },
|
|
164
167
|
...defaults && { defaults },
|
|
165
168
|
...meta && { meta },
|
|
169
|
+
...validation && { validation },
|
|
166
170
|
...button ? { button } : {},
|
|
167
171
|
...field_service_id !== void 0 && { service_id: field_service_id }
|
|
168
172
|
};
|
|
@@ -172,7 +176,7 @@ function coerceOption(src, inheritRole) {
|
|
|
172
176
|
if (!src || typeof src !== "object") src = {};
|
|
173
177
|
const id = str(src.id);
|
|
174
178
|
const label = str(src.label);
|
|
175
|
-
const service_id =
|
|
179
|
+
const service_id = toServiceIdOrUndefined(src.service_id);
|
|
176
180
|
const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
|
|
177
181
|
const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
|
|
178
182
|
const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
|
|
@@ -244,10 +248,20 @@ function toStringArray(v) {
|
|
|
244
248
|
if (!Array.isArray(v)) return [];
|
|
245
249
|
return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
|
|
246
250
|
}
|
|
247
|
-
function
|
|
251
|
+
function toNoticeArray(v) {
|
|
252
|
+
if (!Array.isArray(v)) return [];
|
|
253
|
+
return v.filter((item) => item && typeof item === "object").map((item) => cloneDeep(item));
|
|
254
|
+
}
|
|
255
|
+
function toServiceIdOrUndefined(v) {
|
|
248
256
|
if (v === null || v === void 0) return void 0;
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
if (typeof v === "number") {
|
|
258
|
+
return Number.isFinite(v) ? v : void 0;
|
|
259
|
+
}
|
|
260
|
+
if (typeof v === "string") {
|
|
261
|
+
const trimmed = v.trim();
|
|
262
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
263
|
+
}
|
|
264
|
+
return void 0;
|
|
251
265
|
}
|
|
252
266
|
function str(v) {
|
|
253
267
|
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
@@ -271,14 +285,49 @@ function toServiceIdArray(v) {
|
|
|
271
285
|
(x) => x !== "" && x !== null && x !== void 0
|
|
272
286
|
);
|
|
273
287
|
}
|
|
288
|
+
function normalizeFieldValidationRule(input) {
|
|
289
|
+
if (!input || typeof input !== "object") return void 0;
|
|
290
|
+
const v = input;
|
|
291
|
+
const op = v.op;
|
|
292
|
+
if (op !== "eq" && op !== "neq" && op !== "gt" && op !== "gte" && op !== "lt" && op !== "lte" && op !== "between" && op !== "in" && op !== "nin" && op !== "truthy" && op !== "falsy" && op !== "match") {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const valueBy = v.valueBy === "value" || v.valueBy === "length" || v.valueBy === "eval" ? v.valueBy : void 0;
|
|
296
|
+
const out = {
|
|
297
|
+
op,
|
|
298
|
+
...valueBy ? { valueBy } : {}
|
|
299
|
+
};
|
|
300
|
+
if ("value" in v) out.value = v.value;
|
|
301
|
+
if (typeof v.min === "number" && Number.isFinite(v.min)) out.min = v.min;
|
|
302
|
+
if (typeof v.max === "number" && Number.isFinite(v.max)) out.max = v.max;
|
|
303
|
+
if (Array.isArray(v.values)) out.values = [...v.values];
|
|
304
|
+
if (typeof v.pattern === "string" && v.pattern.trim()) out.pattern = v.pattern;
|
|
305
|
+
if (typeof v.flags === "string") out.flags = v.flags;
|
|
306
|
+
if (typeof v.message === "string" && v.message.trim()) out.message = v.message;
|
|
307
|
+
if (valueBy === "eval" && typeof v.code === "string" && v.code.trim()) {
|
|
308
|
+
out.code = v.code;
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
function normalizeFieldValidation(input) {
|
|
313
|
+
if (Array.isArray(input)) {
|
|
314
|
+
const rules = input.map(normalizeFieldValidationRule).filter(Boolean);
|
|
315
|
+
return rules.length ? rules : void 0;
|
|
316
|
+
}
|
|
317
|
+
const one = normalizeFieldValidationRule(input);
|
|
318
|
+
return one ? [one] : void 0;
|
|
319
|
+
}
|
|
274
320
|
|
|
275
321
|
// src/core/validate/shared.ts
|
|
276
322
|
function isFiniteNumber(v) {
|
|
277
323
|
return typeof v === "number" && Number.isFinite(v);
|
|
278
324
|
}
|
|
325
|
+
function isServiceIdRef(v) {
|
|
326
|
+
return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
|
|
327
|
+
}
|
|
279
328
|
function hasAnyServiceOption(f) {
|
|
280
329
|
var _a;
|
|
281
|
-
return ((_a = f.options) != null ? _a : []).some((o) =>
|
|
330
|
+
return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
|
|
282
331
|
}
|
|
283
332
|
function getByPath(obj, path) {
|
|
284
333
|
if (!path) return void 0;
|
|
@@ -624,7 +673,7 @@ function runVisibilityRulesOnce(v) {
|
|
|
624
673
|
const utilityOptionIds = [];
|
|
625
674
|
for (const f of visible) {
|
|
626
675
|
for (const o of (_c = f.options) != null ? _c : []) {
|
|
627
|
-
if (!
|
|
676
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
628
677
|
const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
|
|
629
678
|
if (role === "base") hasBase = true;
|
|
630
679
|
else if (role === "utility") {
|
|
@@ -1052,7 +1101,7 @@ function validateUtilityMarkers(v) {
|
|
|
1052
1101
|
const optsArr = Array.isArray(f.options) ? f.options : [];
|
|
1053
1102
|
for (const o of optsArr) {
|
|
1054
1103
|
const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
|
|
1055
|
-
const hasService =
|
|
1104
|
+
const hasService = isServiceIdRef(o.service_id);
|
|
1056
1105
|
const util = (_c = o.meta) == null ? void 0 : _c.utility;
|
|
1057
1106
|
if (role === "utility" && hasService) {
|
|
1058
1107
|
v.errors.push({
|
|
@@ -1138,39 +1187,119 @@ function isMultiField(f) {
|
|
|
1138
1187
|
return t === "multiselect" || t === "checkbox" || metaMulti;
|
|
1139
1188
|
}
|
|
1140
1189
|
|
|
1190
|
+
// src/utils/util.ts
|
|
1191
|
+
function toFiniteNumber(v) {
|
|
1192
|
+
const n = Number(v);
|
|
1193
|
+
return Number.isFinite(n) ? n : NaN;
|
|
1194
|
+
}
|
|
1195
|
+
function constraintFitOk(svcMap, candidate, constraints) {
|
|
1196
|
+
const cap = getServiceCapability(svcMap, candidate);
|
|
1197
|
+
if (!cap) return false;
|
|
1198
|
+
if (constraints.dripfeed === true && !cap.dripfeed) return false;
|
|
1199
|
+
if (constraints.refill === true && !cap.refill) return false;
|
|
1200
|
+
return !(constraints.cancel === true && !cap.cancel);
|
|
1201
|
+
}
|
|
1202
|
+
function getServiceCapability(svcMap, candidate) {
|
|
1203
|
+
if (candidate === void 0 || candidate === null) return void 0;
|
|
1204
|
+
const direct = svcMap[candidate];
|
|
1205
|
+
if (direct) return direct;
|
|
1206
|
+
const byString = svcMap[String(candidate)];
|
|
1207
|
+
if (byString) return byString;
|
|
1208
|
+
if (typeof candidate === "string") {
|
|
1209
|
+
const maybeNumber = Number(candidate);
|
|
1210
|
+
if (Number.isFinite(maybeNumber)) {
|
|
1211
|
+
return svcMap[maybeNumber];
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return void 0;
|
|
1215
|
+
}
|
|
1216
|
+
function normalizeRatePolicy(policy) {
|
|
1217
|
+
var _a;
|
|
1218
|
+
if (!policy) return { kind: "lte_primary", pct: 5 };
|
|
1219
|
+
if (policy.kind === "eq_primary") return policy;
|
|
1220
|
+
const pct = Math.max(0, Number((_a = policy.pct) != null ? _a : 0));
|
|
1221
|
+
return { ...policy, pct };
|
|
1222
|
+
}
|
|
1223
|
+
function passesRatePolicy(policy, primaryRate, candidateRate) {
|
|
1224
|
+
if (!Number.isFinite(primaryRate) || !Number.isFinite(candidateRate)) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
const rp = normalizeRatePolicy(policy);
|
|
1228
|
+
switch (rp.kind) {
|
|
1229
|
+
case "eq_primary":
|
|
1230
|
+
return candidateRate === primaryRate;
|
|
1231
|
+
case "lte_primary": {
|
|
1232
|
+
const floor = primaryRate * (1 - rp.pct / 100);
|
|
1233
|
+
return candidateRate <= primaryRate && candidateRate >= floor;
|
|
1234
|
+
}
|
|
1235
|
+
case "within_pct":
|
|
1236
|
+
return candidateRate <= primaryRate * (1 + rp.pct / 100);
|
|
1237
|
+
case "at_least_pct_lower":
|
|
1238
|
+
return candidateRate <= primaryRate * (1 - rp.pct / 100);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function rateOk(svcMap, candidate, primary, policy) {
|
|
1242
|
+
const cand = getServiceCapability(svcMap, candidate);
|
|
1243
|
+
const prim = getServiceCapability(svcMap, primary);
|
|
1244
|
+
if (!cand || !prim) return false;
|
|
1245
|
+
const cRate = toFiniteNumber(cand.rate);
|
|
1246
|
+
const pRate = toFiniteNumber(prim.rate);
|
|
1247
|
+
if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
|
|
1248
|
+
return passesRatePolicy(policy.ratePolicy, pRate, cRate);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1141
1251
|
// src/core/validate/steps/rates.ts
|
|
1142
1252
|
function validateRates(v) {
|
|
1143
1253
|
var _a, _b, _c, _d;
|
|
1254
|
+
const ratePolicy = normalizeRatePolicy(
|
|
1255
|
+
(_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
|
|
1256
|
+
);
|
|
1144
1257
|
for (const f of v.fields) {
|
|
1145
1258
|
if (!isMultiField(f)) continue;
|
|
1146
|
-
const baseRates =
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
1259
|
+
const baseRates = [];
|
|
1260
|
+
for (const o of (_b = f.options) != null ? _b : []) {
|
|
1261
|
+
const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
|
|
1150
1262
|
if (role !== "base") continue;
|
|
1151
1263
|
const sid = o.service_id;
|
|
1152
|
-
if (!
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1264
|
+
if (!isServiceIdRef(sid)) continue;
|
|
1265
|
+
const cap = getServiceCapability(v.serviceMap, sid);
|
|
1266
|
+
const rate = cap == null ? void 0 : cap.rate;
|
|
1267
|
+
if (typeof rate === "number" && Number.isFinite(rate)) {
|
|
1268
|
+
baseRates.push({
|
|
1269
|
+
optionId: o.id,
|
|
1270
|
+
serviceId: String(sid),
|
|
1271
|
+
rate
|
|
1272
|
+
});
|
|
1157
1273
|
}
|
|
1158
1274
|
}
|
|
1159
|
-
if (baseRates.
|
|
1275
|
+
if (baseRates.length <= 1) continue;
|
|
1276
|
+
const primary = baseRates.reduce(
|
|
1277
|
+
(best, current) => current.rate > best.rate ? current : best
|
|
1278
|
+
);
|
|
1279
|
+
const offenders = baseRates.filter(
|
|
1280
|
+
(candidate) => candidate.optionId !== primary.optionId && !passesRatePolicy(ratePolicy, primary.rate, candidate.rate)
|
|
1281
|
+
);
|
|
1282
|
+
if (offenders.length > 0) {
|
|
1283
|
+
v.invalidRateFieldIds.add(f.id);
|
|
1160
1284
|
const affectedIds = [
|
|
1161
1285
|
f.id,
|
|
1162
|
-
...
|
|
1286
|
+
...baseRates.map((entry) => entry.optionId)
|
|
1163
1287
|
];
|
|
1164
1288
|
v.errors.push({
|
|
1165
1289
|
code: "rate_mismatch_across_base",
|
|
1166
1290
|
severity: "error",
|
|
1167
|
-
message: `Base options under field "${f.id}"
|
|
1291
|
+
message: `Base options under field "${f.id}" violate rate policy "${ratePolicy.kind}".`,
|
|
1168
1292
|
nodeId: f.id,
|
|
1169
1293
|
details: withAffected(
|
|
1170
1294
|
{
|
|
1171
1295
|
fieldId: f.id,
|
|
1172
|
-
|
|
1173
|
-
|
|
1296
|
+
policy: ratePolicy.kind,
|
|
1297
|
+
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
1298
|
+
primaryOptionId: primary.optionId,
|
|
1299
|
+
primaryRate: primary.rate,
|
|
1300
|
+
rates: baseRates.map((entry) => entry.rate),
|
|
1301
|
+
optionIds: baseRates.map((entry) => entry.optionId),
|
|
1302
|
+
offenderOptionIds: offenders.map((entry) => entry.optionId)
|
|
1174
1303
|
},
|
|
1175
1304
|
affectedIds.length > 1 ? affectedIds : void 0
|
|
1176
1305
|
)
|
|
@@ -1232,8 +1361,8 @@ function validateConstraints(v) {
|
|
|
1232
1361
|
const visible = v.fieldsVisibleUnder(t.id);
|
|
1233
1362
|
for (const f of visible) {
|
|
1234
1363
|
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1235
|
-
if (!
|
|
1236
|
-
const svc = v.serviceMap
|
|
1364
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
1365
|
+
const svc = getServiceCapability(v.serviceMap, o.service_id);
|
|
1237
1366
|
if (!svc || typeof svc !== "object") continue;
|
|
1238
1367
|
for (const [k, val] of Object.entries(eff)) {
|
|
1239
1368
|
if (val === true && !isServiceFlagEnabled(svc, k)) {
|
|
@@ -1259,8 +1388,8 @@ function validateConstraints(v) {
|
|
|
1259
1388
|
}
|
|
1260
1389
|
for (const t of v.tags) {
|
|
1261
1390
|
const sid = t.service_id;
|
|
1262
|
-
if (!
|
|
1263
|
-
const svc = v.serviceMap
|
|
1391
|
+
if (!isServiceIdRef(sid)) continue;
|
|
1392
|
+
const svc = getServiceCapability(v.serviceMap, sid);
|
|
1264
1393
|
if (!svc || typeof svc !== "object") continue;
|
|
1265
1394
|
const eff = effectiveConstraints(v, t.id);
|
|
1266
1395
|
for (const [k, val] of Object.entries(eff)) {
|
|
@@ -1325,7 +1454,7 @@ function validateGlobalUtilityGuard(v) {
|
|
|
1325
1454
|
let hasBase = false;
|
|
1326
1455
|
for (const f of v.fields) {
|
|
1327
1456
|
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1328
|
-
if (!
|
|
1457
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
1329
1458
|
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
1330
1459
|
if (role === "base") hasBase = true;
|
|
1331
1460
|
else if (role === "utility") hasUtility = true;
|
|
@@ -1470,7 +1599,7 @@ function asArray(v) {
|
|
|
1470
1599
|
if (v === void 0) return void 0;
|
|
1471
1600
|
return Array.isArray(v) ? v : [v];
|
|
1472
1601
|
}
|
|
1473
|
-
function
|
|
1602
|
+
function isServiceIdRef2(v) {
|
|
1474
1603
|
return typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
1475
1604
|
}
|
|
1476
1605
|
function svcSnapshot(serviceMap, sid) {
|
|
@@ -1546,7 +1675,7 @@ function collectServiceItems(args) {
|
|
|
1546
1675
|
if (args.mode === "global") {
|
|
1547
1676
|
for (const t of (_b = args.tags) != null ? _b : []) {
|
|
1548
1677
|
const sid = t.service_id;
|
|
1549
|
-
if (!
|
|
1678
|
+
if (!isServiceIdRef2(sid)) continue;
|
|
1550
1679
|
addServiceRef({
|
|
1551
1680
|
tagId: t.id,
|
|
1552
1681
|
serviceId: sid,
|
|
@@ -1557,7 +1686,7 @@ function collectServiceItems(args) {
|
|
|
1557
1686
|
} else if (args.mode === "visible_group") {
|
|
1558
1687
|
const t = args.tag;
|
|
1559
1688
|
const sid = t ? t.service_id : void 0;
|
|
1560
|
-
if (t &&
|
|
1689
|
+
if (t && isServiceIdRef2(sid)) {
|
|
1561
1690
|
addServiceRef({
|
|
1562
1691
|
tagId: t.id,
|
|
1563
1692
|
serviceId: sid,
|
|
@@ -1569,7 +1698,7 @@ function collectServiceItems(args) {
|
|
|
1569
1698
|
const fields = (_c = args.fields) != null ? _c : [];
|
|
1570
1699
|
for (const f of fields) {
|
|
1571
1700
|
const fSid = f.service_id;
|
|
1572
|
-
if (
|
|
1701
|
+
if (isServiceIdRef2(fSid)) {
|
|
1573
1702
|
addServiceRef({
|
|
1574
1703
|
tagId: args.tagId,
|
|
1575
1704
|
fieldId: f.id,
|
|
@@ -1580,7 +1709,7 @@ function collectServiceItems(args) {
|
|
|
1580
1709
|
}
|
|
1581
1710
|
for (const o of (_d = f.options) != null ? _d : []) {
|
|
1582
1711
|
const oSid = o.service_id;
|
|
1583
|
-
if (!
|
|
1712
|
+
if (!isServiceIdRef2(oSid)) continue;
|
|
1584
1713
|
const role = fieldRoleOf(f, o);
|
|
1585
1714
|
addServiceRef({
|
|
1586
1715
|
tagId: args.tagId,
|
|
@@ -1601,7 +1730,7 @@ function collectServiceItems(args) {
|
|
|
1601
1730
|
const addFallbackNode = (nodeId, list) => {
|
|
1602
1731
|
const arr = Array.isArray(list) ? list : [];
|
|
1603
1732
|
for (const cand of arr) {
|
|
1604
|
-
if (!
|
|
1733
|
+
if (!isServiceIdRef2(cand)) continue;
|
|
1605
1734
|
addServiceRef({
|
|
1606
1735
|
tagId: args.tagId,
|
|
1607
1736
|
nodeId,
|
|
@@ -1625,7 +1754,7 @@ function collectServiceItems(args) {
|
|
|
1625
1754
|
});
|
|
1626
1755
|
const arr = Array.isArray(list) ? list : [];
|
|
1627
1756
|
for (const cand of arr) {
|
|
1628
|
-
if (!
|
|
1757
|
+
if (!isServiceIdRef2(cand)) continue;
|
|
1629
1758
|
addServiceRef({
|
|
1630
1759
|
tagId: args.tagId,
|
|
1631
1760
|
nodeId: primaryKey,
|
|
@@ -1903,85 +2032,8 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
|
|
|
1903
2032
|
}
|
|
1904
2033
|
}
|
|
1905
2034
|
|
|
1906
|
-
// src/core/validate/index.ts
|
|
1907
|
-
function readVisibilitySimOpts(ctx) {
|
|
1908
|
-
const c = ctx;
|
|
1909
|
-
const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
|
|
1910
|
-
const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
|
|
1911
|
-
const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
|
|
1912
|
-
const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
|
|
1913
|
-
const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
|
|
1914
|
-
return {
|
|
1915
|
-
simulate,
|
|
1916
|
-
maxStates,
|
|
1917
|
-
maxDepth,
|
|
1918
|
-
simulateAllRoots,
|
|
1919
|
-
onlyEffectfulTriggers
|
|
1920
|
-
};
|
|
1921
|
-
}
|
|
1922
|
-
function validate(props, ctx = {}) {
|
|
1923
|
-
var _a, _b, _c;
|
|
1924
|
-
const errors = [];
|
|
1925
|
-
const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
|
|
1926
|
-
const selectedKeys = new Set(
|
|
1927
|
-
(_b = ctx.selectedOptionKeys) != null ? _b : []
|
|
1928
|
-
);
|
|
1929
|
-
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
1930
|
-
const fields = Array.isArray(props.fields) ? props.fields : [];
|
|
1931
|
-
const tagById = /* @__PURE__ */ new Map();
|
|
1932
|
-
const fieldById = /* @__PURE__ */ new Map();
|
|
1933
|
-
for (const t of tags) tagById.set(t.id, t);
|
|
1934
|
-
for (const f of fields) fieldById.set(f.id, f);
|
|
1935
|
-
const v = {
|
|
1936
|
-
props,
|
|
1937
|
-
nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
|
|
1938
|
-
options: ctx,
|
|
1939
|
-
errors,
|
|
1940
|
-
serviceMap,
|
|
1941
|
-
selectedKeys,
|
|
1942
|
-
tags,
|
|
1943
|
-
fields,
|
|
1944
|
-
tagById,
|
|
1945
|
-
fieldById,
|
|
1946
|
-
fieldsVisibleUnder: (_tagId) => []
|
|
1947
|
-
};
|
|
1948
|
-
validateStructure(v);
|
|
1949
|
-
validateIdentity(v);
|
|
1950
|
-
validateOptionMaps(v);
|
|
1951
|
-
v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
|
|
1952
|
-
const visSim = readVisibilitySimOpts(ctx);
|
|
1953
|
-
validateVisibility(v, visSim);
|
|
1954
|
-
applyPolicies(
|
|
1955
|
-
v.errors,
|
|
1956
|
-
v.props,
|
|
1957
|
-
v.serviceMap,
|
|
1958
|
-
v.options.policies,
|
|
1959
|
-
v.fieldsVisibleUnder,
|
|
1960
|
-
v.tags
|
|
1961
|
-
);
|
|
1962
|
-
validateServiceVsUserInput(v);
|
|
1963
|
-
validateUtilityMarkers(v);
|
|
1964
|
-
validateRates(v);
|
|
1965
|
-
validateConstraints(v);
|
|
1966
|
-
validateCustomFields(v);
|
|
1967
|
-
validateGlobalUtilityGuard(v);
|
|
1968
|
-
validateUnboundFields(v);
|
|
1969
|
-
validateFallbacks(v);
|
|
1970
|
-
return v.errors;
|
|
1971
|
-
}
|
|
1972
|
-
async function validateAsync(props, ctx = {}) {
|
|
1973
|
-
await Promise.resolve();
|
|
1974
|
-
if (typeof requestAnimationFrame === "function") {
|
|
1975
|
-
await new Promise(
|
|
1976
|
-
(resolve) => requestAnimationFrame(() => resolve())
|
|
1977
|
-
);
|
|
1978
|
-
} else {
|
|
1979
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1980
|
-
}
|
|
1981
|
-
return validate(props, ctx);
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
2035
|
// src/core/builder.ts
|
|
2036
|
+
import { cloneDeep as cloneDeep2 } from "lodash-es";
|
|
1985
2037
|
function createBuilder(opts = {}) {
|
|
1986
2038
|
return new BuilderImpl(opts);
|
|
1987
2039
|
}
|
|
@@ -1995,12 +2047,8 @@ var BuilderImpl = class {
|
|
|
1995
2047
|
this.tagById = /* @__PURE__ */ new Map();
|
|
1996
2048
|
this.fieldById = /* @__PURE__ */ new Map();
|
|
1997
2049
|
this.optionOwnerById = /* @__PURE__ */ new Map();
|
|
1998
|
-
this.history = [];
|
|
1999
|
-
this.future = [];
|
|
2000
2050
|
this._nodemap = null;
|
|
2001
|
-
var _a;
|
|
2002
2051
|
this.options = { ...opts };
|
|
2003
|
-
this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
|
|
2004
2052
|
}
|
|
2005
2053
|
/* ───── lifecycle ─────────────────────────────────────────────────────── */
|
|
2006
2054
|
isTagId(id) {
|
|
@@ -2017,8 +2065,6 @@ var BuilderImpl = class {
|
|
|
2017
2065
|
defaultPricingRole: "base",
|
|
2018
2066
|
constraints: this.getConstraints().map((item) => item.label)
|
|
2019
2067
|
});
|
|
2020
|
-
this.pushHistory(this.props);
|
|
2021
|
-
this.future.length = 0;
|
|
2022
2068
|
this.props = next;
|
|
2023
2069
|
this.rebuildIndexes();
|
|
2024
2070
|
}
|
|
@@ -2230,6 +2276,9 @@ var BuilderImpl = class {
|
|
|
2230
2276
|
errors() {
|
|
2231
2277
|
return validate(this.props, this.options);
|
|
2232
2278
|
}
|
|
2279
|
+
getOptions() {
|
|
2280
|
+
return cloneDeep2(this.options);
|
|
2281
|
+
}
|
|
2233
2282
|
visibleFields(tagId, selectedKeys) {
|
|
2234
2283
|
var _a;
|
|
2235
2284
|
return visibleFieldIdsUnder(this.props, tagId, {
|
|
@@ -2242,23 +2291,6 @@ var BuilderImpl = class {
|
|
|
2242
2291
|
if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
|
|
2243
2292
|
return this._nodemap;
|
|
2244
2293
|
}
|
|
2245
|
-
/* ───── history ─────────────────────────────────────────────────────── */
|
|
2246
|
-
undo() {
|
|
2247
|
-
if (this.history.length === 0) return false;
|
|
2248
|
-
const prev = this.history.pop();
|
|
2249
|
-
this.future.push(structuredCloneSafe(this.props));
|
|
2250
|
-
this.props = prev;
|
|
2251
|
-
this.rebuildIndexes();
|
|
2252
|
-
return true;
|
|
2253
|
-
}
|
|
2254
|
-
redo() {
|
|
2255
|
-
if (this.future.length === 0) return false;
|
|
2256
|
-
const next = this.future.pop();
|
|
2257
|
-
this.pushHistory(this.props);
|
|
2258
|
-
this.props = next;
|
|
2259
|
-
this.rebuildIndexes();
|
|
2260
|
-
return true;
|
|
2261
|
-
}
|
|
2262
2294
|
/* ───── internals ──────────────────────────────────────────────────── */
|
|
2263
2295
|
rebuildIndexes() {
|
|
2264
2296
|
this.tagById.clear();
|
|
@@ -2274,99 +2306,429 @@ var BuilderImpl = class {
|
|
|
2274
2306
|
}
|
|
2275
2307
|
}
|
|
2276
2308
|
}
|
|
2277
|
-
pushHistory(state) {
|
|
2278
|
-
if (!state || !state.filters.length && !state.fields.length) return;
|
|
2279
|
-
this.history.push(structuredCloneSafe(state));
|
|
2280
|
-
if (this.history.length > this.historyLimit) this.history.shift();
|
|
2281
|
-
}
|
|
2282
2309
|
};
|
|
2283
|
-
function structuredCloneSafe(v) {
|
|
2284
|
-
if (typeof globalThis.structuredClone === "function") {
|
|
2285
|
-
return globalThis.structuredClone(v);
|
|
2286
|
-
}
|
|
2287
|
-
return JSON.parse(JSON.stringify(v));
|
|
2288
|
-
}
|
|
2289
2310
|
function toStringSet(v) {
|
|
2290
2311
|
if (!v) return /* @__PURE__ */ new Set();
|
|
2291
2312
|
if (v instanceof Set) return new Set(Array.from(v).map(String));
|
|
2292
2313
|
return new Set(v.map(String));
|
|
2293
2314
|
}
|
|
2294
2315
|
|
|
2295
|
-
// src/core/
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
const
|
|
2305
|
-
const
|
|
2306
|
-
const
|
|
2307
|
-
const
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
const
|
|
2317
|
-
if (
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2316
|
+
// src/core/rate-coherence.ts
|
|
2317
|
+
function validateRateCoherenceDeep(params) {
|
|
2318
|
+
var _a, _b, _c;
|
|
2319
|
+
const { builder, services, tagId } = params;
|
|
2320
|
+
const ratePolicy = normalizeRatePolicy(params.ratePolicy);
|
|
2321
|
+
const props = builder.getProps();
|
|
2322
|
+
const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
|
|
2323
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
2324
|
+
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2325
|
+
const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
|
|
2326
|
+
const tag = tagById.get(tagId);
|
|
2327
|
+
const baselineFieldIds = builder.visibleFields(tagId, []);
|
|
2328
|
+
const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2329
|
+
const anchors = collectAnchors(baselineFields);
|
|
2330
|
+
const diagnostics = [];
|
|
2331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2332
|
+
for (const anchor of anchors) {
|
|
2333
|
+
const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
|
|
2334
|
+
const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2335
|
+
const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
|
|
2336
|
+
for (const fieldId of visibleInvalidFieldIds) {
|
|
2337
|
+
const key = `internal|${tagId}|${fieldId}`;
|
|
2338
|
+
if (seen.has(key)) continue;
|
|
2339
|
+
seen.add(key);
|
|
2340
|
+
diagnostics.push({
|
|
2341
|
+
kind: "internal_field",
|
|
2342
|
+
scope: "visible_group",
|
|
2343
|
+
tagId,
|
|
2344
|
+
fieldId,
|
|
2345
|
+
nodeId: fieldId,
|
|
2346
|
+
message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
|
|
2347
|
+
simulationAnchor: {
|
|
2348
|
+
kind: anchor.kind,
|
|
2349
|
+
id: anchor.id,
|
|
2350
|
+
fieldId: anchor.fieldId,
|
|
2351
|
+
label: anchor.label
|
|
2352
|
+
},
|
|
2353
|
+
invalidFieldIds: [fieldId]
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
const references = visibleFields.flatMap(
|
|
2357
|
+
(field) => collectFieldReferences(field, services)
|
|
2358
|
+
);
|
|
2359
|
+
if (references.length <= 1) continue;
|
|
2360
|
+
const primary = references.reduce((best, current) => {
|
|
2361
|
+
if (current.rate !== best.rate) {
|
|
2362
|
+
return current.rate > best.rate ? current : best;
|
|
2322
2363
|
}
|
|
2323
|
-
|
|
2364
|
+
const bestKey = `${best.fieldId}|${best.nodeId}`;
|
|
2365
|
+
const currentKey = `${current.fieldId}|${current.nodeId}`;
|
|
2366
|
+
return currentKey < bestKey ? current : best;
|
|
2367
|
+
});
|
|
2368
|
+
for (const candidate of references) {
|
|
2369
|
+
if (candidate.nodeId === primary.nodeId) continue;
|
|
2370
|
+
if (candidate.fieldId === primary.fieldId) continue;
|
|
2371
|
+
if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
const key = contextualKey(tagId, primary, candidate, ratePolicy);
|
|
2375
|
+
if (seen.has(key)) continue;
|
|
2376
|
+
seen.add(key);
|
|
2377
|
+
diagnostics.push({
|
|
2378
|
+
kind: "contextual",
|
|
2379
|
+
scope: "visible_group",
|
|
2380
|
+
tagId,
|
|
2381
|
+
nodeId: candidate.nodeId,
|
|
2382
|
+
primary: toDiagnosticRef(primary),
|
|
2383
|
+
offender: toDiagnosticRef(candidate),
|
|
2384
|
+
policy: ratePolicy.kind,
|
|
2385
|
+
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
2386
|
+
message: explainRateMismatch(
|
|
2387
|
+
ratePolicy,
|
|
2388
|
+
primary,
|
|
2389
|
+
candidate,
|
|
2390
|
+
describeLabel(tag)
|
|
2391
|
+
),
|
|
2392
|
+
simulationAnchor: {
|
|
2393
|
+
kind: anchor.kind,
|
|
2394
|
+
id: anchor.id,
|
|
2395
|
+
fieldId: anchor.fieldId,
|
|
2396
|
+
label: anchor.label
|
|
2397
|
+
},
|
|
2398
|
+
invalidFieldIds: visibleInvalidFieldIds
|
|
2399
|
+
});
|
|
2324
2400
|
}
|
|
2325
2401
|
}
|
|
2326
|
-
return
|
|
2402
|
+
return diagnostics;
|
|
2327
2403
|
}
|
|
2328
|
-
function
|
|
2329
|
-
var _a, _b
|
|
2330
|
-
const
|
|
2331
|
-
const
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
reason: "no_primary"
|
|
2343
|
-
});
|
|
2404
|
+
function collectAnchors(fields) {
|
|
2405
|
+
var _a, _b;
|
|
2406
|
+
const anchors = [];
|
|
2407
|
+
for (const field of fields) {
|
|
2408
|
+
if (!isButton(field)) continue;
|
|
2409
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
2410
|
+
for (const option of field.options) {
|
|
2411
|
+
anchors.push({
|
|
2412
|
+
kind: "option",
|
|
2413
|
+
id: option.id,
|
|
2414
|
+
fieldId: field.id,
|
|
2415
|
+
label: (_a = option.label) != null ? _a : option.id
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2344
2418
|
continue;
|
|
2345
2419
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2420
|
+
anchors.push({
|
|
2421
|
+
kind: "field",
|
|
2422
|
+
id: field.id,
|
|
2423
|
+
fieldId: field.id,
|
|
2424
|
+
label: (_b = field.label) != null ? _b : field.id
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
return anchors;
|
|
2428
|
+
}
|
|
2429
|
+
function collectFieldReferences(field, services) {
|
|
2430
|
+
var _a;
|
|
2431
|
+
const members = collectBaseMembers(field, services);
|
|
2432
|
+
if (members.length === 0) return [];
|
|
2433
|
+
if (isMultiField(field)) {
|
|
2434
|
+
const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
|
|
2435
|
+
return [
|
|
2436
|
+
{
|
|
2437
|
+
refKind: "multi",
|
|
2438
|
+
nodeId: field.id,
|
|
2439
|
+
fieldId: field.id,
|
|
2440
|
+
label: (_a = field.label) != null ? _a : field.id,
|
|
2441
|
+
rate: averageRate,
|
|
2442
|
+
members
|
|
2443
|
+
}
|
|
2444
|
+
];
|
|
2445
|
+
}
|
|
2446
|
+
return members.map((member) => ({
|
|
2447
|
+
refKind: "single",
|
|
2448
|
+
nodeId: member.id,
|
|
2449
|
+
fieldId: field.id,
|
|
2450
|
+
label: member.label,
|
|
2451
|
+
rate: member.rate,
|
|
2452
|
+
service_id: member.service_id,
|
|
2453
|
+
members: [member]
|
|
2454
|
+
}));
|
|
2455
|
+
}
|
|
2456
|
+
function collectBaseMembers(field, services) {
|
|
2457
|
+
var _a, _b, _c;
|
|
2458
|
+
const members = [];
|
|
2459
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
2460
|
+
for (const option of field.options) {
|
|
2461
|
+
const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
|
|
2462
|
+
if (role2 !== "base") continue;
|
|
2463
|
+
if (option.service_id === void 0 || option.service_id === null) {
|
|
2356
2464
|
continue;
|
|
2357
2465
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
scope: "node",
|
|
2361
|
-
nodeId,
|
|
2362
|
-
primary,
|
|
2363
|
-
candidate: cand,
|
|
2364
|
-
reason: "cycle"
|
|
2365
|
-
});
|
|
2466
|
+
const cap2 = getServiceCapability(services, option.service_id);
|
|
2467
|
+
if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
|
|
2366
2468
|
continue;
|
|
2367
2469
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2470
|
+
members.push({
|
|
2471
|
+
kind: "option",
|
|
2472
|
+
id: option.id,
|
|
2473
|
+
fieldId: field.id,
|
|
2474
|
+
label: (_b = option.label) != null ? _b : option.id,
|
|
2475
|
+
service_id: option.service_id,
|
|
2476
|
+
rate: cap2.rate
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
return members;
|
|
2480
|
+
}
|
|
2481
|
+
const role = normalizeRole(field.pricing_role, "base");
|
|
2482
|
+
if (role !== "base") return members;
|
|
2483
|
+
if (field.service_id === void 0 || field.service_id === null) return members;
|
|
2484
|
+
const cap = getServiceCapability(services, field.service_id);
|
|
2485
|
+
if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
|
|
2486
|
+
return members;
|
|
2487
|
+
}
|
|
2488
|
+
members.push({
|
|
2489
|
+
kind: "field",
|
|
2490
|
+
id: field.id,
|
|
2491
|
+
fieldId: field.id,
|
|
2492
|
+
label: (_c = field.label) != null ? _c : field.id,
|
|
2493
|
+
service_id: field.service_id,
|
|
2494
|
+
rate: cap.rate
|
|
2495
|
+
});
|
|
2496
|
+
return members;
|
|
2497
|
+
}
|
|
2498
|
+
function isButton(field) {
|
|
2499
|
+
if (field.button === true) return true;
|
|
2500
|
+
return Array.isArray(field.options) && field.options.length > 0;
|
|
2501
|
+
}
|
|
2502
|
+
function normalizeRole(role, fallback) {
|
|
2503
|
+
return role === "base" || role === "utility" ? role : fallback;
|
|
2504
|
+
}
|
|
2505
|
+
function toDiagnosticRef(reference) {
|
|
2506
|
+
return {
|
|
2507
|
+
nodeId: reference.nodeId,
|
|
2508
|
+
fieldId: reference.fieldId,
|
|
2509
|
+
label: reference.label,
|
|
2510
|
+
refKind: reference.refKind,
|
|
2511
|
+
service_id: reference.service_id,
|
|
2512
|
+
rate: reference.rate
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function contextualKey(tagId, primary, candidate, ratePolicy) {
|
|
2516
|
+
const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
|
|
2517
|
+
return [
|
|
2518
|
+
"contextual",
|
|
2519
|
+
tagId,
|
|
2520
|
+
primary.fieldId,
|
|
2521
|
+
primary.nodeId,
|
|
2522
|
+
candidate.fieldId,
|
|
2523
|
+
candidate.nodeId,
|
|
2524
|
+
`${ratePolicy.kind}${pctKey}`
|
|
2525
|
+
].join("|");
|
|
2526
|
+
}
|
|
2527
|
+
function describeLabel(tag) {
|
|
2528
|
+
var _a, _b;
|
|
2529
|
+
return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
|
|
2530
|
+
}
|
|
2531
|
+
function explainRateMismatch(policy, primary, candidate, where) {
|
|
2532
|
+
var _a, _b;
|
|
2533
|
+
const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
|
|
2534
|
+
const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
|
|
2535
|
+
switch (policy.kind) {
|
|
2536
|
+
case "eq_primary":
|
|
2537
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
|
|
2538
|
+
case "lte_primary":
|
|
2539
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
|
|
2540
|
+
case "within_pct":
|
|
2541
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
|
|
2542
|
+
case "at_least_pct_lower":
|
|
2543
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/core/validate/index.ts
|
|
2548
|
+
function readVisibilitySimOpts(ctx) {
|
|
2549
|
+
const c = ctx;
|
|
2550
|
+
const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
|
|
2551
|
+
const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
|
|
2552
|
+
const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
|
|
2553
|
+
const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
|
|
2554
|
+
const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
|
|
2555
|
+
return {
|
|
2556
|
+
simulate,
|
|
2557
|
+
maxStates,
|
|
2558
|
+
maxDepth,
|
|
2559
|
+
simulateAllRoots,
|
|
2560
|
+
onlyEffectfulTriggers
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
function validate(props, ctx = {}) {
|
|
2564
|
+
var _a, _b, _c, _d;
|
|
2565
|
+
const errors = [];
|
|
2566
|
+
const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
|
|
2567
|
+
const selectedKeys = new Set(
|
|
2568
|
+
(_b = ctx.selectedOptionKeys) != null ? _b : []
|
|
2569
|
+
);
|
|
2570
|
+
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
2571
|
+
const fields = Array.isArray(props.fields) ? props.fields : [];
|
|
2572
|
+
const tagById = /* @__PURE__ */ new Map();
|
|
2573
|
+
const fieldById = /* @__PURE__ */ new Map();
|
|
2574
|
+
for (const t of tags) tagById.set(t.id, t);
|
|
2575
|
+
for (const f of fields) fieldById.set(f.id, f);
|
|
2576
|
+
const v = {
|
|
2577
|
+
props,
|
|
2578
|
+
nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
|
|
2579
|
+
options: ctx,
|
|
2580
|
+
errors,
|
|
2581
|
+
serviceMap,
|
|
2582
|
+
selectedKeys,
|
|
2583
|
+
tags,
|
|
2584
|
+
fields,
|
|
2585
|
+
invalidRateFieldIds: /* @__PURE__ */ new Set(),
|
|
2586
|
+
tagById,
|
|
2587
|
+
fieldById,
|
|
2588
|
+
fieldsVisibleUnder: (_tagId) => []
|
|
2589
|
+
};
|
|
2590
|
+
validateStructure(v);
|
|
2591
|
+
validateIdentity(v);
|
|
2592
|
+
validateOptionMaps(v);
|
|
2593
|
+
v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
|
|
2594
|
+
const visSim = readVisibilitySimOpts(ctx);
|
|
2595
|
+
validateVisibility(v, visSim);
|
|
2596
|
+
applyPolicies(
|
|
2597
|
+
v.errors,
|
|
2598
|
+
v.props,
|
|
2599
|
+
v.serviceMap,
|
|
2600
|
+
v.options.policies,
|
|
2601
|
+
v.fieldsVisibleUnder,
|
|
2602
|
+
v.tags
|
|
2603
|
+
);
|
|
2604
|
+
validateServiceVsUserInput(v);
|
|
2605
|
+
validateUtilityMarkers(v);
|
|
2606
|
+
validateRates(v);
|
|
2607
|
+
if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
|
|
2608
|
+
const builder = createBuilder({ serviceMap });
|
|
2609
|
+
builder.load(props);
|
|
2610
|
+
for (const tag of tags) {
|
|
2611
|
+
const diags = validateRateCoherenceDeep({
|
|
2612
|
+
builder,
|
|
2613
|
+
services: serviceMap,
|
|
2614
|
+
tagId: tag.id,
|
|
2615
|
+
ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
|
|
2616
|
+
invalidFieldIds: v.invalidRateFieldIds
|
|
2617
|
+
});
|
|
2618
|
+
for (const diag of diags) {
|
|
2619
|
+
if (diag.kind !== "contextual") continue;
|
|
2620
|
+
errors.push({
|
|
2621
|
+
code: "rate_coherence_violation",
|
|
2622
|
+
severity: "error",
|
|
2623
|
+
message: diag.message,
|
|
2624
|
+
nodeId: diag.nodeId,
|
|
2625
|
+
details: {
|
|
2626
|
+
tagId: diag.tagId,
|
|
2627
|
+
simulationAnchor: diag.simulationAnchor,
|
|
2628
|
+
primary: diag.primary,
|
|
2629
|
+
offender: diag.offender,
|
|
2630
|
+
policy: diag.policy,
|
|
2631
|
+
policyPct: diag.policyPct,
|
|
2632
|
+
invalidFieldIds: diag.invalidFieldIds
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
validateConstraints(v);
|
|
2639
|
+
validateCustomFields(v);
|
|
2640
|
+
validateGlobalUtilityGuard(v);
|
|
2641
|
+
validateUnboundFields(v);
|
|
2642
|
+
validateFallbacks(v);
|
|
2643
|
+
return v.errors;
|
|
2644
|
+
}
|
|
2645
|
+
async function validateAsync(props, ctx = {}) {
|
|
2646
|
+
await Promise.resolve();
|
|
2647
|
+
if (typeof requestAnimationFrame === "function") {
|
|
2648
|
+
await new Promise(
|
|
2649
|
+
(resolve) => requestAnimationFrame(() => resolve())
|
|
2650
|
+
);
|
|
2651
|
+
} else {
|
|
2652
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2653
|
+
}
|
|
2654
|
+
return validate(props, ctx);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/core/fallback.ts
|
|
2658
|
+
var DEFAULT_SETTINGS = {
|
|
2659
|
+
requireConstraintFit: true,
|
|
2660
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
2661
|
+
selectionStrategy: "priority",
|
|
2662
|
+
mode: "strict"
|
|
2663
|
+
};
|
|
2664
|
+
function resolveServiceFallback(params) {
|
|
2665
|
+
var _a, _b, _c, _d, _e;
|
|
2666
|
+
const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
|
|
2667
|
+
const { primary, nodeId, tagId, services } = params;
|
|
2668
|
+
const fb = (_b = params.fallbacks) != null ? _b : {};
|
|
2669
|
+
const tried = [];
|
|
2670
|
+
const lists = [];
|
|
2671
|
+
if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
|
|
2672
|
+
if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
|
|
2673
|
+
const primaryRate = rateOf(services, primary);
|
|
2674
|
+
for (const list of lists) {
|
|
2675
|
+
for (const cand of list) {
|
|
2676
|
+
if (tried.includes(cand)) continue;
|
|
2677
|
+
tried.push(cand);
|
|
2678
|
+
const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
|
|
2679
|
+
if (!candCap) continue;
|
|
2680
|
+
if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
|
|
2681
|
+
if (s.requireConstraintFit && tagId) {
|
|
2682
|
+
const ok = satisfiesTagConstraints(tagId, params, candCap);
|
|
2683
|
+
if (!ok) continue;
|
|
2684
|
+
}
|
|
2685
|
+
return cand;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
return null;
|
|
2689
|
+
}
|
|
2690
|
+
function collectFailedFallbacks(props, services, settings) {
|
|
2691
|
+
var _a, _b, _c;
|
|
2692
|
+
const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
|
|
2693
|
+
const out = [];
|
|
2694
|
+
const fb = (_a = props.fallbacks) != null ? _a : {};
|
|
2695
|
+
const primaryRate = (p) => rateOf(services, p);
|
|
2696
|
+
for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
|
|
2697
|
+
const { primary, tagContexts } = primaryForNode(props, nodeId);
|
|
2698
|
+
if (!primary) {
|
|
2699
|
+
out.push({
|
|
2700
|
+
scope: "node",
|
|
2701
|
+
nodeId,
|
|
2702
|
+
primary: "",
|
|
2703
|
+
candidate: "",
|
|
2704
|
+
reason: "no_primary"
|
|
2705
|
+
});
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
for (const cand of list) {
|
|
2709
|
+
const cap = getCap(services, cand);
|
|
2710
|
+
if (!cap) {
|
|
2711
|
+
out.push({
|
|
2712
|
+
scope: "node",
|
|
2713
|
+
nodeId,
|
|
2714
|
+
primary,
|
|
2715
|
+
candidate: cand,
|
|
2716
|
+
reason: "unknown_service"
|
|
2717
|
+
});
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
if (String(cand) === String(primary)) {
|
|
2721
|
+
out.push({
|
|
2722
|
+
scope: "node",
|
|
2723
|
+
nodeId,
|
|
2724
|
+
primary,
|
|
2725
|
+
candidate: cand,
|
|
2726
|
+
reason: "cycle"
|
|
2727
|
+
});
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
|
|
2731
|
+
out.push({
|
|
2370
2732
|
scope: "node",
|
|
2371
2733
|
nodeId,
|
|
2372
2734
|
primary,
|
|
@@ -2450,27 +2812,10 @@ function passesRate(policy, primaryRate, candRate) {
|
|
|
2450
2812
|
return false;
|
|
2451
2813
|
if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
|
|
2452
2814
|
return false;
|
|
2453
|
-
|
|
2454
|
-
case "lte_primary":
|
|
2455
|
-
return candRate <= primaryRate;
|
|
2456
|
-
case "within_pct":
|
|
2457
|
-
return candRate <= primaryRate * (1 + policy.pct / 100);
|
|
2458
|
-
case "at_least_pct_lower":
|
|
2459
|
-
return candRate <= primaryRate * (1 - policy.pct / 100);
|
|
2460
|
-
}
|
|
2815
|
+
return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
|
|
2461
2816
|
}
|
|
2462
2817
|
function getCap(map, id) {
|
|
2463
|
-
|
|
2464
|
-
if (direct) return direct;
|
|
2465
|
-
const strKey = String(id);
|
|
2466
|
-
const byStr = map[strKey];
|
|
2467
|
-
if (byStr) return byStr;
|
|
2468
|
-
const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
|
|
2469
|
-
if (Number.isFinite(n)) {
|
|
2470
|
-
const byNum = map[n];
|
|
2471
|
-
if (byNum) return byNum;
|
|
2472
|
-
}
|
|
2473
|
-
return void 0;
|
|
2818
|
+
return getServiceCapability(map, id);
|
|
2474
2819
|
}
|
|
2475
2820
|
function isCapFlagEnabled(cap, flagId) {
|
|
2476
2821
|
var _a, _b;
|
|
@@ -2561,186 +2906,6 @@ function getFallbackRegistrationInfo(props, nodeId) {
|
|
|
2561
2906
|
return { primary, tagContexts };
|
|
2562
2907
|
}
|
|
2563
2908
|
|
|
2564
|
-
// src/core/rate-coherence.ts
|
|
2565
|
-
function validateRateCoherenceDeep(params) {
|
|
2566
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
2567
|
-
const { builder, services, tagId } = params;
|
|
2568
|
-
const ratePolicy = (_a = params.ratePolicy) != null ? _a : { kind: "lte_primary" };
|
|
2569
|
-
const props = builder.getProps();
|
|
2570
|
-
const fields = (_b = props.fields) != null ? _b : [];
|
|
2571
|
-
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2572
|
-
const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
|
|
2573
|
-
const tag = tagById.get(tagId);
|
|
2574
|
-
const baselineFieldIds = builder.visibleFields(tagId, []);
|
|
2575
|
-
const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2576
|
-
const anchors = [];
|
|
2577
|
-
for (const f of baselineFields) {
|
|
2578
|
-
if (!isButton(f)) continue;
|
|
2579
|
-
if (Array.isArray(f.options) && f.options.length) {
|
|
2580
|
-
for (const o of f.options) {
|
|
2581
|
-
anchors.push({
|
|
2582
|
-
kind: "option",
|
|
2583
|
-
id: o.id,
|
|
2584
|
-
fieldId: f.id,
|
|
2585
|
-
label: (_d = o.label) != null ? _d : o.id,
|
|
2586
|
-
service_id: numberOrUndefined(o.service_id)
|
|
2587
|
-
});
|
|
2588
|
-
}
|
|
2589
|
-
} else {
|
|
2590
|
-
anchors.push({
|
|
2591
|
-
kind: "field",
|
|
2592
|
-
id: f.id,
|
|
2593
|
-
fieldId: f.id,
|
|
2594
|
-
label: (_e = f.label) != null ? _e : f.id,
|
|
2595
|
-
service_id: numberOrUndefined(f.service_id)
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
const diags = [];
|
|
2600
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2601
|
-
for (const anchor of anchors) {
|
|
2602
|
-
const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
|
|
2603
|
-
const vgFieldIds = builder.visibleFields(tagId, selectedKeys);
|
|
2604
|
-
const vgFields = vgFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2605
|
-
const baseCandidates = [];
|
|
2606
|
-
for (const f of vgFields) {
|
|
2607
|
-
if (!isButton(f)) continue;
|
|
2608
|
-
if (Array.isArray(f.options) && f.options.length) {
|
|
2609
|
-
for (const o of f.options) {
|
|
2610
|
-
const sid = numberOrUndefined(o.service_id);
|
|
2611
|
-
const role = normalizeRole(o.pricing_role, "base");
|
|
2612
|
-
if (sid == null || role !== "base") continue;
|
|
2613
|
-
const r = rateOf2(services, sid);
|
|
2614
|
-
if (!isFiniteNumber2(r)) continue;
|
|
2615
|
-
baseCandidates.push({
|
|
2616
|
-
kind: "option",
|
|
2617
|
-
id: o.id,
|
|
2618
|
-
label: (_f = o.label) != null ? _f : o.id,
|
|
2619
|
-
service_id: sid,
|
|
2620
|
-
rate: r
|
|
2621
|
-
});
|
|
2622
|
-
}
|
|
2623
|
-
} else {
|
|
2624
|
-
const sid = numberOrUndefined(f.service_id);
|
|
2625
|
-
const role = normalizeRole(f.pricing_role, "base");
|
|
2626
|
-
if (sid == null || role !== "base") continue;
|
|
2627
|
-
const r = rateOf2(services, sid);
|
|
2628
|
-
if (!isFiniteNumber2(r)) continue;
|
|
2629
|
-
baseCandidates.push({
|
|
2630
|
-
kind: "field",
|
|
2631
|
-
id: f.id,
|
|
2632
|
-
label: (_g = f.label) != null ? _g : f.id,
|
|
2633
|
-
service_id: sid,
|
|
2634
|
-
rate: r
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
if (baseCandidates.length === 0) continue;
|
|
2639
|
-
const anchorPrimary = anchor.service_id != null ? pickByServiceId(baseCandidates, anchor.service_id) : void 0;
|
|
2640
|
-
const primary = anchorPrimary ? anchorPrimary : baseCandidates[0];
|
|
2641
|
-
for (const cand of baseCandidates) {
|
|
2642
|
-
if (sameService(primary, cand)) continue;
|
|
2643
|
-
if (!rateOkWithPolicy(ratePolicy, cand.rate, primary.rate)) {
|
|
2644
|
-
const key = dedupeKey(tagId, anchor, primary, cand, ratePolicy);
|
|
2645
|
-
if (seen.has(key)) continue;
|
|
2646
|
-
seen.add(key);
|
|
2647
|
-
diags.push({
|
|
2648
|
-
scope: "visible_group",
|
|
2649
|
-
tagId,
|
|
2650
|
-
primary,
|
|
2651
|
-
offender: {
|
|
2652
|
-
kind: cand.kind,
|
|
2653
|
-
id: cand.id,
|
|
2654
|
-
label: cand.label,
|
|
2655
|
-
service_id: cand.service_id,
|
|
2656
|
-
rate: cand.rate
|
|
2657
|
-
},
|
|
2658
|
-
policy: ratePolicy.kind,
|
|
2659
|
-
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
2660
|
-
message: explainRateMismatch(
|
|
2661
|
-
ratePolicy,
|
|
2662
|
-
primary.rate,
|
|
2663
|
-
cand.rate,
|
|
2664
|
-
describeLabel(tag)
|
|
2665
|
-
),
|
|
2666
|
-
simulationAnchor: {
|
|
2667
|
-
kind: anchor.kind,
|
|
2668
|
-
id: anchor.id,
|
|
2669
|
-
fieldId: anchor.fieldId,
|
|
2670
|
-
label: anchor.label
|
|
2671
|
-
}
|
|
2672
|
-
});
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
return diags;
|
|
2677
|
-
}
|
|
2678
|
-
function isButton(f) {
|
|
2679
|
-
if (f.button === true) return true;
|
|
2680
|
-
return Array.isArray(f.options) && f.options.length > 0;
|
|
2681
|
-
}
|
|
2682
|
-
function normalizeRole(role, d) {
|
|
2683
|
-
return role === "utility" || role === "base" ? role : d;
|
|
2684
|
-
}
|
|
2685
|
-
function numberOrUndefined(v) {
|
|
2686
|
-
const n = Number(v);
|
|
2687
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2688
|
-
}
|
|
2689
|
-
function isFiniteNumber2(v) {
|
|
2690
|
-
return typeof v === "number" && Number.isFinite(v);
|
|
2691
|
-
}
|
|
2692
|
-
function rateOf2(map, id) {
|
|
2693
|
-
var _a;
|
|
2694
|
-
if (id === void 0 || id === null) return void 0;
|
|
2695
|
-
const cap = (_a = map[Number(id)]) != null ? _a : map[id];
|
|
2696
|
-
return cap == null ? void 0 : cap.rate;
|
|
2697
|
-
}
|
|
2698
|
-
function pickByServiceId(arr, sid) {
|
|
2699
|
-
return arr.find((x) => x.service_id === sid);
|
|
2700
|
-
}
|
|
2701
|
-
function sameService(a, b) {
|
|
2702
|
-
return a.service_id === b.service_id;
|
|
2703
|
-
}
|
|
2704
|
-
function rateOkWithPolicy(policy, candRate, primaryRate) {
|
|
2705
|
-
var _a, _b;
|
|
2706
|
-
const rp = policy != null ? policy : { kind: "lte_primary" };
|
|
2707
|
-
switch (rp.kind) {
|
|
2708
|
-
case "lte_primary":
|
|
2709
|
-
return candRate <= primaryRate;
|
|
2710
|
-
case "within_pct": {
|
|
2711
|
-
const pct = Math.max(0, (_a = rp.pct) != null ? _a : 0);
|
|
2712
|
-
return candRate <= primaryRate * (1 + pct / 100);
|
|
2713
|
-
}
|
|
2714
|
-
case "at_least_pct_lower": {
|
|
2715
|
-
const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
|
|
2716
|
-
return candRate <= primaryRate * (1 - pct / 100);
|
|
2717
|
-
}
|
|
2718
|
-
default:
|
|
2719
|
-
return candRate <= primaryRate;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
function describeLabel(tag) {
|
|
2723
|
-
var _a, _b;
|
|
2724
|
-
const tagName = (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
|
|
2725
|
-
return `${tagName}`;
|
|
2726
|
-
}
|
|
2727
|
-
function explainRateMismatch(policy, primary, candidate, where) {
|
|
2728
|
-
switch (policy.kind) {
|
|
2729
|
-
case "lte_primary":
|
|
2730
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be \u2264 primary ${primary}.`;
|
|
2731
|
-
case "within_pct":
|
|
2732
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be within ${policy.pct}% of primary ${primary}.`;
|
|
2733
|
-
case "at_least_pct_lower":
|
|
2734
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be at least ${policy.pct}% lower than primary ${primary}.`;
|
|
2735
|
-
default:
|
|
2736
|
-
return `Rate coherence failed (${where}): candidate ${candidate} mismatches primary ${primary}.`;
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
function dedupeKey(tagId, anchor, primary, cand, rp) {
|
|
2740
|
-
const rpKey = rp.kind + ("pct" in rp && typeof rp.pct === "number" ? `:${rp.pct}` : "");
|
|
2741
|
-
return `${tagId}|${anchor.kind}:${anchor.id}|p${primary.service_id}|c${cand.service_id}:${cand.id}|${rpKey}`;
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
2909
|
// src/core/tag-relations.ts
|
|
2745
2910
|
var toId = (x) => typeof x === "string" ? x : x.id;
|
|
2746
2911
|
var toBindList = (b) => {
|
|
@@ -3073,17 +3238,16 @@ function createNodeIndex(builder) {
|
|
|
3073
3238
|
return node;
|
|
3074
3239
|
};
|
|
3075
3240
|
const getNode = (input) => {
|
|
3076
|
-
var _a2, _b2, _c2
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
|
|
3080
|
-
if ("type" in input)
|
|
3081
|
-
return (_b2 = getField(input.id)) != null ? _b2 : mkUnknown(input.id);
|
|
3082
|
-
return (_c2 = getOption(input.id)) != null ? _c2 : mkUnknown(input.id);
|
|
3083
|
-
}
|
|
3084
|
-
const cached = nodeCache.get(input);
|
|
3241
|
+
var _a2, _b2, _c2;
|
|
3242
|
+
const id = typeof input === "object" ? input.id : input;
|
|
3243
|
+
const cached = nodeCache.get(id);
|
|
3085
3244
|
if (cached) return cached;
|
|
3086
|
-
|
|
3245
|
+
const node = nodeMap.get(id);
|
|
3246
|
+
if (!node) return mkUnknown(id);
|
|
3247
|
+
if (node.kind === "tag") return (_a2 = getTag(id)) != null ? _a2 : mkUnknown(id);
|
|
3248
|
+
if (node.kind == "field") return (_b2 = getField(id)) != null ? _b2 : mkUnknown(id);
|
|
3249
|
+
if (node.kind == "option") return (_c2 = getOption(id)) != null ? _c2 : mkUnknown(id);
|
|
3250
|
+
return mkUnknown(id);
|
|
3087
3251
|
};
|
|
3088
3252
|
const mkUnknown = (id) => {
|
|
3089
3253
|
const u = {
|
|
@@ -3104,6 +3268,451 @@ function createNodeIndex(builder) {
|
|
|
3104
3268
|
};
|
|
3105
3269
|
}
|
|
3106
3270
|
|
|
3271
|
+
// src/core/policy.ts
|
|
3272
|
+
var ALLOWED_SCOPES = /* @__PURE__ */ new Set([
|
|
3273
|
+
"global",
|
|
3274
|
+
"visible_group"
|
|
3275
|
+
]);
|
|
3276
|
+
var ALLOWED_SUBJECTS = /* @__PURE__ */ new Set(["services"]);
|
|
3277
|
+
var ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
3278
|
+
"all_equal",
|
|
3279
|
+
"unique",
|
|
3280
|
+
"no_mix",
|
|
3281
|
+
"all_true",
|
|
3282
|
+
"any_true",
|
|
3283
|
+
"max_count",
|
|
3284
|
+
"min_count"
|
|
3285
|
+
]);
|
|
3286
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
3287
|
+
"base",
|
|
3288
|
+
"utility",
|
|
3289
|
+
"both"
|
|
3290
|
+
]);
|
|
3291
|
+
var ALLOWED_SEVERITIES = /* @__PURE__ */ new Set([
|
|
3292
|
+
"error",
|
|
3293
|
+
"warning"
|
|
3294
|
+
]);
|
|
3295
|
+
var ALLOWED_WHERE_OPS = /* @__PURE__ */ new Set(["eq", "neq", "in", "nin", "exists", "truthy", "falsy"]);
|
|
3296
|
+
function normaliseWhere(src, d, i, id) {
|
|
3297
|
+
if (src === void 0) return void 0;
|
|
3298
|
+
if (!Array.isArray(src)) {
|
|
3299
|
+
d.push({
|
|
3300
|
+
ruleIndex: i,
|
|
3301
|
+
ruleId: id,
|
|
3302
|
+
severity: "warning",
|
|
3303
|
+
message: "filter.where must be an array; ignored.",
|
|
3304
|
+
path: "filter.where"
|
|
3305
|
+
});
|
|
3306
|
+
return void 0;
|
|
3307
|
+
}
|
|
3308
|
+
const out = [];
|
|
3309
|
+
src.forEach((raw, j) => {
|
|
3310
|
+
const obj = raw && typeof raw === "object" ? raw : null;
|
|
3311
|
+
const path = typeof (obj == null ? void 0 : obj.path) === "string" && obj.path.trim() ? obj.path.trim() : void 0;
|
|
3312
|
+
if (!path) {
|
|
3313
|
+
d.push({
|
|
3314
|
+
ruleIndex: i,
|
|
3315
|
+
ruleId: id,
|
|
3316
|
+
severity: "warning",
|
|
3317
|
+
message: `filter.where[${j}].path must be a non-empty string; entry ignored.`,
|
|
3318
|
+
path: `filter.where[${j}].path`
|
|
3319
|
+
});
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
if (!path.startsWith("service.")) {
|
|
3323
|
+
d.push({
|
|
3324
|
+
ruleIndex: i,
|
|
3325
|
+
ruleId: id,
|
|
3326
|
+
severity: "warning",
|
|
3327
|
+
message: `filter.where[${j}].path should start with "service." for subject "services".`,
|
|
3328
|
+
path: `filter.where[${j}].path`
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
const opRaw = obj == null ? void 0 : obj.op;
|
|
3332
|
+
const op = opRaw === void 0 ? "eq" : typeof opRaw === "string" && ALLOWED_WHERE_OPS.has(opRaw) ? opRaw : "eq";
|
|
3333
|
+
if (opRaw !== void 0 && !(typeof opRaw === "string" && ALLOWED_WHERE_OPS.has(opRaw))) {
|
|
3334
|
+
d.push({
|
|
3335
|
+
ruleIndex: i,
|
|
3336
|
+
ruleId: id,
|
|
3337
|
+
severity: "warning",
|
|
3338
|
+
message: `Unknown filter.where[${j}].op; defaulted to "eq".`,
|
|
3339
|
+
path: `filter.where[${j}].op`
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
const value = obj == null ? void 0 : obj.value;
|
|
3343
|
+
if (op === "exists" || op === "truthy" || op === "falsy") {
|
|
3344
|
+
if (value !== void 0) {
|
|
3345
|
+
d.push({
|
|
3346
|
+
ruleIndex: i,
|
|
3347
|
+
ruleId: id,
|
|
3348
|
+
severity: "warning",
|
|
3349
|
+
message: `filter.where[${j}] op "${op}" does not use "value".`,
|
|
3350
|
+
path: `filter.where[${j}].value`
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
} else if (op === "in" || op === "nin") {
|
|
3354
|
+
if (!Array.isArray(value)) {
|
|
3355
|
+
d.push({
|
|
3356
|
+
ruleIndex: i,
|
|
3357
|
+
ruleId: id,
|
|
3358
|
+
severity: "warning",
|
|
3359
|
+
message: `filter.where[${j}] op "${op}" expects an array "value".`,
|
|
3360
|
+
path: `filter.where[${j}].value`
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
out.push({ path, op, value });
|
|
3365
|
+
});
|
|
3366
|
+
return out.length ? out : void 0;
|
|
3367
|
+
}
|
|
3368
|
+
function compilePolicies(raw) {
|
|
3369
|
+
const diagnostics = [];
|
|
3370
|
+
const policies = [];
|
|
3371
|
+
if (!Array.isArray(raw)) {
|
|
3372
|
+
diagnostics.push({
|
|
3373
|
+
ruleIndex: -1,
|
|
3374
|
+
severity: "error",
|
|
3375
|
+
message: "Policies root must be an array."
|
|
3376
|
+
});
|
|
3377
|
+
return { policies, diagnostics };
|
|
3378
|
+
}
|
|
3379
|
+
raw.forEach((entry, i) => {
|
|
3380
|
+
const d = [];
|
|
3381
|
+
const src = entry && typeof entry === "object" ? entry : {};
|
|
3382
|
+
let id = typeof src.id === "string" && src.id.trim() ? src.id.trim() : void 0;
|
|
3383
|
+
if (!id) {
|
|
3384
|
+
id = `policy_${i + 1}`;
|
|
3385
|
+
d.push({
|
|
3386
|
+
ruleIndex: i,
|
|
3387
|
+
ruleId: id,
|
|
3388
|
+
severity: "warning",
|
|
3389
|
+
message: 'Missing "id"; generated automatically.',
|
|
3390
|
+
path: "id"
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const label = typeof src.label === "string" && src.label.trim() ? src.label.trim() : id;
|
|
3394
|
+
if (!(typeof src.label === "string" && src.label.trim())) {
|
|
3395
|
+
d.push({
|
|
3396
|
+
ruleIndex: i,
|
|
3397
|
+
ruleId: id,
|
|
3398
|
+
severity: "warning",
|
|
3399
|
+
message: 'Missing "label"; defaulted to rule id.',
|
|
3400
|
+
path: "label"
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
let scope = ALLOWED_SCOPES.has(src.scope) ? src.scope : src.scope === void 0 ? "visible_group" : "visible_group";
|
|
3404
|
+
if (src.scope !== void 0 && !ALLOWED_SCOPES.has(src.scope)) {
|
|
3405
|
+
d.push({
|
|
3406
|
+
ruleIndex: i,
|
|
3407
|
+
ruleId: id,
|
|
3408
|
+
severity: "warning",
|
|
3409
|
+
message: 'Unknown "scope"; defaulted to "visible_group".',
|
|
3410
|
+
path: "scope"
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
let subject = ALLOWED_SUBJECTS.has(src.subject) ? src.subject : "services";
|
|
3414
|
+
if (src.subject !== void 0 && !ALLOWED_SUBJECTS.has(src.subject)) {
|
|
3415
|
+
d.push({
|
|
3416
|
+
ruleIndex: i,
|
|
3417
|
+
ruleId: id,
|
|
3418
|
+
severity: "warning",
|
|
3419
|
+
message: 'Unknown "subject"; defaulted to "services".',
|
|
3420
|
+
path: "subject"
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
const op = src.op;
|
|
3424
|
+
if (!ALLOWED_OPS.has(op)) {
|
|
3425
|
+
d.push({
|
|
3426
|
+
ruleIndex: i,
|
|
3427
|
+
ruleId: id,
|
|
3428
|
+
severity: "error",
|
|
3429
|
+
message: `Invalid "op": ${String(op)}.`,
|
|
3430
|
+
path: "op"
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
let projection = typeof src.projection === "string" && src.projection.trim() ? src.projection.trim() : "service.id";
|
|
3434
|
+
if (subject === "services" && projection && !projection.startsWith("service.")) {
|
|
3435
|
+
d.push({
|
|
3436
|
+
ruleIndex: i,
|
|
3437
|
+
ruleId: id,
|
|
3438
|
+
severity: "warning",
|
|
3439
|
+
message: 'Projection should start with "service." for subject "services".',
|
|
3440
|
+
path: "projection"
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
const filterSrc = src.filter && typeof src.filter === "object" ? src.filter : void 0;
|
|
3444
|
+
const role = (filterSrc == null ? void 0 : filterSrc.role) && ALLOWED_ROLES.has(filterSrc.role) ? filterSrc.role : "both";
|
|
3445
|
+
if ((filterSrc == null ? void 0 : filterSrc.role) && !ALLOWED_ROLES.has(filterSrc.role)) {
|
|
3446
|
+
d.push({
|
|
3447
|
+
ruleIndex: i,
|
|
3448
|
+
ruleId: id,
|
|
3449
|
+
severity: "warning",
|
|
3450
|
+
message: 'Unknown filter.role; defaulted to "both".',
|
|
3451
|
+
path: "filter.role"
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
const filter = {
|
|
3455
|
+
role,
|
|
3456
|
+
tag_id: (filterSrc == null ? void 0 : filterSrc.tag_id) !== void 0 ? Array.isArray(filterSrc.tag_id) ? filterSrc.tag_id : [filterSrc.tag_id] : void 0,
|
|
3457
|
+
field_id: (filterSrc == null ? void 0 : filterSrc.field_id) !== void 0 ? Array.isArray(filterSrc.field_id) ? filterSrc.field_id : [filterSrc.field_id] : void 0,
|
|
3458
|
+
where: normaliseWhere(filterSrc == null ? void 0 : filterSrc.where, d, i, id)
|
|
3459
|
+
};
|
|
3460
|
+
const severity = ALLOWED_SEVERITIES.has(src.severity) ? src.severity : "error";
|
|
3461
|
+
if (src.severity !== void 0 && !ALLOWED_SEVERITIES.has(src.severity)) {
|
|
3462
|
+
d.push({
|
|
3463
|
+
ruleIndex: i,
|
|
3464
|
+
ruleId: id,
|
|
3465
|
+
severity: "warning",
|
|
3466
|
+
message: 'Unknown "severity"; defaulted to "error".',
|
|
3467
|
+
path: "severity"
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
const value = src.value;
|
|
3471
|
+
if (op === "max_count" || op === "min_count") {
|
|
3472
|
+
if (!(typeof value === "number" && Number.isFinite(value))) {
|
|
3473
|
+
d.push({
|
|
3474
|
+
ruleIndex: i,
|
|
3475
|
+
ruleId: id,
|
|
3476
|
+
severity: "error",
|
|
3477
|
+
message: `"${op}" requires numeric "value".`,
|
|
3478
|
+
path: "value"
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
} else if (op === "all_true" || op === "any_true") {
|
|
3482
|
+
if (value !== void 0) {
|
|
3483
|
+
d.push({
|
|
3484
|
+
ruleIndex: i,
|
|
3485
|
+
ruleId: id,
|
|
3486
|
+
severity: "warning",
|
|
3487
|
+
message: `"${op}" ignores "value"; it checks all/any true.`,
|
|
3488
|
+
path: "value"
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
} else {
|
|
3492
|
+
if (value !== void 0) {
|
|
3493
|
+
d.push({
|
|
3494
|
+
ruleIndex: i,
|
|
3495
|
+
ruleId: id,
|
|
3496
|
+
severity: "warning",
|
|
3497
|
+
message: `"${op}" does not use "value".`,
|
|
3498
|
+
path: "value"
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
const hasFatal = d.some((x) => x.severity === "error");
|
|
3503
|
+
if (!hasFatal) {
|
|
3504
|
+
const rule = {
|
|
3505
|
+
id,
|
|
3506
|
+
label,
|
|
3507
|
+
// ✅ now always present
|
|
3508
|
+
scope,
|
|
3509
|
+
subject,
|
|
3510
|
+
filter,
|
|
3511
|
+
projection,
|
|
3512
|
+
op,
|
|
3513
|
+
value,
|
|
3514
|
+
severity,
|
|
3515
|
+
message: typeof src.message === "string" ? src.message : void 0
|
|
3516
|
+
};
|
|
3517
|
+
policies.push(rule);
|
|
3518
|
+
}
|
|
3519
|
+
diagnostics.push(...d);
|
|
3520
|
+
});
|
|
3521
|
+
return { policies, diagnostics };
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
// src/core/service-filter.ts
|
|
3525
|
+
function filterServicesForVisibleGroup(input, deps) {
|
|
3526
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3527
|
+
const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
|
|
3528
|
+
const { context } = input;
|
|
3529
|
+
const usedSet = new Set(context.usedServiceIds.map(String));
|
|
3530
|
+
const primary = context.usedServiceIds[0];
|
|
3531
|
+
const fb = {
|
|
3532
|
+
requireConstraintFit: true,
|
|
3533
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
3534
|
+
selectionStrategy: "priority",
|
|
3535
|
+
mode: "strict",
|
|
3536
|
+
...(_d = context.fallback) != null ? _d : {}
|
|
3537
|
+
};
|
|
3538
|
+
const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
|
|
3539
|
+
deps.builder,
|
|
3540
|
+
context.tagId,
|
|
3541
|
+
context.selectedButtons
|
|
3542
|
+
);
|
|
3543
|
+
const checks = [];
|
|
3544
|
+
let lastDiagnostics = void 0;
|
|
3545
|
+
for (const id of input.candidates) {
|
|
3546
|
+
if (usedSet.has(String(id))) continue;
|
|
3547
|
+
const cap = getServiceCapability(svcMap, id);
|
|
3548
|
+
if (!cap) {
|
|
3549
|
+
checks.push({
|
|
3550
|
+
id,
|
|
3551
|
+
ok: false,
|
|
3552
|
+
fitsConstraints: false,
|
|
3553
|
+
passesRate: false,
|
|
3554
|
+
passesPolicies: false,
|
|
3555
|
+
reasons: ["missing_capability"]
|
|
3556
|
+
});
|
|
3557
|
+
continue;
|
|
3558
|
+
}
|
|
3559
|
+
const fitsConstraints = constraintFitOk(
|
|
3560
|
+
svcMap,
|
|
3561
|
+
cap.id,
|
|
3562
|
+
(_e = context.effectiveConstraints) != null ? _e : {}
|
|
3563
|
+
);
|
|
3564
|
+
const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
|
|
3565
|
+
const polRes = evaluatePoliciesRaw(
|
|
3566
|
+
(_f = context.policies) != null ? _f : [],
|
|
3567
|
+
[...context.usedServiceIds, id],
|
|
3568
|
+
svcMap,
|
|
3569
|
+
context.tagId,
|
|
3570
|
+
visibleServiceIds
|
|
3571
|
+
);
|
|
3572
|
+
const passesPolicies = polRes.ok;
|
|
3573
|
+
lastDiagnostics = polRes.diagnostics;
|
|
3574
|
+
const reasons = [];
|
|
3575
|
+
if (!fitsConstraints) reasons.push("constraint_mismatch");
|
|
3576
|
+
if (!passesRate2) reasons.push("rate_policy");
|
|
3577
|
+
if (!passesPolicies) reasons.push("policy_error");
|
|
3578
|
+
checks.push({
|
|
3579
|
+
id,
|
|
3580
|
+
ok: fitsConstraints && passesRate2 && passesPolicies,
|
|
3581
|
+
fitsConstraints,
|
|
3582
|
+
passesRate: passesRate2,
|
|
3583
|
+
passesPolicies,
|
|
3584
|
+
policyErrors: polRes.errors.length ? polRes.errors : void 0,
|
|
3585
|
+
policyWarnings: polRes.warnings.length ? polRes.warnings : void 0,
|
|
3586
|
+
reasons,
|
|
3587
|
+
cap,
|
|
3588
|
+
rate: toFiniteNumber(cap.rate)
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
return {
|
|
3592
|
+
checks,
|
|
3593
|
+
diagnostics: lastDiagnostics && lastDiagnostics.length ? lastDiagnostics : void 0
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
function evaluatePoliciesRaw(raw, serviceIds, svcMap, tagId, visibleServiceIds) {
|
|
3597
|
+
const compiled = compilePolicies(raw);
|
|
3598
|
+
const evaluated = evaluateServicePolicies(
|
|
3599
|
+
compiled.policies,
|
|
3600
|
+
serviceIds,
|
|
3601
|
+
svcMap,
|
|
3602
|
+
tagId,
|
|
3603
|
+
visibleServiceIds
|
|
3604
|
+
);
|
|
3605
|
+
return {
|
|
3606
|
+
...evaluated,
|
|
3607
|
+
diagnostics: compiled.diagnostics
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
function evaluateServicePolicies(rules, svcIds, svcMap, tagId, visibleServiceIds) {
|
|
3611
|
+
var _a, _b, _c;
|
|
3612
|
+
const errors = [];
|
|
3613
|
+
const warnings = [];
|
|
3614
|
+
if (!rules || !rules.length) return { ok: true, errors, warnings };
|
|
3615
|
+
const relevant = rules.filter(
|
|
3616
|
+
(r) => r.subject === "services" && (r.scope === "visible_group" || r.scope === "global")
|
|
3617
|
+
);
|
|
3618
|
+
for (const r of relevant) {
|
|
3619
|
+
const scoped = scopeServiceIdsForRule(svcIds, r, visibleServiceIds);
|
|
3620
|
+
const ids = scoped.filter(
|
|
3621
|
+
(id) => matchesRuleFilter(getServiceCapability(svcMap, id), r, tagId)
|
|
3622
|
+
);
|
|
3623
|
+
const projection = r.projection || "service.id";
|
|
3624
|
+
const values = ids.map(
|
|
3625
|
+
(id) => policyProjectValue(getServiceCapability(svcMap, id), projection)
|
|
3626
|
+
);
|
|
3627
|
+
let ok = true;
|
|
3628
|
+
switch (r.op) {
|
|
3629
|
+
case "all_equal":
|
|
3630
|
+
ok = values.length <= 1 || values.every((v) => v === values[0]);
|
|
3631
|
+
break;
|
|
3632
|
+
case "unique": {
|
|
3633
|
+
const uniq2 = new Set(values.map((v) => String(v)));
|
|
3634
|
+
ok = uniq2.size === values.length;
|
|
3635
|
+
break;
|
|
3636
|
+
}
|
|
3637
|
+
case "no_mix": {
|
|
3638
|
+
const uniq2 = new Set(values.map((v) => String(v)));
|
|
3639
|
+
ok = uniq2.size <= 1;
|
|
3640
|
+
break;
|
|
3641
|
+
}
|
|
3642
|
+
case "all_true":
|
|
3643
|
+
ok = values.every((v) => !!v);
|
|
3644
|
+
break;
|
|
3645
|
+
case "any_true":
|
|
3646
|
+
ok = values.some((v) => !!v);
|
|
3647
|
+
break;
|
|
3648
|
+
case "max_count": {
|
|
3649
|
+
const n = typeof r.value === "number" ? r.value : NaN;
|
|
3650
|
+
ok = Number.isFinite(n) ? values.length <= n : true;
|
|
3651
|
+
break;
|
|
3652
|
+
}
|
|
3653
|
+
case "min_count": {
|
|
3654
|
+
const n = typeof r.value === "number" ? r.value : NaN;
|
|
3655
|
+
ok = Number.isFinite(n) ? values.length >= n : true;
|
|
3656
|
+
break;
|
|
3657
|
+
}
|
|
3658
|
+
default:
|
|
3659
|
+
ok = true;
|
|
3660
|
+
}
|
|
3661
|
+
if (!ok) {
|
|
3662
|
+
if (((_a = r.severity) != null ? _a : "error") === "error") {
|
|
3663
|
+
errors.push((_b = r.id) != null ? _b : "policy_error");
|
|
3664
|
+
} else {
|
|
3665
|
+
warnings.push((_c = r.id) != null ? _c : "policy_warning");
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
3670
|
+
}
|
|
3671
|
+
function scopeServiceIdsForRule(serviceIds, rule, visibleServiceIds) {
|
|
3672
|
+
if (rule.scope !== "visible_group" || !visibleServiceIds) return serviceIds;
|
|
3673
|
+
return serviceIds.filter((id) => visibleServiceIds.has(String(id)));
|
|
3674
|
+
}
|
|
3675
|
+
function collectVisibleServiceIds(builder, tagId, selectedButtons) {
|
|
3676
|
+
var _a, _b, _c;
|
|
3677
|
+
const out = /* @__PURE__ */ new Set();
|
|
3678
|
+
const props = builder.getProps();
|
|
3679
|
+
const tags = (_a = props.filters) != null ? _a : [];
|
|
3680
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
3681
|
+
const tag = tags.find((t) => t.id === tagId);
|
|
3682
|
+
if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
|
|
3683
|
+
const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
|
|
3684
|
+
for (const field of fields) {
|
|
3685
|
+
if (!visibleFieldIds.has(field.id)) continue;
|
|
3686
|
+
if (field.service_id != null) {
|
|
3687
|
+
out.add(String(field.service_id));
|
|
3688
|
+
}
|
|
3689
|
+
for (const option of (_c = field.options) != null ? _c : []) {
|
|
3690
|
+
if (option.service_id != null) {
|
|
3691
|
+
out.add(String(option.service_id));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
return out;
|
|
3696
|
+
}
|
|
3697
|
+
function policyProjectValue(cap, projection) {
|
|
3698
|
+
if (!cap) return void 0;
|
|
3699
|
+
const key = projection.startsWith("service.") ? projection.slice(8) : projection;
|
|
3700
|
+
return cap[key];
|
|
3701
|
+
}
|
|
3702
|
+
function matchesRuleFilter(cap, rule, tagId) {
|
|
3703
|
+
if (!cap) return false;
|
|
3704
|
+
const f = rule.filter;
|
|
3705
|
+
if (!f) return true;
|
|
3706
|
+
if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
|
|
3707
|
+
return true;
|
|
3708
|
+
}
|
|
3709
|
+
function toStrSet(v) {
|
|
3710
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
3711
|
+
const s = /* @__PURE__ */ new Set();
|
|
3712
|
+
for (const x of arr) s.add(String(x));
|
|
3713
|
+
return s;
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3107
3716
|
// src/utils/prune-fallbacks.ts
|
|
3108
3717
|
function pruneInvalidNodeFallbacks(props, services, settings) {
|
|
3109
3718
|
var _a, _b;
|
|
@@ -3190,43 +3799,6 @@ function toBindArray(bind) {
|
|
|
3190
3799
|
return Array.isArray(bind) ? bind.slice() : [bind];
|
|
3191
3800
|
}
|
|
3192
3801
|
|
|
3193
|
-
// src/utils/util.ts
|
|
3194
|
-
function toFiniteNumber(v) {
|
|
3195
|
-
const n = Number(v);
|
|
3196
|
-
return Number.isFinite(n) ? n : NaN;
|
|
3197
|
-
}
|
|
3198
|
-
function constraintFitOk(svcMap, candidate, constraints) {
|
|
3199
|
-
const cap = svcMap[Number(candidate)];
|
|
3200
|
-
if (!cap) return false;
|
|
3201
|
-
if (constraints.dripfeed === true && !cap.dripfeed) return false;
|
|
3202
|
-
if (constraints.refill === true && !cap.refill) return false;
|
|
3203
|
-
return !(constraints.cancel === true && !cap.cancel);
|
|
3204
|
-
}
|
|
3205
|
-
function rateOk(svcMap, candidate, primary, policy) {
|
|
3206
|
-
var _a, _b, _c;
|
|
3207
|
-
const cand = svcMap[Number(candidate)];
|
|
3208
|
-
const prim = svcMap[Number(primary)];
|
|
3209
|
-
if (!cand || !prim) return false;
|
|
3210
|
-
const cRate = toFiniteNumber(cand.rate);
|
|
3211
|
-
const pRate = toFiniteNumber(prim.rate);
|
|
3212
|
-
if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
|
|
3213
|
-
const rp = (_a = policy.ratePolicy) != null ? _a : { kind: "lte_primary" };
|
|
3214
|
-
switch (rp.kind) {
|
|
3215
|
-
case "lte_primary":
|
|
3216
|
-
return cRate <= pRate;
|
|
3217
|
-
case "within_pct": {
|
|
3218
|
-
const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
|
|
3219
|
-
return cRate <= pRate * (1 + pct / 100);
|
|
3220
|
-
}
|
|
3221
|
-
case "at_least_pct_lower": {
|
|
3222
|
-
const pct = Math.max(0, (_c = rp.pct) != null ? _c : 0);
|
|
3223
|
-
return cRate <= pRate * (1 - pct / 100);
|
|
3224
|
-
}
|
|
3225
|
-
default:
|
|
3226
|
-
return false;
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
3802
|
// src/utils/build-order-snapshot.ts
|
|
3231
3803
|
function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
|
|
3232
3804
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -3236,7 +3808,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
|
|
|
3236
3808
|
) ? settings.hostDefaultQuantity : 1;
|
|
3237
3809
|
const fbSettings = {
|
|
3238
3810
|
requireConstraintFit: true,
|
|
3239
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3811
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
3240
3812
|
selectionStrategy: "priority",
|
|
3241
3813
|
mode: mode === "dev" ? "dev" : "strict",
|
|
3242
3814
|
...(_c = settings.fallback) != null ? _c : {}
|
|
@@ -3272,7 +3844,9 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
|
|
|
3272
3844
|
const qtyRes = resolveQuantity(
|
|
3273
3845
|
visibleFieldIds,
|
|
3274
3846
|
fieldById,
|
|
3847
|
+
tagById,
|
|
3275
3848
|
selection,
|
|
3849
|
+
tagId,
|
|
3276
3850
|
hostDefaultQty
|
|
3277
3851
|
);
|
|
3278
3852
|
const quantity = qtyRes.quantity;
|
|
@@ -3387,7 +3961,7 @@ function buildInputs(visibleFieldIds, fieldById, selection) {
|
|
|
3387
3961
|
}
|
|
3388
3962
|
return { formValues, selections };
|
|
3389
3963
|
}
|
|
3390
|
-
function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
|
|
3964
|
+
function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId, hostDefault) {
|
|
3391
3965
|
var _a;
|
|
3392
3966
|
for (const fid of visibleFieldIds) {
|
|
3393
3967
|
const f = fieldById.get(fid);
|
|
@@ -3404,7 +3978,16 @@ function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
|
|
|
3404
3978
|
source: { kind: "field", id: f.id, rule }
|
|
3405
3979
|
};
|
|
3406
3980
|
}
|
|
3981
|
+
break;
|
|
3407
3982
|
}
|
|
3983
|
+
const nodeDefault = resolveNodeDefaultQuantity(
|
|
3984
|
+
visibleFieldIds,
|
|
3985
|
+
fieldById,
|
|
3986
|
+
tagById,
|
|
3987
|
+
selection,
|
|
3988
|
+
tagId
|
|
3989
|
+
);
|
|
3990
|
+
if (nodeDefault) return nodeDefault;
|
|
3408
3991
|
return {
|
|
3409
3992
|
quantity: hostDefault,
|
|
3410
3993
|
source: { kind: "default", defaultedFromHost: true }
|
|
@@ -3417,9 +4000,37 @@ function readQuantityRule(v) {
|
|
|
3417
4000
|
return void 0;
|
|
3418
4001
|
const out = { valueBy: src.valueBy };
|
|
3419
4002
|
if (src.code && typeof src.code === "string") out.code = src.code;
|
|
4003
|
+
if (typeof src.multiply === "number" && Number.isFinite(src.multiply)) {
|
|
4004
|
+
out.multiply = src.multiply;
|
|
4005
|
+
}
|
|
4006
|
+
if (typeof src.fallback === "number" && Number.isFinite(src.fallback)) {
|
|
4007
|
+
out.fallback = src.fallback;
|
|
4008
|
+
}
|
|
4009
|
+
if (src.clamp && typeof src.clamp === "object") {
|
|
4010
|
+
const min = typeof src.clamp.min === "number" && Number.isFinite(src.clamp.min) ? src.clamp.min : void 0;
|
|
4011
|
+
const max = typeof src.clamp.max === "number" && Number.isFinite(src.clamp.max) ? src.clamp.max : void 0;
|
|
4012
|
+
if (min !== void 0 || max !== void 0) {
|
|
4013
|
+
out.clamp = {
|
|
4014
|
+
...min !== void 0 ? { min } : {},
|
|
4015
|
+
...max !== void 0 ? { max } : {}
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
3420
4019
|
return out;
|
|
3421
4020
|
}
|
|
3422
4021
|
function evaluateQuantityRule(rule, raw) {
|
|
4022
|
+
const evaluated = evaluateRawQuantityRule(rule, raw);
|
|
4023
|
+
if (Number.isFinite(evaluated)) {
|
|
4024
|
+
const adjusted = applyQuantityTransforms(evaluated, rule);
|
|
4025
|
+
if (Number.isFinite(adjusted) && adjusted > 0) return adjusted;
|
|
4026
|
+
}
|
|
4027
|
+
if (typeof rule.fallback === "number" && Number.isFinite(rule.fallback)) {
|
|
4028
|
+
const fallback = applyClamp(rule.fallback, rule.clamp);
|
|
4029
|
+
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
4030
|
+
}
|
|
4031
|
+
return NaN;
|
|
4032
|
+
}
|
|
4033
|
+
function evaluateRawQuantityRule(rule, raw) {
|
|
3423
4034
|
switch (rule.valueBy) {
|
|
3424
4035
|
case "value": {
|
|
3425
4036
|
const n = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
@@ -3452,6 +4063,65 @@ function evaluateQuantityRule(rule, raw) {
|
|
|
3452
4063
|
return NaN;
|
|
3453
4064
|
}
|
|
3454
4065
|
}
|
|
4066
|
+
function applyQuantityTransforms(value, rule) {
|
|
4067
|
+
let next = value;
|
|
4068
|
+
if (typeof rule.multiply === "number" && Number.isFinite(rule.multiply)) {
|
|
4069
|
+
next *= rule.multiply;
|
|
4070
|
+
}
|
|
4071
|
+
return applyClamp(next, rule.clamp);
|
|
4072
|
+
}
|
|
4073
|
+
function applyClamp(value, clamp2) {
|
|
4074
|
+
let next = value;
|
|
4075
|
+
if ((clamp2 == null ? void 0 : clamp2.min) !== void 0) next = Math.max(next, clamp2.min);
|
|
4076
|
+
if ((clamp2 == null ? void 0 : clamp2.max) !== void 0) next = Math.min(next, clamp2.max);
|
|
4077
|
+
return next;
|
|
4078
|
+
}
|
|
4079
|
+
function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
|
|
4080
|
+
var _a, _b, _c, _d;
|
|
4081
|
+
const optionVisit = buildOptionVisitOrder(selection, fieldById);
|
|
4082
|
+
for (const { fieldId, optionId } of optionVisit) {
|
|
4083
|
+
if (!visibleFieldIds.includes(fieldId)) continue;
|
|
4084
|
+
const field = fieldById.get(fieldId);
|
|
4085
|
+
const option = (_a = field == null ? void 0 : field.options) == null ? void 0 : _a.find((item) => item.id === optionId);
|
|
4086
|
+
const quantityDefault = readQuantityDefault(
|
|
4087
|
+
(_b = option == null ? void 0 : option.meta) == null ? void 0 : _b.quantityDefault
|
|
4088
|
+
);
|
|
4089
|
+
if (quantityDefault !== void 0) {
|
|
4090
|
+
return {
|
|
4091
|
+
quantity: quantityDefault,
|
|
4092
|
+
source: { kind: "option", id: optionId }
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
for (const fieldId of visibleFieldIds) {
|
|
4097
|
+
const field = fieldById.get(fieldId);
|
|
4098
|
+
if (!field) continue;
|
|
4099
|
+
const isButtonStyle = field.button === true || Array.isArray(field.options) && field.options.length > 0;
|
|
4100
|
+
if (!isButtonStyle) continue;
|
|
4101
|
+
const quantityDefault = readQuantityDefault(
|
|
4102
|
+
field.quantityDefault
|
|
4103
|
+
);
|
|
4104
|
+
if (quantityDefault !== void 0) {
|
|
4105
|
+
return {
|
|
4106
|
+
quantity: quantityDefault,
|
|
4107
|
+
source: { kind: "field", id: field.id }
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
const tagQuantityDefault = readQuantityDefault(
|
|
4112
|
+
(_d = (_c = tagById.get(tagId)) == null ? void 0 : _c.meta) == null ? void 0 : _d.quantityDefault
|
|
4113
|
+
);
|
|
4114
|
+
if (tagQuantityDefault !== void 0) {
|
|
4115
|
+
return {
|
|
4116
|
+
quantity: tagQuantityDefault,
|
|
4117
|
+
source: { kind: "tag", id: tagId }
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
return void 0;
|
|
4121
|
+
}
|
|
4122
|
+
function readQuantityDefault(value) {
|
|
4123
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
4124
|
+
}
|
|
3455
4125
|
function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById) {
|
|
3456
4126
|
var _a;
|
|
3457
4127
|
const serviceMap = {};
|
|
@@ -3570,7 +4240,7 @@ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
|
|
|
3570
4240
|
fallbacks.global
|
|
3571
4241
|
)) {
|
|
3572
4242
|
if (!present.has(String(primary))) continue;
|
|
3573
|
-
const primId =
|
|
4243
|
+
const primId = isFiniteNumber2(primary) ? Number(primary) : primary;
|
|
3574
4244
|
const kept = [];
|
|
3575
4245
|
for (const cand of cands != null ? cands : []) {
|
|
3576
4246
|
if (!rateOk(svcMap, cand, primId, policy)) continue;
|
|
@@ -3589,7 +4259,7 @@ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
|
|
|
3589
4259
|
};
|
|
3590
4260
|
}
|
|
3591
4261
|
}
|
|
3592
|
-
function
|
|
4262
|
+
function isFiniteNumber2(v) {
|
|
3593
4263
|
return typeof v === "number" && Number.isFinite(v);
|
|
3594
4264
|
}
|
|
3595
4265
|
function collectUtilityLineItems(visibleFieldIds, fieldById, selection, quantity) {
|
|
@@ -3644,9 +4314,14 @@ function readUtilityMarker(v) {
|
|
|
3644
4314
|
if (src.mode !== "flat" && src.mode !== "per_quantity" && src.mode !== "per_value" && src.mode !== "percent")
|
|
3645
4315
|
return void 0;
|
|
3646
4316
|
const out = { mode: src.mode, rate: src.rate };
|
|
3647
|
-
if (src.valueBy === "value" || src.valueBy === "length"
|
|
4317
|
+
if (src.valueBy === "value" || src.valueBy === "length")
|
|
3648
4318
|
out.valueBy = src.valueBy;
|
|
3649
|
-
if (src.
|
|
4319
|
+
if (src.percentBase === "service_total" || src.percentBase === "base_service" || src.percentBase === "all") {
|
|
4320
|
+
out.percentBase = src.percentBase;
|
|
4321
|
+
}
|
|
4322
|
+
if (typeof src.label === "string" && src.label.trim()) {
|
|
4323
|
+
out.label = src.label.trim();
|
|
4324
|
+
}
|
|
3650
4325
|
return out;
|
|
3651
4326
|
}
|
|
3652
4327
|
function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
|
|
@@ -3655,14 +4330,14 @@ function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
|
|
|
3655
4330
|
nodeId,
|
|
3656
4331
|
mode: marker.mode,
|
|
3657
4332
|
rate: marker.rate,
|
|
4333
|
+
...marker.percentBase ? { percentBase: marker.percentBase } : {},
|
|
4334
|
+
...marker.label ? { label: marker.label } : {},
|
|
3658
4335
|
inputs: { quantity }
|
|
3659
4336
|
};
|
|
3660
4337
|
if (marker.mode === "per_value") {
|
|
3661
4338
|
base.inputs.valueBy = (_a = marker.valueBy) != null ? _a : "value";
|
|
3662
4339
|
if (marker.valueBy === "length") {
|
|
3663
4340
|
base.inputs.value = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : 0;
|
|
3664
|
-
} else if (marker.valueBy === "eval") {
|
|
3665
|
-
base.inputs.evalCodeUsed = true;
|
|
3666
4341
|
} else {
|
|
3667
4342
|
base.inputs.value = Array.isArray(value) ? (_b = value[0]) != null ? _b : null : value != null ? value : null;
|
|
3668
4343
|
}
|
|
@@ -3733,47 +4408,13 @@ function buildDevWarnings(props, svcMap, _tagId, _snapshotServiceMap, originalFa
|
|
|
3733
4408
|
return out;
|
|
3734
4409
|
}
|
|
3735
4410
|
function toSnapshotPolicy(settings) {
|
|
3736
|
-
var _a
|
|
4411
|
+
var _a;
|
|
3737
4412
|
const requireConstraintFit = (_a = settings.requireConstraintFit) != null ? _a : true;
|
|
3738
|
-
const rp = (
|
|
3739
|
-
|
|
3740
|
-
case "lte_primary":
|
|
3741
|
-
return {
|
|
3742
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3743
|
-
requireConstraintFit
|
|
3744
|
-
};
|
|
3745
|
-
case "within_pct":
|
|
3746
|
-
return {
|
|
3747
|
-
ratePolicy: {
|
|
3748
|
-
kind: "lte_primary",
|
|
3749
|
-
thresholdPct: Math.max(0, (_c = rp.pct) != null ? _c : 0)
|
|
3750
|
-
},
|
|
3751
|
-
requireConstraintFit
|
|
3752
|
-
};
|
|
3753
|
-
case "at_least_pct_lower":
|
|
3754
|
-
return {
|
|
3755
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3756
|
-
requireConstraintFit
|
|
3757
|
-
};
|
|
3758
|
-
default:
|
|
3759
|
-
return {
|
|
3760
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3761
|
-
requireConstraintFit
|
|
3762
|
-
};
|
|
3763
|
-
}
|
|
4413
|
+
const rp = normalizeRatePolicy(settings.ratePolicy);
|
|
4414
|
+
return { ratePolicy: rp, requireConstraintFit };
|
|
3764
4415
|
}
|
|
3765
4416
|
function getCap2(map, id) {
|
|
3766
|
-
|
|
3767
|
-
if (direct) return direct;
|
|
3768
|
-
const strKey = String(id);
|
|
3769
|
-
const byStr = map[strKey];
|
|
3770
|
-
if (byStr) return byStr;
|
|
3771
|
-
const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
|
|
3772
|
-
if (Number.isFinite(n)) {
|
|
3773
|
-
const byNum = map[n];
|
|
3774
|
-
if (byNum) return byNum;
|
|
3775
|
-
}
|
|
3776
|
-
return void 0;
|
|
4417
|
+
return getServiceCapability(map, id);
|
|
3777
4418
|
}
|
|
3778
4419
|
function resolveMinMax(servicesList, services) {
|
|
3779
4420
|
let min = void 0;
|
|
@@ -4137,9 +4778,11 @@ export {
|
|
|
4137
4778
|
createBuilder,
|
|
4138
4779
|
createFallbackEditor,
|
|
4139
4780
|
createNodeIndex,
|
|
4781
|
+
filterServicesForVisibleGroup,
|
|
4140
4782
|
getEligibleFallbacks,
|
|
4141
4783
|
getFallbackRegistrationInfo,
|
|
4142
4784
|
normalise,
|
|
4785
|
+
normalizeFieldValidation,
|
|
4143
4786
|
resolveServiceFallback,
|
|
4144
4787
|
validate,
|
|
4145
4788
|
validateAsync,
|