@timeax/digital-service-engine 0.0.6 → 0.0.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 +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 +10 -10
- package/schema/editor-snapshot.schema.json +373 -9
- package/schema/service-props.schema.json +86 -9
package/dist/core/index.cjs
CHANGED
|
@@ -25,9 +25,11 @@ __export(core_exports, {
|
|
|
25
25
|
createBuilder: () => createBuilder,
|
|
26
26
|
createFallbackEditor: () => createFallbackEditor,
|
|
27
27
|
createNodeIndex: () => createNodeIndex,
|
|
28
|
+
filterServicesForVisibleGroup: () => filterServicesForVisibleGroup,
|
|
28
29
|
getEligibleFallbacks: () => getEligibleFallbacks,
|
|
29
30
|
getFallbackRegistrationInfo: () => getFallbackRegistrationInfo,
|
|
30
31
|
normalise: () => normalise,
|
|
32
|
+
normalizeFieldValidation: () => normalizeFieldValidation,
|
|
31
33
|
resolveServiceFallback: () => resolveServiceFallback,
|
|
32
34
|
validate: () => validate,
|
|
33
35
|
validateAsync: () => validateAsync,
|
|
@@ -50,6 +52,7 @@ function normalise(input, opts = {}) {
|
|
|
50
52
|
const excludes_for_buttons = toStringArrayMap(
|
|
51
53
|
obj.excludes_for_buttons
|
|
52
54
|
);
|
|
55
|
+
const notices = toNoticeArray(obj.notices);
|
|
53
56
|
let filters = rawFilters.map((t) => coerceTag(t, constraints));
|
|
54
57
|
const fields = rawFields.map((f) => coerceField(f, defRole));
|
|
55
58
|
if (!filters.some((t) => t.id === "t:root")) {
|
|
@@ -65,6 +68,7 @@ function normalise(input, opts = {}) {
|
|
|
65
68
|
...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
|
|
66
69
|
fallbacks
|
|
67
70
|
},
|
|
71
|
+
...notices.length > 0 && { notices },
|
|
68
72
|
schema_version: typeof obj.schema_version === "string" ? obj.schema_version : "1.0"
|
|
69
73
|
};
|
|
70
74
|
propagateConstraints(out, constraints);
|
|
@@ -136,7 +140,7 @@ function coerceTag(src, flagKeys) {
|
|
|
136
140
|
const id = str(src.id);
|
|
137
141
|
const label = str(src.label);
|
|
138
142
|
const bind_id = str(src.bind_id) || (id == "t:root" ? void 0 : "t:root");
|
|
139
|
-
const service_id =
|
|
143
|
+
const service_id = toServiceIdOrUndefined(src.service_id);
|
|
140
144
|
const includes = toStringArray(src.includes);
|
|
141
145
|
const excludes = toStringArray(src.excludes);
|
|
142
146
|
let constraints = void 0;
|
|
@@ -185,7 +189,8 @@ function coerceField(src, defRole) {
|
|
|
185
189
|
const component = type === "custom" ? str(src.component) || void 0 : void 0;
|
|
186
190
|
const meta = src.meta && typeof src.meta === "object" ? { ...src.meta } : void 0;
|
|
187
191
|
const button = srcHasOptions ? true : src.button === true;
|
|
188
|
-
const
|
|
192
|
+
const validation = normalizeFieldValidation(src.validation);
|
|
193
|
+
const field_service_id_raw = toServiceIdOrUndefined(src.service_id);
|
|
189
194
|
const field_service_id = button && pricing_role !== "utility" && field_service_id_raw !== void 0 ? field_service_id_raw : void 0;
|
|
190
195
|
const field = {
|
|
191
196
|
id,
|
|
@@ -200,6 +205,7 @@ function coerceField(src, defRole) {
|
|
|
200
205
|
...ui && { ui },
|
|
201
206
|
...defaults && { defaults },
|
|
202
207
|
...meta && { meta },
|
|
208
|
+
...validation && { validation },
|
|
203
209
|
...button ? { button } : {},
|
|
204
210
|
...field_service_id !== void 0 && { service_id: field_service_id }
|
|
205
211
|
};
|
|
@@ -209,7 +215,7 @@ function coerceOption(src, inheritRole) {
|
|
|
209
215
|
if (!src || typeof src !== "object") src = {};
|
|
210
216
|
const id = str(src.id);
|
|
211
217
|
const label = str(src.label);
|
|
212
|
-
const service_id =
|
|
218
|
+
const service_id = toServiceIdOrUndefined(src.service_id);
|
|
213
219
|
const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
|
|
214
220
|
const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
|
|
215
221
|
const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
|
|
@@ -281,10 +287,20 @@ function toStringArray(v) {
|
|
|
281
287
|
if (!Array.isArray(v)) return [];
|
|
282
288
|
return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
|
|
283
289
|
}
|
|
284
|
-
function
|
|
290
|
+
function toNoticeArray(v) {
|
|
291
|
+
if (!Array.isArray(v)) return [];
|
|
292
|
+
return v.filter((item) => item && typeof item === "object").map((item) => (0, import_lodash_es.cloneDeep)(item));
|
|
293
|
+
}
|
|
294
|
+
function toServiceIdOrUndefined(v) {
|
|
285
295
|
if (v === null || v === void 0) return void 0;
|
|
286
|
-
|
|
287
|
-
|
|
296
|
+
if (typeof v === "number") {
|
|
297
|
+
return Number.isFinite(v) ? v : void 0;
|
|
298
|
+
}
|
|
299
|
+
if (typeof v === "string") {
|
|
300
|
+
const trimmed = v.trim();
|
|
301
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
302
|
+
}
|
|
303
|
+
return void 0;
|
|
288
304
|
}
|
|
289
305
|
function str(v) {
|
|
290
306
|
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
@@ -308,14 +324,49 @@ function toServiceIdArray(v) {
|
|
|
308
324
|
(x) => x !== "" && x !== null && x !== void 0
|
|
309
325
|
);
|
|
310
326
|
}
|
|
327
|
+
function normalizeFieldValidationRule(input) {
|
|
328
|
+
if (!input || typeof input !== "object") return void 0;
|
|
329
|
+
const v = input;
|
|
330
|
+
const op = v.op;
|
|
331
|
+
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") {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
const valueBy = v.valueBy === "value" || v.valueBy === "length" || v.valueBy === "eval" ? v.valueBy : void 0;
|
|
335
|
+
const out = {
|
|
336
|
+
op,
|
|
337
|
+
...valueBy ? { valueBy } : {}
|
|
338
|
+
};
|
|
339
|
+
if ("value" in v) out.value = v.value;
|
|
340
|
+
if (typeof v.min === "number" && Number.isFinite(v.min)) out.min = v.min;
|
|
341
|
+
if (typeof v.max === "number" && Number.isFinite(v.max)) out.max = v.max;
|
|
342
|
+
if (Array.isArray(v.values)) out.values = [...v.values];
|
|
343
|
+
if (typeof v.pattern === "string" && v.pattern.trim()) out.pattern = v.pattern;
|
|
344
|
+
if (typeof v.flags === "string") out.flags = v.flags;
|
|
345
|
+
if (typeof v.message === "string" && v.message.trim()) out.message = v.message;
|
|
346
|
+
if (valueBy === "eval" && typeof v.code === "string" && v.code.trim()) {
|
|
347
|
+
out.code = v.code;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
function normalizeFieldValidation(input) {
|
|
352
|
+
if (Array.isArray(input)) {
|
|
353
|
+
const rules = input.map(normalizeFieldValidationRule).filter(Boolean);
|
|
354
|
+
return rules.length ? rules : void 0;
|
|
355
|
+
}
|
|
356
|
+
const one = normalizeFieldValidationRule(input);
|
|
357
|
+
return one ? [one] : void 0;
|
|
358
|
+
}
|
|
311
359
|
|
|
312
360
|
// src/core/validate/shared.ts
|
|
313
361
|
function isFiniteNumber(v) {
|
|
314
362
|
return typeof v === "number" && Number.isFinite(v);
|
|
315
363
|
}
|
|
364
|
+
function isServiceIdRef(v) {
|
|
365
|
+
return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
|
|
366
|
+
}
|
|
316
367
|
function hasAnyServiceOption(f) {
|
|
317
368
|
var _a;
|
|
318
|
-
return ((_a = f.options) != null ? _a : []).some((o) =>
|
|
369
|
+
return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
|
|
319
370
|
}
|
|
320
371
|
function getByPath(obj, path) {
|
|
321
372
|
if (!path) return void 0;
|
|
@@ -661,7 +712,7 @@ function runVisibilityRulesOnce(v) {
|
|
|
661
712
|
const utilityOptionIds = [];
|
|
662
713
|
for (const f of visible) {
|
|
663
714
|
for (const o of (_c = f.options) != null ? _c : []) {
|
|
664
|
-
if (!
|
|
715
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
665
716
|
const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
|
|
666
717
|
if (role === "base") hasBase = true;
|
|
667
718
|
else if (role === "utility") {
|
|
@@ -1089,7 +1140,7 @@ function validateUtilityMarkers(v) {
|
|
|
1089
1140
|
const optsArr = Array.isArray(f.options) ? f.options : [];
|
|
1090
1141
|
for (const o of optsArr) {
|
|
1091
1142
|
const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
|
|
1092
|
-
const hasService =
|
|
1143
|
+
const hasService = isServiceIdRef(o.service_id);
|
|
1093
1144
|
const util = (_c = o.meta) == null ? void 0 : _c.utility;
|
|
1094
1145
|
if (role === "utility" && hasService) {
|
|
1095
1146
|
v.errors.push({
|
|
@@ -1175,39 +1226,119 @@ function isMultiField(f) {
|
|
|
1175
1226
|
return t === "multiselect" || t === "checkbox" || metaMulti;
|
|
1176
1227
|
}
|
|
1177
1228
|
|
|
1229
|
+
// src/utils/util.ts
|
|
1230
|
+
function toFiniteNumber(v) {
|
|
1231
|
+
const n = Number(v);
|
|
1232
|
+
return Number.isFinite(n) ? n : NaN;
|
|
1233
|
+
}
|
|
1234
|
+
function constraintFitOk(svcMap, candidate, constraints) {
|
|
1235
|
+
const cap = getServiceCapability(svcMap, candidate);
|
|
1236
|
+
if (!cap) return false;
|
|
1237
|
+
if (constraints.dripfeed === true && !cap.dripfeed) return false;
|
|
1238
|
+
if (constraints.refill === true && !cap.refill) return false;
|
|
1239
|
+
return !(constraints.cancel === true && !cap.cancel);
|
|
1240
|
+
}
|
|
1241
|
+
function getServiceCapability(svcMap, candidate) {
|
|
1242
|
+
if (candidate === void 0 || candidate === null) return void 0;
|
|
1243
|
+
const direct = svcMap[candidate];
|
|
1244
|
+
if (direct) return direct;
|
|
1245
|
+
const byString = svcMap[String(candidate)];
|
|
1246
|
+
if (byString) return byString;
|
|
1247
|
+
if (typeof candidate === "string") {
|
|
1248
|
+
const maybeNumber = Number(candidate);
|
|
1249
|
+
if (Number.isFinite(maybeNumber)) {
|
|
1250
|
+
return svcMap[maybeNumber];
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return void 0;
|
|
1254
|
+
}
|
|
1255
|
+
function normalizeRatePolicy(policy) {
|
|
1256
|
+
var _a;
|
|
1257
|
+
if (!policy) return { kind: "lte_primary", pct: 5 };
|
|
1258
|
+
if (policy.kind === "eq_primary") return policy;
|
|
1259
|
+
const pct = Math.max(0, Number((_a = policy.pct) != null ? _a : 0));
|
|
1260
|
+
return { ...policy, pct };
|
|
1261
|
+
}
|
|
1262
|
+
function passesRatePolicy(policy, primaryRate, candidateRate) {
|
|
1263
|
+
if (!Number.isFinite(primaryRate) || !Number.isFinite(candidateRate)) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
const rp = normalizeRatePolicy(policy);
|
|
1267
|
+
switch (rp.kind) {
|
|
1268
|
+
case "eq_primary":
|
|
1269
|
+
return candidateRate === primaryRate;
|
|
1270
|
+
case "lte_primary": {
|
|
1271
|
+
const floor = primaryRate * (1 - rp.pct / 100);
|
|
1272
|
+
return candidateRate <= primaryRate && candidateRate >= floor;
|
|
1273
|
+
}
|
|
1274
|
+
case "within_pct":
|
|
1275
|
+
return candidateRate <= primaryRate * (1 + rp.pct / 100);
|
|
1276
|
+
case "at_least_pct_lower":
|
|
1277
|
+
return candidateRate <= primaryRate * (1 - rp.pct / 100);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function rateOk(svcMap, candidate, primary, policy) {
|
|
1281
|
+
const cand = getServiceCapability(svcMap, candidate);
|
|
1282
|
+
const prim = getServiceCapability(svcMap, primary);
|
|
1283
|
+
if (!cand || !prim) return false;
|
|
1284
|
+
const cRate = toFiniteNumber(cand.rate);
|
|
1285
|
+
const pRate = toFiniteNumber(prim.rate);
|
|
1286
|
+
if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
|
|
1287
|
+
return passesRatePolicy(policy.ratePolicy, pRate, cRate);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1178
1290
|
// src/core/validate/steps/rates.ts
|
|
1179
1291
|
function validateRates(v) {
|
|
1180
1292
|
var _a, _b, _c, _d;
|
|
1293
|
+
const ratePolicy = normalizeRatePolicy(
|
|
1294
|
+
(_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
|
|
1295
|
+
);
|
|
1181
1296
|
for (const f of v.fields) {
|
|
1182
1297
|
if (!isMultiField(f)) continue;
|
|
1183
|
-
const baseRates =
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
1298
|
+
const baseRates = [];
|
|
1299
|
+
for (const o of (_b = f.options) != null ? _b : []) {
|
|
1300
|
+
const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
|
|
1187
1301
|
if (role !== "base") continue;
|
|
1188
1302
|
const sid = o.service_id;
|
|
1189
|
-
if (!
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1303
|
+
if (!isServiceIdRef(sid)) continue;
|
|
1304
|
+
const cap = getServiceCapability(v.serviceMap, sid);
|
|
1305
|
+
const rate = cap == null ? void 0 : cap.rate;
|
|
1306
|
+
if (typeof rate === "number" && Number.isFinite(rate)) {
|
|
1307
|
+
baseRates.push({
|
|
1308
|
+
optionId: o.id,
|
|
1309
|
+
serviceId: String(sid),
|
|
1310
|
+
rate
|
|
1311
|
+
});
|
|
1194
1312
|
}
|
|
1195
1313
|
}
|
|
1196
|
-
if (baseRates.
|
|
1314
|
+
if (baseRates.length <= 1) continue;
|
|
1315
|
+
const primary = baseRates.reduce(
|
|
1316
|
+
(best, current) => current.rate > best.rate ? current : best
|
|
1317
|
+
);
|
|
1318
|
+
const offenders = baseRates.filter(
|
|
1319
|
+
(candidate) => candidate.optionId !== primary.optionId && !passesRatePolicy(ratePolicy, primary.rate, candidate.rate)
|
|
1320
|
+
);
|
|
1321
|
+
if (offenders.length > 0) {
|
|
1322
|
+
v.invalidRateFieldIds.add(f.id);
|
|
1197
1323
|
const affectedIds = [
|
|
1198
1324
|
f.id,
|
|
1199
|
-
...
|
|
1325
|
+
...baseRates.map((entry) => entry.optionId)
|
|
1200
1326
|
];
|
|
1201
1327
|
v.errors.push({
|
|
1202
1328
|
code: "rate_mismatch_across_base",
|
|
1203
1329
|
severity: "error",
|
|
1204
|
-
message: `Base options under field "${f.id}"
|
|
1330
|
+
message: `Base options under field "${f.id}" violate rate policy "${ratePolicy.kind}".`,
|
|
1205
1331
|
nodeId: f.id,
|
|
1206
1332
|
details: withAffected(
|
|
1207
1333
|
{
|
|
1208
1334
|
fieldId: f.id,
|
|
1209
|
-
|
|
1210
|
-
|
|
1335
|
+
policy: ratePolicy.kind,
|
|
1336
|
+
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
1337
|
+
primaryOptionId: primary.optionId,
|
|
1338
|
+
primaryRate: primary.rate,
|
|
1339
|
+
rates: baseRates.map((entry) => entry.rate),
|
|
1340
|
+
optionIds: baseRates.map((entry) => entry.optionId),
|
|
1341
|
+
offenderOptionIds: offenders.map((entry) => entry.optionId)
|
|
1211
1342
|
},
|
|
1212
1343
|
affectedIds.length > 1 ? affectedIds : void 0
|
|
1213
1344
|
)
|
|
@@ -1269,8 +1400,8 @@ function validateConstraints(v) {
|
|
|
1269
1400
|
const visible = v.fieldsVisibleUnder(t.id);
|
|
1270
1401
|
for (const f of visible) {
|
|
1271
1402
|
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1272
|
-
if (!
|
|
1273
|
-
const svc = v.serviceMap
|
|
1403
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
1404
|
+
const svc = getServiceCapability(v.serviceMap, o.service_id);
|
|
1274
1405
|
if (!svc || typeof svc !== "object") continue;
|
|
1275
1406
|
for (const [k, val] of Object.entries(eff)) {
|
|
1276
1407
|
if (val === true && !isServiceFlagEnabled(svc, k)) {
|
|
@@ -1296,8 +1427,8 @@ function validateConstraints(v) {
|
|
|
1296
1427
|
}
|
|
1297
1428
|
for (const t of v.tags) {
|
|
1298
1429
|
const sid = t.service_id;
|
|
1299
|
-
if (!
|
|
1300
|
-
const svc = v.serviceMap
|
|
1430
|
+
if (!isServiceIdRef(sid)) continue;
|
|
1431
|
+
const svc = getServiceCapability(v.serviceMap, sid);
|
|
1301
1432
|
if (!svc || typeof svc !== "object") continue;
|
|
1302
1433
|
const eff = effectiveConstraints(v, t.id);
|
|
1303
1434
|
for (const [k, val] of Object.entries(eff)) {
|
|
@@ -1362,7 +1493,7 @@ function validateGlobalUtilityGuard(v) {
|
|
|
1362
1493
|
let hasBase = false;
|
|
1363
1494
|
for (const f of v.fields) {
|
|
1364
1495
|
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1365
|
-
if (!
|
|
1496
|
+
if (!isServiceIdRef(o.service_id)) continue;
|
|
1366
1497
|
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
1367
1498
|
if (role === "base") hasBase = true;
|
|
1368
1499
|
else if (role === "utility") hasUtility = true;
|
|
@@ -1507,7 +1638,7 @@ function asArray(v) {
|
|
|
1507
1638
|
if (v === void 0) return void 0;
|
|
1508
1639
|
return Array.isArray(v) ? v : [v];
|
|
1509
1640
|
}
|
|
1510
|
-
function
|
|
1641
|
+
function isServiceIdRef2(v) {
|
|
1511
1642
|
return typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
1512
1643
|
}
|
|
1513
1644
|
function svcSnapshot(serviceMap, sid) {
|
|
@@ -1583,7 +1714,7 @@ function collectServiceItems(args) {
|
|
|
1583
1714
|
if (args.mode === "global") {
|
|
1584
1715
|
for (const t of (_b = args.tags) != null ? _b : []) {
|
|
1585
1716
|
const sid = t.service_id;
|
|
1586
|
-
if (!
|
|
1717
|
+
if (!isServiceIdRef2(sid)) continue;
|
|
1587
1718
|
addServiceRef({
|
|
1588
1719
|
tagId: t.id,
|
|
1589
1720
|
serviceId: sid,
|
|
@@ -1594,7 +1725,7 @@ function collectServiceItems(args) {
|
|
|
1594
1725
|
} else if (args.mode === "visible_group") {
|
|
1595
1726
|
const t = args.tag;
|
|
1596
1727
|
const sid = t ? t.service_id : void 0;
|
|
1597
|
-
if (t &&
|
|
1728
|
+
if (t && isServiceIdRef2(sid)) {
|
|
1598
1729
|
addServiceRef({
|
|
1599
1730
|
tagId: t.id,
|
|
1600
1731
|
serviceId: sid,
|
|
@@ -1606,7 +1737,7 @@ function collectServiceItems(args) {
|
|
|
1606
1737
|
const fields = (_c = args.fields) != null ? _c : [];
|
|
1607
1738
|
for (const f of fields) {
|
|
1608
1739
|
const fSid = f.service_id;
|
|
1609
|
-
if (
|
|
1740
|
+
if (isServiceIdRef2(fSid)) {
|
|
1610
1741
|
addServiceRef({
|
|
1611
1742
|
tagId: args.tagId,
|
|
1612
1743
|
fieldId: f.id,
|
|
@@ -1617,7 +1748,7 @@ function collectServiceItems(args) {
|
|
|
1617
1748
|
}
|
|
1618
1749
|
for (const o of (_d = f.options) != null ? _d : []) {
|
|
1619
1750
|
const oSid = o.service_id;
|
|
1620
|
-
if (!
|
|
1751
|
+
if (!isServiceIdRef2(oSid)) continue;
|
|
1621
1752
|
const role = fieldRoleOf(f, o);
|
|
1622
1753
|
addServiceRef({
|
|
1623
1754
|
tagId: args.tagId,
|
|
@@ -1638,7 +1769,7 @@ function collectServiceItems(args) {
|
|
|
1638
1769
|
const addFallbackNode = (nodeId, list) => {
|
|
1639
1770
|
const arr = Array.isArray(list) ? list : [];
|
|
1640
1771
|
for (const cand of arr) {
|
|
1641
|
-
if (!
|
|
1772
|
+
if (!isServiceIdRef2(cand)) continue;
|
|
1642
1773
|
addServiceRef({
|
|
1643
1774
|
tagId: args.tagId,
|
|
1644
1775
|
nodeId,
|
|
@@ -1662,7 +1793,7 @@ function collectServiceItems(args) {
|
|
|
1662
1793
|
});
|
|
1663
1794
|
const arr = Array.isArray(list) ? list : [];
|
|
1664
1795
|
for (const cand of arr) {
|
|
1665
|
-
if (!
|
|
1796
|
+
if (!isServiceIdRef2(cand)) continue;
|
|
1666
1797
|
addServiceRef({
|
|
1667
1798
|
tagId: args.tagId,
|
|
1668
1799
|
nodeId: primaryKey,
|
|
@@ -1940,85 +2071,8 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
|
|
|
1940
2071
|
}
|
|
1941
2072
|
}
|
|
1942
2073
|
|
|
1943
|
-
// src/core/validate/index.ts
|
|
1944
|
-
function readVisibilitySimOpts(ctx) {
|
|
1945
|
-
const c = ctx;
|
|
1946
|
-
const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
|
|
1947
|
-
const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
|
|
1948
|
-
const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
|
|
1949
|
-
const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
|
|
1950
|
-
const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
|
|
1951
|
-
return {
|
|
1952
|
-
simulate,
|
|
1953
|
-
maxStates,
|
|
1954
|
-
maxDepth,
|
|
1955
|
-
simulateAllRoots,
|
|
1956
|
-
onlyEffectfulTriggers
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
function validate(props, ctx = {}) {
|
|
1960
|
-
var _a, _b, _c;
|
|
1961
|
-
const errors = [];
|
|
1962
|
-
const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
|
|
1963
|
-
const selectedKeys = new Set(
|
|
1964
|
-
(_b = ctx.selectedOptionKeys) != null ? _b : []
|
|
1965
|
-
);
|
|
1966
|
-
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
1967
|
-
const fields = Array.isArray(props.fields) ? props.fields : [];
|
|
1968
|
-
const tagById = /* @__PURE__ */ new Map();
|
|
1969
|
-
const fieldById = /* @__PURE__ */ new Map();
|
|
1970
|
-
for (const t of tags) tagById.set(t.id, t);
|
|
1971
|
-
for (const f of fields) fieldById.set(f.id, f);
|
|
1972
|
-
const v = {
|
|
1973
|
-
props,
|
|
1974
|
-
nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
|
|
1975
|
-
options: ctx,
|
|
1976
|
-
errors,
|
|
1977
|
-
serviceMap,
|
|
1978
|
-
selectedKeys,
|
|
1979
|
-
tags,
|
|
1980
|
-
fields,
|
|
1981
|
-
tagById,
|
|
1982
|
-
fieldById,
|
|
1983
|
-
fieldsVisibleUnder: (_tagId) => []
|
|
1984
|
-
};
|
|
1985
|
-
validateStructure(v);
|
|
1986
|
-
validateIdentity(v);
|
|
1987
|
-
validateOptionMaps(v);
|
|
1988
|
-
v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
|
|
1989
|
-
const visSim = readVisibilitySimOpts(ctx);
|
|
1990
|
-
validateVisibility(v, visSim);
|
|
1991
|
-
applyPolicies(
|
|
1992
|
-
v.errors,
|
|
1993
|
-
v.props,
|
|
1994
|
-
v.serviceMap,
|
|
1995
|
-
v.options.policies,
|
|
1996
|
-
v.fieldsVisibleUnder,
|
|
1997
|
-
v.tags
|
|
1998
|
-
);
|
|
1999
|
-
validateServiceVsUserInput(v);
|
|
2000
|
-
validateUtilityMarkers(v);
|
|
2001
|
-
validateRates(v);
|
|
2002
|
-
validateConstraints(v);
|
|
2003
|
-
validateCustomFields(v);
|
|
2004
|
-
validateGlobalUtilityGuard(v);
|
|
2005
|
-
validateUnboundFields(v);
|
|
2006
|
-
validateFallbacks(v);
|
|
2007
|
-
return v.errors;
|
|
2008
|
-
}
|
|
2009
|
-
async function validateAsync(props, ctx = {}) {
|
|
2010
|
-
await Promise.resolve();
|
|
2011
|
-
if (typeof requestAnimationFrame === "function") {
|
|
2012
|
-
await new Promise(
|
|
2013
|
-
(resolve) => requestAnimationFrame(() => resolve())
|
|
2014
|
-
);
|
|
2015
|
-
} else {
|
|
2016
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2017
|
-
}
|
|
2018
|
-
return validate(props, ctx);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
2074
|
// src/core/builder.ts
|
|
2075
|
+
var import_lodash_es2 = require("lodash-es");
|
|
2022
2076
|
function createBuilder(opts = {}) {
|
|
2023
2077
|
return new BuilderImpl(opts);
|
|
2024
2078
|
}
|
|
@@ -2032,12 +2086,8 @@ var BuilderImpl = class {
|
|
|
2032
2086
|
this.tagById = /* @__PURE__ */ new Map();
|
|
2033
2087
|
this.fieldById = /* @__PURE__ */ new Map();
|
|
2034
2088
|
this.optionOwnerById = /* @__PURE__ */ new Map();
|
|
2035
|
-
this.history = [];
|
|
2036
|
-
this.future = [];
|
|
2037
2089
|
this._nodemap = null;
|
|
2038
|
-
var _a;
|
|
2039
2090
|
this.options = { ...opts };
|
|
2040
|
-
this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
|
|
2041
2091
|
}
|
|
2042
2092
|
/* ───── lifecycle ─────────────────────────────────────────────────────── */
|
|
2043
2093
|
isTagId(id) {
|
|
@@ -2054,8 +2104,6 @@ var BuilderImpl = class {
|
|
|
2054
2104
|
defaultPricingRole: "base",
|
|
2055
2105
|
constraints: this.getConstraints().map((item) => item.label)
|
|
2056
2106
|
});
|
|
2057
|
-
this.pushHistory(this.props);
|
|
2058
|
-
this.future.length = 0;
|
|
2059
2107
|
this.props = next;
|
|
2060
2108
|
this.rebuildIndexes();
|
|
2061
2109
|
}
|
|
@@ -2267,6 +2315,9 @@ var BuilderImpl = class {
|
|
|
2267
2315
|
errors() {
|
|
2268
2316
|
return validate(this.props, this.options);
|
|
2269
2317
|
}
|
|
2318
|
+
getOptions() {
|
|
2319
|
+
return (0, import_lodash_es2.cloneDeep)(this.options);
|
|
2320
|
+
}
|
|
2270
2321
|
visibleFields(tagId, selectedKeys) {
|
|
2271
2322
|
var _a;
|
|
2272
2323
|
return visibleFieldIdsUnder(this.props, tagId, {
|
|
@@ -2279,23 +2330,6 @@ var BuilderImpl = class {
|
|
|
2279
2330
|
if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
|
|
2280
2331
|
return this._nodemap;
|
|
2281
2332
|
}
|
|
2282
|
-
/* ───── history ─────────────────────────────────────────────────────── */
|
|
2283
|
-
undo() {
|
|
2284
|
-
if (this.history.length === 0) return false;
|
|
2285
|
-
const prev = this.history.pop();
|
|
2286
|
-
this.future.push(structuredCloneSafe(this.props));
|
|
2287
|
-
this.props = prev;
|
|
2288
|
-
this.rebuildIndexes();
|
|
2289
|
-
return true;
|
|
2290
|
-
}
|
|
2291
|
-
redo() {
|
|
2292
|
-
if (this.future.length === 0) return false;
|
|
2293
|
-
const next = this.future.pop();
|
|
2294
|
-
this.pushHistory(this.props);
|
|
2295
|
-
this.props = next;
|
|
2296
|
-
this.rebuildIndexes();
|
|
2297
|
-
return true;
|
|
2298
|
-
}
|
|
2299
2333
|
/* ───── internals ──────────────────────────────────────────────────── */
|
|
2300
2334
|
rebuildIndexes() {
|
|
2301
2335
|
this.tagById.clear();
|
|
@@ -2311,99 +2345,429 @@ var BuilderImpl = class {
|
|
|
2311
2345
|
}
|
|
2312
2346
|
}
|
|
2313
2347
|
}
|
|
2314
|
-
pushHistory(state) {
|
|
2315
|
-
if (!state || !state.filters.length && !state.fields.length) return;
|
|
2316
|
-
this.history.push(structuredCloneSafe(state));
|
|
2317
|
-
if (this.history.length > this.historyLimit) this.history.shift();
|
|
2318
|
-
}
|
|
2319
2348
|
};
|
|
2320
|
-
function structuredCloneSafe(v) {
|
|
2321
|
-
if (typeof globalThis.structuredClone === "function") {
|
|
2322
|
-
return globalThis.structuredClone(v);
|
|
2323
|
-
}
|
|
2324
|
-
return JSON.parse(JSON.stringify(v));
|
|
2325
|
-
}
|
|
2326
2349
|
function toStringSet(v) {
|
|
2327
2350
|
if (!v) return /* @__PURE__ */ new Set();
|
|
2328
2351
|
if (v instanceof Set) return new Set(Array.from(v).map(String));
|
|
2329
2352
|
return new Set(v.map(String));
|
|
2330
2353
|
}
|
|
2331
2354
|
|
|
2332
|
-
// src/core/
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
const
|
|
2342
|
-
const
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
if (
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2355
|
+
// src/core/rate-coherence.ts
|
|
2356
|
+
function validateRateCoherenceDeep(params) {
|
|
2357
|
+
var _a, _b, _c;
|
|
2358
|
+
const { builder, services, tagId } = params;
|
|
2359
|
+
const ratePolicy = normalizeRatePolicy(params.ratePolicy);
|
|
2360
|
+
const props = builder.getProps();
|
|
2361
|
+
const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
|
|
2362
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
2363
|
+
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2364
|
+
const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
|
|
2365
|
+
const tag = tagById.get(tagId);
|
|
2366
|
+
const baselineFieldIds = builder.visibleFields(tagId, []);
|
|
2367
|
+
const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2368
|
+
const anchors = collectAnchors(baselineFields);
|
|
2369
|
+
const diagnostics = [];
|
|
2370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2371
|
+
for (const anchor of anchors) {
|
|
2372
|
+
const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
|
|
2373
|
+
const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2374
|
+
const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
|
|
2375
|
+
for (const fieldId of visibleInvalidFieldIds) {
|
|
2376
|
+
const key = `internal|${tagId}|${fieldId}`;
|
|
2377
|
+
if (seen.has(key)) continue;
|
|
2378
|
+
seen.add(key);
|
|
2379
|
+
diagnostics.push({
|
|
2380
|
+
kind: "internal_field",
|
|
2381
|
+
scope: "visible_group",
|
|
2382
|
+
tagId,
|
|
2383
|
+
fieldId,
|
|
2384
|
+
nodeId: fieldId,
|
|
2385
|
+
message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
|
|
2386
|
+
simulationAnchor: {
|
|
2387
|
+
kind: anchor.kind,
|
|
2388
|
+
id: anchor.id,
|
|
2389
|
+
fieldId: anchor.fieldId,
|
|
2390
|
+
label: anchor.label
|
|
2391
|
+
},
|
|
2392
|
+
invalidFieldIds: [fieldId]
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
const references = visibleFields.flatMap(
|
|
2396
|
+
(field) => collectFieldReferences(field, services)
|
|
2397
|
+
);
|
|
2398
|
+
if (references.length <= 1) continue;
|
|
2399
|
+
const primary = references.reduce((best, current) => {
|
|
2400
|
+
if (current.rate !== best.rate) {
|
|
2401
|
+
return current.rate > best.rate ? current : best;
|
|
2359
2402
|
}
|
|
2360
|
-
|
|
2403
|
+
const bestKey = `${best.fieldId}|${best.nodeId}`;
|
|
2404
|
+
const currentKey = `${current.fieldId}|${current.nodeId}`;
|
|
2405
|
+
return currentKey < bestKey ? current : best;
|
|
2406
|
+
});
|
|
2407
|
+
for (const candidate of references) {
|
|
2408
|
+
if (candidate.nodeId === primary.nodeId) continue;
|
|
2409
|
+
if (candidate.fieldId === primary.fieldId) continue;
|
|
2410
|
+
if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
const key = contextualKey(tagId, primary, candidate, ratePolicy);
|
|
2414
|
+
if (seen.has(key)) continue;
|
|
2415
|
+
seen.add(key);
|
|
2416
|
+
diagnostics.push({
|
|
2417
|
+
kind: "contextual",
|
|
2418
|
+
scope: "visible_group",
|
|
2419
|
+
tagId,
|
|
2420
|
+
nodeId: candidate.nodeId,
|
|
2421
|
+
primary: toDiagnosticRef(primary),
|
|
2422
|
+
offender: toDiagnosticRef(candidate),
|
|
2423
|
+
policy: ratePolicy.kind,
|
|
2424
|
+
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
2425
|
+
message: explainRateMismatch(
|
|
2426
|
+
ratePolicy,
|
|
2427
|
+
primary,
|
|
2428
|
+
candidate,
|
|
2429
|
+
describeLabel(tag)
|
|
2430
|
+
),
|
|
2431
|
+
simulationAnchor: {
|
|
2432
|
+
kind: anchor.kind,
|
|
2433
|
+
id: anchor.id,
|
|
2434
|
+
fieldId: anchor.fieldId,
|
|
2435
|
+
label: anchor.label
|
|
2436
|
+
},
|
|
2437
|
+
invalidFieldIds: visibleInvalidFieldIds
|
|
2438
|
+
});
|
|
2361
2439
|
}
|
|
2362
2440
|
}
|
|
2363
|
-
return
|
|
2441
|
+
return diagnostics;
|
|
2364
2442
|
}
|
|
2365
|
-
function
|
|
2366
|
-
var _a, _b
|
|
2367
|
-
const
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
reason: "no_primary"
|
|
2380
|
-
});
|
|
2443
|
+
function collectAnchors(fields) {
|
|
2444
|
+
var _a, _b;
|
|
2445
|
+
const anchors = [];
|
|
2446
|
+
for (const field of fields) {
|
|
2447
|
+
if (!isButton(field)) continue;
|
|
2448
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
2449
|
+
for (const option of field.options) {
|
|
2450
|
+
anchors.push({
|
|
2451
|
+
kind: "option",
|
|
2452
|
+
id: option.id,
|
|
2453
|
+
fieldId: field.id,
|
|
2454
|
+
label: (_a = option.label) != null ? _a : option.id
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2381
2457
|
continue;
|
|
2382
2458
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2459
|
+
anchors.push({
|
|
2460
|
+
kind: "field",
|
|
2461
|
+
id: field.id,
|
|
2462
|
+
fieldId: field.id,
|
|
2463
|
+
label: (_b = field.label) != null ? _b : field.id
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
return anchors;
|
|
2467
|
+
}
|
|
2468
|
+
function collectFieldReferences(field, services) {
|
|
2469
|
+
var _a;
|
|
2470
|
+
const members = collectBaseMembers(field, services);
|
|
2471
|
+
if (members.length === 0) return [];
|
|
2472
|
+
if (isMultiField(field)) {
|
|
2473
|
+
const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
|
|
2474
|
+
return [
|
|
2475
|
+
{
|
|
2476
|
+
refKind: "multi",
|
|
2477
|
+
nodeId: field.id,
|
|
2478
|
+
fieldId: field.id,
|
|
2479
|
+
label: (_a = field.label) != null ? _a : field.id,
|
|
2480
|
+
rate: averageRate,
|
|
2481
|
+
members
|
|
2482
|
+
}
|
|
2483
|
+
];
|
|
2484
|
+
}
|
|
2485
|
+
return members.map((member) => ({
|
|
2486
|
+
refKind: "single",
|
|
2487
|
+
nodeId: member.id,
|
|
2488
|
+
fieldId: field.id,
|
|
2489
|
+
label: member.label,
|
|
2490
|
+
rate: member.rate,
|
|
2491
|
+
service_id: member.service_id,
|
|
2492
|
+
members: [member]
|
|
2493
|
+
}));
|
|
2494
|
+
}
|
|
2495
|
+
function collectBaseMembers(field, services) {
|
|
2496
|
+
var _a, _b, _c;
|
|
2497
|
+
const members = [];
|
|
2498
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
2499
|
+
for (const option of field.options) {
|
|
2500
|
+
const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
|
|
2501
|
+
if (role2 !== "base") continue;
|
|
2502
|
+
if (option.service_id === void 0 || option.service_id === null) {
|
|
2393
2503
|
continue;
|
|
2394
2504
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
scope: "node",
|
|
2398
|
-
nodeId,
|
|
2399
|
-
primary,
|
|
2400
|
-
candidate: cand,
|
|
2401
|
-
reason: "cycle"
|
|
2402
|
-
});
|
|
2505
|
+
const cap2 = getServiceCapability(services, option.service_id);
|
|
2506
|
+
if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
|
|
2403
2507
|
continue;
|
|
2404
2508
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2509
|
+
members.push({
|
|
2510
|
+
kind: "option",
|
|
2511
|
+
id: option.id,
|
|
2512
|
+
fieldId: field.id,
|
|
2513
|
+
label: (_b = option.label) != null ? _b : option.id,
|
|
2514
|
+
service_id: option.service_id,
|
|
2515
|
+
rate: cap2.rate
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
return members;
|
|
2519
|
+
}
|
|
2520
|
+
const role = normalizeRole(field.pricing_role, "base");
|
|
2521
|
+
if (role !== "base") return members;
|
|
2522
|
+
if (field.service_id === void 0 || field.service_id === null) return members;
|
|
2523
|
+
const cap = getServiceCapability(services, field.service_id);
|
|
2524
|
+
if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
|
|
2525
|
+
return members;
|
|
2526
|
+
}
|
|
2527
|
+
members.push({
|
|
2528
|
+
kind: "field",
|
|
2529
|
+
id: field.id,
|
|
2530
|
+
fieldId: field.id,
|
|
2531
|
+
label: (_c = field.label) != null ? _c : field.id,
|
|
2532
|
+
service_id: field.service_id,
|
|
2533
|
+
rate: cap.rate
|
|
2534
|
+
});
|
|
2535
|
+
return members;
|
|
2536
|
+
}
|
|
2537
|
+
function isButton(field) {
|
|
2538
|
+
if (field.button === true) return true;
|
|
2539
|
+
return Array.isArray(field.options) && field.options.length > 0;
|
|
2540
|
+
}
|
|
2541
|
+
function normalizeRole(role, fallback) {
|
|
2542
|
+
return role === "base" || role === "utility" ? role : fallback;
|
|
2543
|
+
}
|
|
2544
|
+
function toDiagnosticRef(reference) {
|
|
2545
|
+
return {
|
|
2546
|
+
nodeId: reference.nodeId,
|
|
2547
|
+
fieldId: reference.fieldId,
|
|
2548
|
+
label: reference.label,
|
|
2549
|
+
refKind: reference.refKind,
|
|
2550
|
+
service_id: reference.service_id,
|
|
2551
|
+
rate: reference.rate
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
function contextualKey(tagId, primary, candidate, ratePolicy) {
|
|
2555
|
+
const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
|
|
2556
|
+
return [
|
|
2557
|
+
"contextual",
|
|
2558
|
+
tagId,
|
|
2559
|
+
primary.fieldId,
|
|
2560
|
+
primary.nodeId,
|
|
2561
|
+
candidate.fieldId,
|
|
2562
|
+
candidate.nodeId,
|
|
2563
|
+
`${ratePolicy.kind}${pctKey}`
|
|
2564
|
+
].join("|");
|
|
2565
|
+
}
|
|
2566
|
+
function describeLabel(tag) {
|
|
2567
|
+
var _a, _b;
|
|
2568
|
+
return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
|
|
2569
|
+
}
|
|
2570
|
+
function explainRateMismatch(policy, primary, candidate, where) {
|
|
2571
|
+
var _a, _b;
|
|
2572
|
+
const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
|
|
2573
|
+
const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
|
|
2574
|
+
switch (policy.kind) {
|
|
2575
|
+
case "eq_primary":
|
|
2576
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
|
|
2577
|
+
case "lte_primary":
|
|
2578
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
|
|
2579
|
+
case "within_pct":
|
|
2580
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
|
|
2581
|
+
case "at_least_pct_lower":
|
|
2582
|
+
return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/core/validate/index.ts
|
|
2587
|
+
function readVisibilitySimOpts(ctx) {
|
|
2588
|
+
const c = ctx;
|
|
2589
|
+
const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
|
|
2590
|
+
const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
|
|
2591
|
+
const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
|
|
2592
|
+
const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
|
|
2593
|
+
const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
|
|
2594
|
+
return {
|
|
2595
|
+
simulate,
|
|
2596
|
+
maxStates,
|
|
2597
|
+
maxDepth,
|
|
2598
|
+
simulateAllRoots,
|
|
2599
|
+
onlyEffectfulTriggers
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
function validate(props, ctx = {}) {
|
|
2603
|
+
var _a, _b, _c, _d;
|
|
2604
|
+
const errors = [];
|
|
2605
|
+
const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
|
|
2606
|
+
const selectedKeys = new Set(
|
|
2607
|
+
(_b = ctx.selectedOptionKeys) != null ? _b : []
|
|
2608
|
+
);
|
|
2609
|
+
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
2610
|
+
const fields = Array.isArray(props.fields) ? props.fields : [];
|
|
2611
|
+
const tagById = /* @__PURE__ */ new Map();
|
|
2612
|
+
const fieldById = /* @__PURE__ */ new Map();
|
|
2613
|
+
for (const t of tags) tagById.set(t.id, t);
|
|
2614
|
+
for (const f of fields) fieldById.set(f.id, f);
|
|
2615
|
+
const v = {
|
|
2616
|
+
props,
|
|
2617
|
+
nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
|
|
2618
|
+
options: ctx,
|
|
2619
|
+
errors,
|
|
2620
|
+
serviceMap,
|
|
2621
|
+
selectedKeys,
|
|
2622
|
+
tags,
|
|
2623
|
+
fields,
|
|
2624
|
+
invalidRateFieldIds: /* @__PURE__ */ new Set(),
|
|
2625
|
+
tagById,
|
|
2626
|
+
fieldById,
|
|
2627
|
+
fieldsVisibleUnder: (_tagId) => []
|
|
2628
|
+
};
|
|
2629
|
+
validateStructure(v);
|
|
2630
|
+
validateIdentity(v);
|
|
2631
|
+
validateOptionMaps(v);
|
|
2632
|
+
v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
|
|
2633
|
+
const visSim = readVisibilitySimOpts(ctx);
|
|
2634
|
+
validateVisibility(v, visSim);
|
|
2635
|
+
applyPolicies(
|
|
2636
|
+
v.errors,
|
|
2637
|
+
v.props,
|
|
2638
|
+
v.serviceMap,
|
|
2639
|
+
v.options.policies,
|
|
2640
|
+
v.fieldsVisibleUnder,
|
|
2641
|
+
v.tags
|
|
2642
|
+
);
|
|
2643
|
+
validateServiceVsUserInput(v);
|
|
2644
|
+
validateUtilityMarkers(v);
|
|
2645
|
+
validateRates(v);
|
|
2646
|
+
if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
|
|
2647
|
+
const builder = createBuilder({ serviceMap });
|
|
2648
|
+
builder.load(props);
|
|
2649
|
+
for (const tag of tags) {
|
|
2650
|
+
const diags = validateRateCoherenceDeep({
|
|
2651
|
+
builder,
|
|
2652
|
+
services: serviceMap,
|
|
2653
|
+
tagId: tag.id,
|
|
2654
|
+
ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
|
|
2655
|
+
invalidFieldIds: v.invalidRateFieldIds
|
|
2656
|
+
});
|
|
2657
|
+
for (const diag of diags) {
|
|
2658
|
+
if (diag.kind !== "contextual") continue;
|
|
2659
|
+
errors.push({
|
|
2660
|
+
code: "rate_coherence_violation",
|
|
2661
|
+
severity: "error",
|
|
2662
|
+
message: diag.message,
|
|
2663
|
+
nodeId: diag.nodeId,
|
|
2664
|
+
details: {
|
|
2665
|
+
tagId: diag.tagId,
|
|
2666
|
+
simulationAnchor: diag.simulationAnchor,
|
|
2667
|
+
primary: diag.primary,
|
|
2668
|
+
offender: diag.offender,
|
|
2669
|
+
policy: diag.policy,
|
|
2670
|
+
policyPct: diag.policyPct,
|
|
2671
|
+
invalidFieldIds: diag.invalidFieldIds
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
validateConstraints(v);
|
|
2678
|
+
validateCustomFields(v);
|
|
2679
|
+
validateGlobalUtilityGuard(v);
|
|
2680
|
+
validateUnboundFields(v);
|
|
2681
|
+
validateFallbacks(v);
|
|
2682
|
+
return v.errors;
|
|
2683
|
+
}
|
|
2684
|
+
async function validateAsync(props, ctx = {}) {
|
|
2685
|
+
await Promise.resolve();
|
|
2686
|
+
if (typeof requestAnimationFrame === "function") {
|
|
2687
|
+
await new Promise(
|
|
2688
|
+
(resolve) => requestAnimationFrame(() => resolve())
|
|
2689
|
+
);
|
|
2690
|
+
} else {
|
|
2691
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2692
|
+
}
|
|
2693
|
+
return validate(props, ctx);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// src/core/fallback.ts
|
|
2697
|
+
var DEFAULT_SETTINGS = {
|
|
2698
|
+
requireConstraintFit: true,
|
|
2699
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
2700
|
+
selectionStrategy: "priority",
|
|
2701
|
+
mode: "strict"
|
|
2702
|
+
};
|
|
2703
|
+
function resolveServiceFallback(params) {
|
|
2704
|
+
var _a, _b, _c, _d, _e;
|
|
2705
|
+
const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
|
|
2706
|
+
const { primary, nodeId, tagId, services } = params;
|
|
2707
|
+
const fb = (_b = params.fallbacks) != null ? _b : {};
|
|
2708
|
+
const tried = [];
|
|
2709
|
+
const lists = [];
|
|
2710
|
+
if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
|
|
2711
|
+
if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
|
|
2712
|
+
const primaryRate = rateOf(services, primary);
|
|
2713
|
+
for (const list of lists) {
|
|
2714
|
+
for (const cand of list) {
|
|
2715
|
+
if (tried.includes(cand)) continue;
|
|
2716
|
+
tried.push(cand);
|
|
2717
|
+
const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
|
|
2718
|
+
if (!candCap) continue;
|
|
2719
|
+
if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
|
|
2720
|
+
if (s.requireConstraintFit && tagId) {
|
|
2721
|
+
const ok = satisfiesTagConstraints(tagId, params, candCap);
|
|
2722
|
+
if (!ok) continue;
|
|
2723
|
+
}
|
|
2724
|
+
return cand;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return null;
|
|
2728
|
+
}
|
|
2729
|
+
function collectFailedFallbacks(props, services, settings) {
|
|
2730
|
+
var _a, _b, _c;
|
|
2731
|
+
const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
|
|
2732
|
+
const out = [];
|
|
2733
|
+
const fb = (_a = props.fallbacks) != null ? _a : {};
|
|
2734
|
+
const primaryRate = (p) => rateOf(services, p);
|
|
2735
|
+
for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
|
|
2736
|
+
const { primary, tagContexts } = primaryForNode(props, nodeId);
|
|
2737
|
+
if (!primary) {
|
|
2738
|
+
out.push({
|
|
2739
|
+
scope: "node",
|
|
2740
|
+
nodeId,
|
|
2741
|
+
primary: "",
|
|
2742
|
+
candidate: "",
|
|
2743
|
+
reason: "no_primary"
|
|
2744
|
+
});
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
for (const cand of list) {
|
|
2748
|
+
const cap = getCap(services, cand);
|
|
2749
|
+
if (!cap) {
|
|
2750
|
+
out.push({
|
|
2751
|
+
scope: "node",
|
|
2752
|
+
nodeId,
|
|
2753
|
+
primary,
|
|
2754
|
+
candidate: cand,
|
|
2755
|
+
reason: "unknown_service"
|
|
2756
|
+
});
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
if (String(cand) === String(primary)) {
|
|
2760
|
+
out.push({
|
|
2761
|
+
scope: "node",
|
|
2762
|
+
nodeId,
|
|
2763
|
+
primary,
|
|
2764
|
+
candidate: cand,
|
|
2765
|
+
reason: "cycle"
|
|
2766
|
+
});
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
|
|
2770
|
+
out.push({
|
|
2407
2771
|
scope: "node",
|
|
2408
2772
|
nodeId,
|
|
2409
2773
|
primary,
|
|
@@ -2487,27 +2851,10 @@ function passesRate(policy, primaryRate, candRate) {
|
|
|
2487
2851
|
return false;
|
|
2488
2852
|
if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
|
|
2489
2853
|
return false;
|
|
2490
|
-
|
|
2491
|
-
case "lte_primary":
|
|
2492
|
-
return candRate <= primaryRate;
|
|
2493
|
-
case "within_pct":
|
|
2494
|
-
return candRate <= primaryRate * (1 + policy.pct / 100);
|
|
2495
|
-
case "at_least_pct_lower":
|
|
2496
|
-
return candRate <= primaryRate * (1 - policy.pct / 100);
|
|
2497
|
-
}
|
|
2854
|
+
return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
|
|
2498
2855
|
}
|
|
2499
2856
|
function getCap(map, id) {
|
|
2500
|
-
|
|
2501
|
-
if (direct) return direct;
|
|
2502
|
-
const strKey = String(id);
|
|
2503
|
-
const byStr = map[strKey];
|
|
2504
|
-
if (byStr) return byStr;
|
|
2505
|
-
const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
|
|
2506
|
-
if (Number.isFinite(n)) {
|
|
2507
|
-
const byNum = map[n];
|
|
2508
|
-
if (byNum) return byNum;
|
|
2509
|
-
}
|
|
2510
|
-
return void 0;
|
|
2857
|
+
return getServiceCapability(map, id);
|
|
2511
2858
|
}
|
|
2512
2859
|
function isCapFlagEnabled(cap, flagId) {
|
|
2513
2860
|
var _a, _b;
|
|
@@ -2598,186 +2945,6 @@ function getFallbackRegistrationInfo(props, nodeId) {
|
|
|
2598
2945
|
return { primary, tagContexts };
|
|
2599
2946
|
}
|
|
2600
2947
|
|
|
2601
|
-
// src/core/rate-coherence.ts
|
|
2602
|
-
function validateRateCoherenceDeep(params) {
|
|
2603
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
2604
|
-
const { builder, services, tagId } = params;
|
|
2605
|
-
const ratePolicy = (_a = params.ratePolicy) != null ? _a : { kind: "lte_primary" };
|
|
2606
|
-
const props = builder.getProps();
|
|
2607
|
-
const fields = (_b = props.fields) != null ? _b : [];
|
|
2608
|
-
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2609
|
-
const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
|
|
2610
|
-
const tag = tagById.get(tagId);
|
|
2611
|
-
const baselineFieldIds = builder.visibleFields(tagId, []);
|
|
2612
|
-
const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2613
|
-
const anchors = [];
|
|
2614
|
-
for (const f of baselineFields) {
|
|
2615
|
-
if (!isButton(f)) continue;
|
|
2616
|
-
if (Array.isArray(f.options) && f.options.length) {
|
|
2617
|
-
for (const o of f.options) {
|
|
2618
|
-
anchors.push({
|
|
2619
|
-
kind: "option",
|
|
2620
|
-
id: o.id,
|
|
2621
|
-
fieldId: f.id,
|
|
2622
|
-
label: (_d = o.label) != null ? _d : o.id,
|
|
2623
|
-
service_id: numberOrUndefined(o.service_id)
|
|
2624
|
-
});
|
|
2625
|
-
}
|
|
2626
|
-
} else {
|
|
2627
|
-
anchors.push({
|
|
2628
|
-
kind: "field",
|
|
2629
|
-
id: f.id,
|
|
2630
|
-
fieldId: f.id,
|
|
2631
|
-
label: (_e = f.label) != null ? _e : f.id,
|
|
2632
|
-
service_id: numberOrUndefined(f.service_id)
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
const diags = [];
|
|
2637
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2638
|
-
for (const anchor of anchors) {
|
|
2639
|
-
const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
|
|
2640
|
-
const vgFieldIds = builder.visibleFields(tagId, selectedKeys);
|
|
2641
|
-
const vgFields = vgFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2642
|
-
const baseCandidates = [];
|
|
2643
|
-
for (const f of vgFields) {
|
|
2644
|
-
if (!isButton(f)) continue;
|
|
2645
|
-
if (Array.isArray(f.options) && f.options.length) {
|
|
2646
|
-
for (const o of f.options) {
|
|
2647
|
-
const sid = numberOrUndefined(o.service_id);
|
|
2648
|
-
const role = normalizeRole(o.pricing_role, "base");
|
|
2649
|
-
if (sid == null || role !== "base") continue;
|
|
2650
|
-
const r = rateOf2(services, sid);
|
|
2651
|
-
if (!isFiniteNumber2(r)) continue;
|
|
2652
|
-
baseCandidates.push({
|
|
2653
|
-
kind: "option",
|
|
2654
|
-
id: o.id,
|
|
2655
|
-
label: (_f = o.label) != null ? _f : o.id,
|
|
2656
|
-
service_id: sid,
|
|
2657
|
-
rate: r
|
|
2658
|
-
});
|
|
2659
|
-
}
|
|
2660
|
-
} else {
|
|
2661
|
-
const sid = numberOrUndefined(f.service_id);
|
|
2662
|
-
const role = normalizeRole(f.pricing_role, "base");
|
|
2663
|
-
if (sid == null || role !== "base") continue;
|
|
2664
|
-
const r = rateOf2(services, sid);
|
|
2665
|
-
if (!isFiniteNumber2(r)) continue;
|
|
2666
|
-
baseCandidates.push({
|
|
2667
|
-
kind: "field",
|
|
2668
|
-
id: f.id,
|
|
2669
|
-
label: (_g = f.label) != null ? _g : f.id,
|
|
2670
|
-
service_id: sid,
|
|
2671
|
-
rate: r
|
|
2672
|
-
});
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
if (baseCandidates.length === 0) continue;
|
|
2676
|
-
const anchorPrimary = anchor.service_id != null ? pickByServiceId(baseCandidates, anchor.service_id) : void 0;
|
|
2677
|
-
const primary = anchorPrimary ? anchorPrimary : baseCandidates[0];
|
|
2678
|
-
for (const cand of baseCandidates) {
|
|
2679
|
-
if (sameService(primary, cand)) continue;
|
|
2680
|
-
if (!rateOkWithPolicy(ratePolicy, cand.rate, primary.rate)) {
|
|
2681
|
-
const key = dedupeKey(tagId, anchor, primary, cand, ratePolicy);
|
|
2682
|
-
if (seen.has(key)) continue;
|
|
2683
|
-
seen.add(key);
|
|
2684
|
-
diags.push({
|
|
2685
|
-
scope: "visible_group",
|
|
2686
|
-
tagId,
|
|
2687
|
-
primary,
|
|
2688
|
-
offender: {
|
|
2689
|
-
kind: cand.kind,
|
|
2690
|
-
id: cand.id,
|
|
2691
|
-
label: cand.label,
|
|
2692
|
-
service_id: cand.service_id,
|
|
2693
|
-
rate: cand.rate
|
|
2694
|
-
},
|
|
2695
|
-
policy: ratePolicy.kind,
|
|
2696
|
-
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
2697
|
-
message: explainRateMismatch(
|
|
2698
|
-
ratePolicy,
|
|
2699
|
-
primary.rate,
|
|
2700
|
-
cand.rate,
|
|
2701
|
-
describeLabel(tag)
|
|
2702
|
-
),
|
|
2703
|
-
simulationAnchor: {
|
|
2704
|
-
kind: anchor.kind,
|
|
2705
|
-
id: anchor.id,
|
|
2706
|
-
fieldId: anchor.fieldId,
|
|
2707
|
-
label: anchor.label
|
|
2708
|
-
}
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
return diags;
|
|
2714
|
-
}
|
|
2715
|
-
function isButton(f) {
|
|
2716
|
-
if (f.button === true) return true;
|
|
2717
|
-
return Array.isArray(f.options) && f.options.length > 0;
|
|
2718
|
-
}
|
|
2719
|
-
function normalizeRole(role, d) {
|
|
2720
|
-
return role === "utility" || role === "base" ? role : d;
|
|
2721
|
-
}
|
|
2722
|
-
function numberOrUndefined(v) {
|
|
2723
|
-
const n = Number(v);
|
|
2724
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2725
|
-
}
|
|
2726
|
-
function isFiniteNumber2(v) {
|
|
2727
|
-
return typeof v === "number" && Number.isFinite(v);
|
|
2728
|
-
}
|
|
2729
|
-
function rateOf2(map, id) {
|
|
2730
|
-
var _a;
|
|
2731
|
-
if (id === void 0 || id === null) return void 0;
|
|
2732
|
-
const cap = (_a = map[Number(id)]) != null ? _a : map[id];
|
|
2733
|
-
return cap == null ? void 0 : cap.rate;
|
|
2734
|
-
}
|
|
2735
|
-
function pickByServiceId(arr, sid) {
|
|
2736
|
-
return arr.find((x) => x.service_id === sid);
|
|
2737
|
-
}
|
|
2738
|
-
function sameService(a, b) {
|
|
2739
|
-
return a.service_id === b.service_id;
|
|
2740
|
-
}
|
|
2741
|
-
function rateOkWithPolicy(policy, candRate, primaryRate) {
|
|
2742
|
-
var _a, _b;
|
|
2743
|
-
const rp = policy != null ? policy : { kind: "lte_primary" };
|
|
2744
|
-
switch (rp.kind) {
|
|
2745
|
-
case "lte_primary":
|
|
2746
|
-
return candRate <= primaryRate;
|
|
2747
|
-
case "within_pct": {
|
|
2748
|
-
const pct = Math.max(0, (_a = rp.pct) != null ? _a : 0);
|
|
2749
|
-
return candRate <= primaryRate * (1 + pct / 100);
|
|
2750
|
-
}
|
|
2751
|
-
case "at_least_pct_lower": {
|
|
2752
|
-
const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
|
|
2753
|
-
return candRate <= primaryRate * (1 - pct / 100);
|
|
2754
|
-
}
|
|
2755
|
-
default:
|
|
2756
|
-
return candRate <= primaryRate;
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
function describeLabel(tag) {
|
|
2760
|
-
var _a, _b;
|
|
2761
|
-
const tagName = (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
|
|
2762
|
-
return `${tagName}`;
|
|
2763
|
-
}
|
|
2764
|
-
function explainRateMismatch(policy, primary, candidate, where) {
|
|
2765
|
-
switch (policy.kind) {
|
|
2766
|
-
case "lte_primary":
|
|
2767
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be \u2264 primary ${primary}.`;
|
|
2768
|
-
case "within_pct":
|
|
2769
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be within ${policy.pct}% of primary ${primary}.`;
|
|
2770
|
-
case "at_least_pct_lower":
|
|
2771
|
-
return `Rate coherence failed (${where}): candidate ${candidate} must be at least ${policy.pct}% lower than primary ${primary}.`;
|
|
2772
|
-
default:
|
|
2773
|
-
return `Rate coherence failed (${where}): candidate ${candidate} mismatches primary ${primary}.`;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
function dedupeKey(tagId, anchor, primary, cand, rp) {
|
|
2777
|
-
const rpKey = rp.kind + ("pct" in rp && typeof rp.pct === "number" ? `:${rp.pct}` : "");
|
|
2778
|
-
return `${tagId}|${anchor.kind}:${anchor.id}|p${primary.service_id}|c${cand.service_id}:${cand.id}|${rpKey}`;
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
2948
|
// src/core/tag-relations.ts
|
|
2782
2949
|
var toId = (x) => typeof x === "string" ? x : x.id;
|
|
2783
2950
|
var toBindList = (b) => {
|
|
@@ -3110,17 +3277,16 @@ function createNodeIndex(builder) {
|
|
|
3110
3277
|
return node;
|
|
3111
3278
|
};
|
|
3112
3279
|
const getNode = (input) => {
|
|
3113
|
-
var _a2, _b2, _c2
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
|
|
3117
|
-
if ("type" in input)
|
|
3118
|
-
return (_b2 = getField(input.id)) != null ? _b2 : mkUnknown(input.id);
|
|
3119
|
-
return (_c2 = getOption(input.id)) != null ? _c2 : mkUnknown(input.id);
|
|
3120
|
-
}
|
|
3121
|
-
const cached = nodeCache.get(input);
|
|
3280
|
+
var _a2, _b2, _c2;
|
|
3281
|
+
const id = typeof input === "object" ? input.id : input;
|
|
3282
|
+
const cached = nodeCache.get(id);
|
|
3122
3283
|
if (cached) return cached;
|
|
3123
|
-
|
|
3284
|
+
const node = nodeMap.get(id);
|
|
3285
|
+
if (!node) return mkUnknown(id);
|
|
3286
|
+
if (node.kind === "tag") return (_a2 = getTag(id)) != null ? _a2 : mkUnknown(id);
|
|
3287
|
+
if (node.kind == "field") return (_b2 = getField(id)) != null ? _b2 : mkUnknown(id);
|
|
3288
|
+
if (node.kind == "option") return (_c2 = getOption(id)) != null ? _c2 : mkUnknown(id);
|
|
3289
|
+
return mkUnknown(id);
|
|
3124
3290
|
};
|
|
3125
3291
|
const mkUnknown = (id) => {
|
|
3126
3292
|
const u = {
|
|
@@ -3141,6 +3307,451 @@ function createNodeIndex(builder) {
|
|
|
3141
3307
|
};
|
|
3142
3308
|
}
|
|
3143
3309
|
|
|
3310
|
+
// src/core/policy.ts
|
|
3311
|
+
var ALLOWED_SCOPES = /* @__PURE__ */ new Set([
|
|
3312
|
+
"global",
|
|
3313
|
+
"visible_group"
|
|
3314
|
+
]);
|
|
3315
|
+
var ALLOWED_SUBJECTS = /* @__PURE__ */ new Set(["services"]);
|
|
3316
|
+
var ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
3317
|
+
"all_equal",
|
|
3318
|
+
"unique",
|
|
3319
|
+
"no_mix",
|
|
3320
|
+
"all_true",
|
|
3321
|
+
"any_true",
|
|
3322
|
+
"max_count",
|
|
3323
|
+
"min_count"
|
|
3324
|
+
]);
|
|
3325
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
3326
|
+
"base",
|
|
3327
|
+
"utility",
|
|
3328
|
+
"both"
|
|
3329
|
+
]);
|
|
3330
|
+
var ALLOWED_SEVERITIES = /* @__PURE__ */ new Set([
|
|
3331
|
+
"error",
|
|
3332
|
+
"warning"
|
|
3333
|
+
]);
|
|
3334
|
+
var ALLOWED_WHERE_OPS = /* @__PURE__ */ new Set(["eq", "neq", "in", "nin", "exists", "truthy", "falsy"]);
|
|
3335
|
+
function normaliseWhere(src, d, i, id) {
|
|
3336
|
+
if (src === void 0) return void 0;
|
|
3337
|
+
if (!Array.isArray(src)) {
|
|
3338
|
+
d.push({
|
|
3339
|
+
ruleIndex: i,
|
|
3340
|
+
ruleId: id,
|
|
3341
|
+
severity: "warning",
|
|
3342
|
+
message: "filter.where must be an array; ignored.",
|
|
3343
|
+
path: "filter.where"
|
|
3344
|
+
});
|
|
3345
|
+
return void 0;
|
|
3346
|
+
}
|
|
3347
|
+
const out = [];
|
|
3348
|
+
src.forEach((raw, j) => {
|
|
3349
|
+
const obj = raw && typeof raw === "object" ? raw : null;
|
|
3350
|
+
const path = typeof (obj == null ? void 0 : obj.path) === "string" && obj.path.trim() ? obj.path.trim() : void 0;
|
|
3351
|
+
if (!path) {
|
|
3352
|
+
d.push({
|
|
3353
|
+
ruleIndex: i,
|
|
3354
|
+
ruleId: id,
|
|
3355
|
+
severity: "warning",
|
|
3356
|
+
message: `filter.where[${j}].path must be a non-empty string; entry ignored.`,
|
|
3357
|
+
path: `filter.where[${j}].path`
|
|
3358
|
+
});
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
if (!path.startsWith("service.")) {
|
|
3362
|
+
d.push({
|
|
3363
|
+
ruleIndex: i,
|
|
3364
|
+
ruleId: id,
|
|
3365
|
+
severity: "warning",
|
|
3366
|
+
message: `filter.where[${j}].path should start with "service." for subject "services".`,
|
|
3367
|
+
path: `filter.where[${j}].path`
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
const opRaw = obj == null ? void 0 : obj.op;
|
|
3371
|
+
const op = opRaw === void 0 ? "eq" : typeof opRaw === "string" && ALLOWED_WHERE_OPS.has(opRaw) ? opRaw : "eq";
|
|
3372
|
+
if (opRaw !== void 0 && !(typeof opRaw === "string" && ALLOWED_WHERE_OPS.has(opRaw))) {
|
|
3373
|
+
d.push({
|
|
3374
|
+
ruleIndex: i,
|
|
3375
|
+
ruleId: id,
|
|
3376
|
+
severity: "warning",
|
|
3377
|
+
message: `Unknown filter.where[${j}].op; defaulted to "eq".`,
|
|
3378
|
+
path: `filter.where[${j}].op`
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
const value = obj == null ? void 0 : obj.value;
|
|
3382
|
+
if (op === "exists" || op === "truthy" || op === "falsy") {
|
|
3383
|
+
if (value !== void 0) {
|
|
3384
|
+
d.push({
|
|
3385
|
+
ruleIndex: i,
|
|
3386
|
+
ruleId: id,
|
|
3387
|
+
severity: "warning",
|
|
3388
|
+
message: `filter.where[${j}] op "${op}" does not use "value".`,
|
|
3389
|
+
path: `filter.where[${j}].value`
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
} else if (op === "in" || op === "nin") {
|
|
3393
|
+
if (!Array.isArray(value)) {
|
|
3394
|
+
d.push({
|
|
3395
|
+
ruleIndex: i,
|
|
3396
|
+
ruleId: id,
|
|
3397
|
+
severity: "warning",
|
|
3398
|
+
message: `filter.where[${j}] op "${op}" expects an array "value".`,
|
|
3399
|
+
path: `filter.where[${j}].value`
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
out.push({ path, op, value });
|
|
3404
|
+
});
|
|
3405
|
+
return out.length ? out : void 0;
|
|
3406
|
+
}
|
|
3407
|
+
function compilePolicies(raw) {
|
|
3408
|
+
const diagnostics = [];
|
|
3409
|
+
const policies = [];
|
|
3410
|
+
if (!Array.isArray(raw)) {
|
|
3411
|
+
diagnostics.push({
|
|
3412
|
+
ruleIndex: -1,
|
|
3413
|
+
severity: "error",
|
|
3414
|
+
message: "Policies root must be an array."
|
|
3415
|
+
});
|
|
3416
|
+
return { policies, diagnostics };
|
|
3417
|
+
}
|
|
3418
|
+
raw.forEach((entry, i) => {
|
|
3419
|
+
const d = [];
|
|
3420
|
+
const src = entry && typeof entry === "object" ? entry : {};
|
|
3421
|
+
let id = typeof src.id === "string" && src.id.trim() ? src.id.trim() : void 0;
|
|
3422
|
+
if (!id) {
|
|
3423
|
+
id = `policy_${i + 1}`;
|
|
3424
|
+
d.push({
|
|
3425
|
+
ruleIndex: i,
|
|
3426
|
+
ruleId: id,
|
|
3427
|
+
severity: "warning",
|
|
3428
|
+
message: 'Missing "id"; generated automatically.',
|
|
3429
|
+
path: "id"
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
const label = typeof src.label === "string" && src.label.trim() ? src.label.trim() : id;
|
|
3433
|
+
if (!(typeof src.label === "string" && src.label.trim())) {
|
|
3434
|
+
d.push({
|
|
3435
|
+
ruleIndex: i,
|
|
3436
|
+
ruleId: id,
|
|
3437
|
+
severity: "warning",
|
|
3438
|
+
message: 'Missing "label"; defaulted to rule id.',
|
|
3439
|
+
path: "label"
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
let scope = ALLOWED_SCOPES.has(src.scope) ? src.scope : src.scope === void 0 ? "visible_group" : "visible_group";
|
|
3443
|
+
if (src.scope !== void 0 && !ALLOWED_SCOPES.has(src.scope)) {
|
|
3444
|
+
d.push({
|
|
3445
|
+
ruleIndex: i,
|
|
3446
|
+
ruleId: id,
|
|
3447
|
+
severity: "warning",
|
|
3448
|
+
message: 'Unknown "scope"; defaulted to "visible_group".',
|
|
3449
|
+
path: "scope"
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
let subject = ALLOWED_SUBJECTS.has(src.subject) ? src.subject : "services";
|
|
3453
|
+
if (src.subject !== void 0 && !ALLOWED_SUBJECTS.has(src.subject)) {
|
|
3454
|
+
d.push({
|
|
3455
|
+
ruleIndex: i,
|
|
3456
|
+
ruleId: id,
|
|
3457
|
+
severity: "warning",
|
|
3458
|
+
message: 'Unknown "subject"; defaulted to "services".',
|
|
3459
|
+
path: "subject"
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
const op = src.op;
|
|
3463
|
+
if (!ALLOWED_OPS.has(op)) {
|
|
3464
|
+
d.push({
|
|
3465
|
+
ruleIndex: i,
|
|
3466
|
+
ruleId: id,
|
|
3467
|
+
severity: "error",
|
|
3468
|
+
message: `Invalid "op": ${String(op)}.`,
|
|
3469
|
+
path: "op"
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
let projection = typeof src.projection === "string" && src.projection.trim() ? src.projection.trim() : "service.id";
|
|
3473
|
+
if (subject === "services" && projection && !projection.startsWith("service.")) {
|
|
3474
|
+
d.push({
|
|
3475
|
+
ruleIndex: i,
|
|
3476
|
+
ruleId: id,
|
|
3477
|
+
severity: "warning",
|
|
3478
|
+
message: 'Projection should start with "service." for subject "services".',
|
|
3479
|
+
path: "projection"
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
const filterSrc = src.filter && typeof src.filter === "object" ? src.filter : void 0;
|
|
3483
|
+
const role = (filterSrc == null ? void 0 : filterSrc.role) && ALLOWED_ROLES.has(filterSrc.role) ? filterSrc.role : "both";
|
|
3484
|
+
if ((filterSrc == null ? void 0 : filterSrc.role) && !ALLOWED_ROLES.has(filterSrc.role)) {
|
|
3485
|
+
d.push({
|
|
3486
|
+
ruleIndex: i,
|
|
3487
|
+
ruleId: id,
|
|
3488
|
+
severity: "warning",
|
|
3489
|
+
message: 'Unknown filter.role; defaulted to "both".',
|
|
3490
|
+
path: "filter.role"
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
const filter = {
|
|
3494
|
+
role,
|
|
3495
|
+
tag_id: (filterSrc == null ? void 0 : filterSrc.tag_id) !== void 0 ? Array.isArray(filterSrc.tag_id) ? filterSrc.tag_id : [filterSrc.tag_id] : void 0,
|
|
3496
|
+
field_id: (filterSrc == null ? void 0 : filterSrc.field_id) !== void 0 ? Array.isArray(filterSrc.field_id) ? filterSrc.field_id : [filterSrc.field_id] : void 0,
|
|
3497
|
+
where: normaliseWhere(filterSrc == null ? void 0 : filterSrc.where, d, i, id)
|
|
3498
|
+
};
|
|
3499
|
+
const severity = ALLOWED_SEVERITIES.has(src.severity) ? src.severity : "error";
|
|
3500
|
+
if (src.severity !== void 0 && !ALLOWED_SEVERITIES.has(src.severity)) {
|
|
3501
|
+
d.push({
|
|
3502
|
+
ruleIndex: i,
|
|
3503
|
+
ruleId: id,
|
|
3504
|
+
severity: "warning",
|
|
3505
|
+
message: 'Unknown "severity"; defaulted to "error".',
|
|
3506
|
+
path: "severity"
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
const value = src.value;
|
|
3510
|
+
if (op === "max_count" || op === "min_count") {
|
|
3511
|
+
if (!(typeof value === "number" && Number.isFinite(value))) {
|
|
3512
|
+
d.push({
|
|
3513
|
+
ruleIndex: i,
|
|
3514
|
+
ruleId: id,
|
|
3515
|
+
severity: "error",
|
|
3516
|
+
message: `"${op}" requires numeric "value".`,
|
|
3517
|
+
path: "value"
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
} else if (op === "all_true" || op === "any_true") {
|
|
3521
|
+
if (value !== void 0) {
|
|
3522
|
+
d.push({
|
|
3523
|
+
ruleIndex: i,
|
|
3524
|
+
ruleId: id,
|
|
3525
|
+
severity: "warning",
|
|
3526
|
+
message: `"${op}" ignores "value"; it checks all/any true.`,
|
|
3527
|
+
path: "value"
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
} else {
|
|
3531
|
+
if (value !== void 0) {
|
|
3532
|
+
d.push({
|
|
3533
|
+
ruleIndex: i,
|
|
3534
|
+
ruleId: id,
|
|
3535
|
+
severity: "warning",
|
|
3536
|
+
message: `"${op}" does not use "value".`,
|
|
3537
|
+
path: "value"
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
const hasFatal = d.some((x) => x.severity === "error");
|
|
3542
|
+
if (!hasFatal) {
|
|
3543
|
+
const rule = {
|
|
3544
|
+
id,
|
|
3545
|
+
label,
|
|
3546
|
+
// ✅ now always present
|
|
3547
|
+
scope,
|
|
3548
|
+
subject,
|
|
3549
|
+
filter,
|
|
3550
|
+
projection,
|
|
3551
|
+
op,
|
|
3552
|
+
value,
|
|
3553
|
+
severity,
|
|
3554
|
+
message: typeof src.message === "string" ? src.message : void 0
|
|
3555
|
+
};
|
|
3556
|
+
policies.push(rule);
|
|
3557
|
+
}
|
|
3558
|
+
diagnostics.push(...d);
|
|
3559
|
+
});
|
|
3560
|
+
return { policies, diagnostics };
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/core/service-filter.ts
|
|
3564
|
+
function filterServicesForVisibleGroup(input, deps) {
|
|
3565
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3566
|
+
const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
|
|
3567
|
+
const { context } = input;
|
|
3568
|
+
const usedSet = new Set(context.usedServiceIds.map(String));
|
|
3569
|
+
const primary = context.usedServiceIds[0];
|
|
3570
|
+
const fb = {
|
|
3571
|
+
requireConstraintFit: true,
|
|
3572
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
3573
|
+
selectionStrategy: "priority",
|
|
3574
|
+
mode: "strict",
|
|
3575
|
+
...(_d = context.fallback) != null ? _d : {}
|
|
3576
|
+
};
|
|
3577
|
+
const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
|
|
3578
|
+
deps.builder,
|
|
3579
|
+
context.tagId,
|
|
3580
|
+
context.selectedButtons
|
|
3581
|
+
);
|
|
3582
|
+
const checks = [];
|
|
3583
|
+
let lastDiagnostics = void 0;
|
|
3584
|
+
for (const id of input.candidates) {
|
|
3585
|
+
if (usedSet.has(String(id))) continue;
|
|
3586
|
+
const cap = getServiceCapability(svcMap, id);
|
|
3587
|
+
if (!cap) {
|
|
3588
|
+
checks.push({
|
|
3589
|
+
id,
|
|
3590
|
+
ok: false,
|
|
3591
|
+
fitsConstraints: false,
|
|
3592
|
+
passesRate: false,
|
|
3593
|
+
passesPolicies: false,
|
|
3594
|
+
reasons: ["missing_capability"]
|
|
3595
|
+
});
|
|
3596
|
+
continue;
|
|
3597
|
+
}
|
|
3598
|
+
const fitsConstraints = constraintFitOk(
|
|
3599
|
+
svcMap,
|
|
3600
|
+
cap.id,
|
|
3601
|
+
(_e = context.effectiveConstraints) != null ? _e : {}
|
|
3602
|
+
);
|
|
3603
|
+
const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
|
|
3604
|
+
const polRes = evaluatePoliciesRaw(
|
|
3605
|
+
(_f = context.policies) != null ? _f : [],
|
|
3606
|
+
[...context.usedServiceIds, id],
|
|
3607
|
+
svcMap,
|
|
3608
|
+
context.tagId,
|
|
3609
|
+
visibleServiceIds
|
|
3610
|
+
);
|
|
3611
|
+
const passesPolicies = polRes.ok;
|
|
3612
|
+
lastDiagnostics = polRes.diagnostics;
|
|
3613
|
+
const reasons = [];
|
|
3614
|
+
if (!fitsConstraints) reasons.push("constraint_mismatch");
|
|
3615
|
+
if (!passesRate2) reasons.push("rate_policy");
|
|
3616
|
+
if (!passesPolicies) reasons.push("policy_error");
|
|
3617
|
+
checks.push({
|
|
3618
|
+
id,
|
|
3619
|
+
ok: fitsConstraints && passesRate2 && passesPolicies,
|
|
3620
|
+
fitsConstraints,
|
|
3621
|
+
passesRate: passesRate2,
|
|
3622
|
+
passesPolicies,
|
|
3623
|
+
policyErrors: polRes.errors.length ? polRes.errors : void 0,
|
|
3624
|
+
policyWarnings: polRes.warnings.length ? polRes.warnings : void 0,
|
|
3625
|
+
reasons,
|
|
3626
|
+
cap,
|
|
3627
|
+
rate: toFiniteNumber(cap.rate)
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
return {
|
|
3631
|
+
checks,
|
|
3632
|
+
diagnostics: lastDiagnostics && lastDiagnostics.length ? lastDiagnostics : void 0
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
function evaluatePoliciesRaw(raw, serviceIds, svcMap, tagId, visibleServiceIds) {
|
|
3636
|
+
const compiled = compilePolicies(raw);
|
|
3637
|
+
const evaluated = evaluateServicePolicies(
|
|
3638
|
+
compiled.policies,
|
|
3639
|
+
serviceIds,
|
|
3640
|
+
svcMap,
|
|
3641
|
+
tagId,
|
|
3642
|
+
visibleServiceIds
|
|
3643
|
+
);
|
|
3644
|
+
return {
|
|
3645
|
+
...evaluated,
|
|
3646
|
+
diagnostics: compiled.diagnostics
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
function evaluateServicePolicies(rules, svcIds, svcMap, tagId, visibleServiceIds) {
|
|
3650
|
+
var _a, _b, _c;
|
|
3651
|
+
const errors = [];
|
|
3652
|
+
const warnings = [];
|
|
3653
|
+
if (!rules || !rules.length) return { ok: true, errors, warnings };
|
|
3654
|
+
const relevant = rules.filter(
|
|
3655
|
+
(r) => r.subject === "services" && (r.scope === "visible_group" || r.scope === "global")
|
|
3656
|
+
);
|
|
3657
|
+
for (const r of relevant) {
|
|
3658
|
+
const scoped = scopeServiceIdsForRule(svcIds, r, visibleServiceIds);
|
|
3659
|
+
const ids = scoped.filter(
|
|
3660
|
+
(id) => matchesRuleFilter(getServiceCapability(svcMap, id), r, tagId)
|
|
3661
|
+
);
|
|
3662
|
+
const projection = r.projection || "service.id";
|
|
3663
|
+
const values = ids.map(
|
|
3664
|
+
(id) => policyProjectValue(getServiceCapability(svcMap, id), projection)
|
|
3665
|
+
);
|
|
3666
|
+
let ok = true;
|
|
3667
|
+
switch (r.op) {
|
|
3668
|
+
case "all_equal":
|
|
3669
|
+
ok = values.length <= 1 || values.every((v) => v === values[0]);
|
|
3670
|
+
break;
|
|
3671
|
+
case "unique": {
|
|
3672
|
+
const uniq2 = new Set(values.map((v) => String(v)));
|
|
3673
|
+
ok = uniq2.size === values.length;
|
|
3674
|
+
break;
|
|
3675
|
+
}
|
|
3676
|
+
case "no_mix": {
|
|
3677
|
+
const uniq2 = new Set(values.map((v) => String(v)));
|
|
3678
|
+
ok = uniq2.size <= 1;
|
|
3679
|
+
break;
|
|
3680
|
+
}
|
|
3681
|
+
case "all_true":
|
|
3682
|
+
ok = values.every((v) => !!v);
|
|
3683
|
+
break;
|
|
3684
|
+
case "any_true":
|
|
3685
|
+
ok = values.some((v) => !!v);
|
|
3686
|
+
break;
|
|
3687
|
+
case "max_count": {
|
|
3688
|
+
const n = typeof r.value === "number" ? r.value : NaN;
|
|
3689
|
+
ok = Number.isFinite(n) ? values.length <= n : true;
|
|
3690
|
+
break;
|
|
3691
|
+
}
|
|
3692
|
+
case "min_count": {
|
|
3693
|
+
const n = typeof r.value === "number" ? r.value : NaN;
|
|
3694
|
+
ok = Number.isFinite(n) ? values.length >= n : true;
|
|
3695
|
+
break;
|
|
3696
|
+
}
|
|
3697
|
+
default:
|
|
3698
|
+
ok = true;
|
|
3699
|
+
}
|
|
3700
|
+
if (!ok) {
|
|
3701
|
+
if (((_a = r.severity) != null ? _a : "error") === "error") {
|
|
3702
|
+
errors.push((_b = r.id) != null ? _b : "policy_error");
|
|
3703
|
+
} else {
|
|
3704
|
+
warnings.push((_c = r.id) != null ? _c : "policy_warning");
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
3709
|
+
}
|
|
3710
|
+
function scopeServiceIdsForRule(serviceIds, rule, visibleServiceIds) {
|
|
3711
|
+
if (rule.scope !== "visible_group" || !visibleServiceIds) return serviceIds;
|
|
3712
|
+
return serviceIds.filter((id) => visibleServiceIds.has(String(id)));
|
|
3713
|
+
}
|
|
3714
|
+
function collectVisibleServiceIds(builder, tagId, selectedButtons) {
|
|
3715
|
+
var _a, _b, _c;
|
|
3716
|
+
const out = /* @__PURE__ */ new Set();
|
|
3717
|
+
const props = builder.getProps();
|
|
3718
|
+
const tags = (_a = props.filters) != null ? _a : [];
|
|
3719
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
3720
|
+
const tag = tags.find((t) => t.id === tagId);
|
|
3721
|
+
if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
|
|
3722
|
+
const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
|
|
3723
|
+
for (const field of fields) {
|
|
3724
|
+
if (!visibleFieldIds.has(field.id)) continue;
|
|
3725
|
+
if (field.service_id != null) {
|
|
3726
|
+
out.add(String(field.service_id));
|
|
3727
|
+
}
|
|
3728
|
+
for (const option of (_c = field.options) != null ? _c : []) {
|
|
3729
|
+
if (option.service_id != null) {
|
|
3730
|
+
out.add(String(option.service_id));
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return out;
|
|
3735
|
+
}
|
|
3736
|
+
function policyProjectValue(cap, projection) {
|
|
3737
|
+
if (!cap) return void 0;
|
|
3738
|
+
const key = projection.startsWith("service.") ? projection.slice(8) : projection;
|
|
3739
|
+
return cap[key];
|
|
3740
|
+
}
|
|
3741
|
+
function matchesRuleFilter(cap, rule, tagId) {
|
|
3742
|
+
if (!cap) return false;
|
|
3743
|
+
const f = rule.filter;
|
|
3744
|
+
if (!f) return true;
|
|
3745
|
+
if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
|
|
3746
|
+
return true;
|
|
3747
|
+
}
|
|
3748
|
+
function toStrSet(v) {
|
|
3749
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
3750
|
+
const s = /* @__PURE__ */ new Set();
|
|
3751
|
+
for (const x of arr) s.add(String(x));
|
|
3752
|
+
return s;
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3144
3755
|
// src/utils/prune-fallbacks.ts
|
|
3145
3756
|
function pruneInvalidNodeFallbacks(props, services, settings) {
|
|
3146
3757
|
var _a, _b;
|
|
@@ -3227,43 +3838,6 @@ function toBindArray(bind) {
|
|
|
3227
3838
|
return Array.isArray(bind) ? bind.slice() : [bind];
|
|
3228
3839
|
}
|
|
3229
3840
|
|
|
3230
|
-
// src/utils/util.ts
|
|
3231
|
-
function toFiniteNumber(v) {
|
|
3232
|
-
const n = Number(v);
|
|
3233
|
-
return Number.isFinite(n) ? n : NaN;
|
|
3234
|
-
}
|
|
3235
|
-
function constraintFitOk(svcMap, candidate, constraints) {
|
|
3236
|
-
const cap = svcMap[Number(candidate)];
|
|
3237
|
-
if (!cap) return false;
|
|
3238
|
-
if (constraints.dripfeed === true && !cap.dripfeed) return false;
|
|
3239
|
-
if (constraints.refill === true && !cap.refill) return false;
|
|
3240
|
-
return !(constraints.cancel === true && !cap.cancel);
|
|
3241
|
-
}
|
|
3242
|
-
function rateOk(svcMap, candidate, primary, policy) {
|
|
3243
|
-
var _a, _b, _c;
|
|
3244
|
-
const cand = svcMap[Number(candidate)];
|
|
3245
|
-
const prim = svcMap[Number(primary)];
|
|
3246
|
-
if (!cand || !prim) return false;
|
|
3247
|
-
const cRate = toFiniteNumber(cand.rate);
|
|
3248
|
-
const pRate = toFiniteNumber(prim.rate);
|
|
3249
|
-
if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
|
|
3250
|
-
const rp = (_a = policy.ratePolicy) != null ? _a : { kind: "lte_primary" };
|
|
3251
|
-
switch (rp.kind) {
|
|
3252
|
-
case "lte_primary":
|
|
3253
|
-
return cRate <= pRate;
|
|
3254
|
-
case "within_pct": {
|
|
3255
|
-
const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
|
|
3256
|
-
return cRate <= pRate * (1 + pct / 100);
|
|
3257
|
-
}
|
|
3258
|
-
case "at_least_pct_lower": {
|
|
3259
|
-
const pct = Math.max(0, (_c = rp.pct) != null ? _c : 0);
|
|
3260
|
-
return cRate <= pRate * (1 - pct / 100);
|
|
3261
|
-
}
|
|
3262
|
-
default:
|
|
3263
|
-
return false;
|
|
3264
|
-
}
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
3841
|
// src/utils/build-order-snapshot.ts
|
|
3268
3842
|
function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
|
|
3269
3843
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -3273,7 +3847,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
|
|
|
3273
3847
|
) ? settings.hostDefaultQuantity : 1;
|
|
3274
3848
|
const fbSettings = {
|
|
3275
3849
|
requireConstraintFit: true,
|
|
3276
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3850
|
+
ratePolicy: { kind: "lte_primary", pct: 5 },
|
|
3277
3851
|
selectionStrategy: "priority",
|
|
3278
3852
|
mode: mode === "dev" ? "dev" : "strict",
|
|
3279
3853
|
...(_c = settings.fallback) != null ? _c : {}
|
|
@@ -3309,7 +3883,9 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
|
|
|
3309
3883
|
const qtyRes = resolveQuantity(
|
|
3310
3884
|
visibleFieldIds,
|
|
3311
3885
|
fieldById,
|
|
3886
|
+
tagById,
|
|
3312
3887
|
selection,
|
|
3888
|
+
tagId,
|
|
3313
3889
|
hostDefaultQty
|
|
3314
3890
|
);
|
|
3315
3891
|
const quantity = qtyRes.quantity;
|
|
@@ -3424,7 +4000,7 @@ function buildInputs(visibleFieldIds, fieldById, selection) {
|
|
|
3424
4000
|
}
|
|
3425
4001
|
return { formValues, selections };
|
|
3426
4002
|
}
|
|
3427
|
-
function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
|
|
4003
|
+
function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId, hostDefault) {
|
|
3428
4004
|
var _a;
|
|
3429
4005
|
for (const fid of visibleFieldIds) {
|
|
3430
4006
|
const f = fieldById.get(fid);
|
|
@@ -3441,7 +4017,16 @@ function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
|
|
|
3441
4017
|
source: { kind: "field", id: f.id, rule }
|
|
3442
4018
|
};
|
|
3443
4019
|
}
|
|
4020
|
+
break;
|
|
3444
4021
|
}
|
|
4022
|
+
const nodeDefault = resolveNodeDefaultQuantity(
|
|
4023
|
+
visibleFieldIds,
|
|
4024
|
+
fieldById,
|
|
4025
|
+
tagById,
|
|
4026
|
+
selection,
|
|
4027
|
+
tagId
|
|
4028
|
+
);
|
|
4029
|
+
if (nodeDefault) return nodeDefault;
|
|
3445
4030
|
return {
|
|
3446
4031
|
quantity: hostDefault,
|
|
3447
4032
|
source: { kind: "default", defaultedFromHost: true }
|
|
@@ -3454,9 +4039,37 @@ function readQuantityRule(v) {
|
|
|
3454
4039
|
return void 0;
|
|
3455
4040
|
const out = { valueBy: src.valueBy };
|
|
3456
4041
|
if (src.code && typeof src.code === "string") out.code = src.code;
|
|
4042
|
+
if (typeof src.multiply === "number" && Number.isFinite(src.multiply)) {
|
|
4043
|
+
out.multiply = src.multiply;
|
|
4044
|
+
}
|
|
4045
|
+
if (typeof src.fallback === "number" && Number.isFinite(src.fallback)) {
|
|
4046
|
+
out.fallback = src.fallback;
|
|
4047
|
+
}
|
|
4048
|
+
if (src.clamp && typeof src.clamp === "object") {
|
|
4049
|
+
const min = typeof src.clamp.min === "number" && Number.isFinite(src.clamp.min) ? src.clamp.min : void 0;
|
|
4050
|
+
const max = typeof src.clamp.max === "number" && Number.isFinite(src.clamp.max) ? src.clamp.max : void 0;
|
|
4051
|
+
if (min !== void 0 || max !== void 0) {
|
|
4052
|
+
out.clamp = {
|
|
4053
|
+
...min !== void 0 ? { min } : {},
|
|
4054
|
+
...max !== void 0 ? { max } : {}
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
3457
4058
|
return out;
|
|
3458
4059
|
}
|
|
3459
4060
|
function evaluateQuantityRule(rule, raw) {
|
|
4061
|
+
const evaluated = evaluateRawQuantityRule(rule, raw);
|
|
4062
|
+
if (Number.isFinite(evaluated)) {
|
|
4063
|
+
const adjusted = applyQuantityTransforms(evaluated, rule);
|
|
4064
|
+
if (Number.isFinite(adjusted) && adjusted > 0) return adjusted;
|
|
4065
|
+
}
|
|
4066
|
+
if (typeof rule.fallback === "number" && Number.isFinite(rule.fallback)) {
|
|
4067
|
+
const fallback = applyClamp(rule.fallback, rule.clamp);
|
|
4068
|
+
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
4069
|
+
}
|
|
4070
|
+
return NaN;
|
|
4071
|
+
}
|
|
4072
|
+
function evaluateRawQuantityRule(rule, raw) {
|
|
3460
4073
|
switch (rule.valueBy) {
|
|
3461
4074
|
case "value": {
|
|
3462
4075
|
const n = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
@@ -3489,6 +4102,65 @@ function evaluateQuantityRule(rule, raw) {
|
|
|
3489
4102
|
return NaN;
|
|
3490
4103
|
}
|
|
3491
4104
|
}
|
|
4105
|
+
function applyQuantityTransforms(value, rule) {
|
|
4106
|
+
let next = value;
|
|
4107
|
+
if (typeof rule.multiply === "number" && Number.isFinite(rule.multiply)) {
|
|
4108
|
+
next *= rule.multiply;
|
|
4109
|
+
}
|
|
4110
|
+
return applyClamp(next, rule.clamp);
|
|
4111
|
+
}
|
|
4112
|
+
function applyClamp(value, clamp2) {
|
|
4113
|
+
let next = value;
|
|
4114
|
+
if ((clamp2 == null ? void 0 : clamp2.min) !== void 0) next = Math.max(next, clamp2.min);
|
|
4115
|
+
if ((clamp2 == null ? void 0 : clamp2.max) !== void 0) next = Math.min(next, clamp2.max);
|
|
4116
|
+
return next;
|
|
4117
|
+
}
|
|
4118
|
+
function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
|
|
4119
|
+
var _a, _b, _c, _d;
|
|
4120
|
+
const optionVisit = buildOptionVisitOrder(selection, fieldById);
|
|
4121
|
+
for (const { fieldId, optionId } of optionVisit) {
|
|
4122
|
+
if (!visibleFieldIds.includes(fieldId)) continue;
|
|
4123
|
+
const field = fieldById.get(fieldId);
|
|
4124
|
+
const option = (_a = field == null ? void 0 : field.options) == null ? void 0 : _a.find((item) => item.id === optionId);
|
|
4125
|
+
const quantityDefault = readQuantityDefault(
|
|
4126
|
+
(_b = option == null ? void 0 : option.meta) == null ? void 0 : _b.quantityDefault
|
|
4127
|
+
);
|
|
4128
|
+
if (quantityDefault !== void 0) {
|
|
4129
|
+
return {
|
|
4130
|
+
quantity: quantityDefault,
|
|
4131
|
+
source: { kind: "option", id: optionId }
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
for (const fieldId of visibleFieldIds) {
|
|
4136
|
+
const field = fieldById.get(fieldId);
|
|
4137
|
+
if (!field) continue;
|
|
4138
|
+
const isButtonStyle = field.button === true || Array.isArray(field.options) && field.options.length > 0;
|
|
4139
|
+
if (!isButtonStyle) continue;
|
|
4140
|
+
const quantityDefault = readQuantityDefault(
|
|
4141
|
+
field.quantityDefault
|
|
4142
|
+
);
|
|
4143
|
+
if (quantityDefault !== void 0) {
|
|
4144
|
+
return {
|
|
4145
|
+
quantity: quantityDefault,
|
|
4146
|
+
source: { kind: "field", id: field.id }
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
const tagQuantityDefault = readQuantityDefault(
|
|
4151
|
+
(_d = (_c = tagById.get(tagId)) == null ? void 0 : _c.meta) == null ? void 0 : _d.quantityDefault
|
|
4152
|
+
);
|
|
4153
|
+
if (tagQuantityDefault !== void 0) {
|
|
4154
|
+
return {
|
|
4155
|
+
quantity: tagQuantityDefault,
|
|
4156
|
+
source: { kind: "tag", id: tagId }
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
return void 0;
|
|
4160
|
+
}
|
|
4161
|
+
function readQuantityDefault(value) {
|
|
4162
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
4163
|
+
}
|
|
3492
4164
|
function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById) {
|
|
3493
4165
|
var _a;
|
|
3494
4166
|
const serviceMap = {};
|
|
@@ -3607,7 +4279,7 @@ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
|
|
|
3607
4279
|
fallbacks.global
|
|
3608
4280
|
)) {
|
|
3609
4281
|
if (!present.has(String(primary))) continue;
|
|
3610
|
-
const primId =
|
|
4282
|
+
const primId = isFiniteNumber2(primary) ? Number(primary) : primary;
|
|
3611
4283
|
const kept = [];
|
|
3612
4284
|
for (const cand of cands != null ? cands : []) {
|
|
3613
4285
|
if (!rateOk(svcMap, cand, primId, policy)) continue;
|
|
@@ -3626,7 +4298,7 @@ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
|
|
|
3626
4298
|
};
|
|
3627
4299
|
}
|
|
3628
4300
|
}
|
|
3629
|
-
function
|
|
4301
|
+
function isFiniteNumber2(v) {
|
|
3630
4302
|
return typeof v === "number" && Number.isFinite(v);
|
|
3631
4303
|
}
|
|
3632
4304
|
function collectUtilityLineItems(visibleFieldIds, fieldById, selection, quantity) {
|
|
@@ -3681,9 +4353,14 @@ function readUtilityMarker(v) {
|
|
|
3681
4353
|
if (src.mode !== "flat" && src.mode !== "per_quantity" && src.mode !== "per_value" && src.mode !== "percent")
|
|
3682
4354
|
return void 0;
|
|
3683
4355
|
const out = { mode: src.mode, rate: src.rate };
|
|
3684
|
-
if (src.valueBy === "value" || src.valueBy === "length"
|
|
4356
|
+
if (src.valueBy === "value" || src.valueBy === "length")
|
|
3685
4357
|
out.valueBy = src.valueBy;
|
|
3686
|
-
if (src.
|
|
4358
|
+
if (src.percentBase === "service_total" || src.percentBase === "base_service" || src.percentBase === "all") {
|
|
4359
|
+
out.percentBase = src.percentBase;
|
|
4360
|
+
}
|
|
4361
|
+
if (typeof src.label === "string" && src.label.trim()) {
|
|
4362
|
+
out.label = src.label.trim();
|
|
4363
|
+
}
|
|
3687
4364
|
return out;
|
|
3688
4365
|
}
|
|
3689
4366
|
function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
|
|
@@ -3692,14 +4369,14 @@ function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
|
|
|
3692
4369
|
nodeId,
|
|
3693
4370
|
mode: marker.mode,
|
|
3694
4371
|
rate: marker.rate,
|
|
4372
|
+
...marker.percentBase ? { percentBase: marker.percentBase } : {},
|
|
4373
|
+
...marker.label ? { label: marker.label } : {},
|
|
3695
4374
|
inputs: { quantity }
|
|
3696
4375
|
};
|
|
3697
4376
|
if (marker.mode === "per_value") {
|
|
3698
4377
|
base.inputs.valueBy = (_a = marker.valueBy) != null ? _a : "value";
|
|
3699
4378
|
if (marker.valueBy === "length") {
|
|
3700
4379
|
base.inputs.value = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : 0;
|
|
3701
|
-
} else if (marker.valueBy === "eval") {
|
|
3702
|
-
base.inputs.evalCodeUsed = true;
|
|
3703
4380
|
} else {
|
|
3704
4381
|
base.inputs.value = Array.isArray(value) ? (_b = value[0]) != null ? _b : null : value != null ? value : null;
|
|
3705
4382
|
}
|
|
@@ -3770,47 +4447,13 @@ function buildDevWarnings(props, svcMap, _tagId, _snapshotServiceMap, originalFa
|
|
|
3770
4447
|
return out;
|
|
3771
4448
|
}
|
|
3772
4449
|
function toSnapshotPolicy(settings) {
|
|
3773
|
-
var _a
|
|
4450
|
+
var _a;
|
|
3774
4451
|
const requireConstraintFit = (_a = settings.requireConstraintFit) != null ? _a : true;
|
|
3775
|
-
const rp = (
|
|
3776
|
-
|
|
3777
|
-
case "lte_primary":
|
|
3778
|
-
return {
|
|
3779
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3780
|
-
requireConstraintFit
|
|
3781
|
-
};
|
|
3782
|
-
case "within_pct":
|
|
3783
|
-
return {
|
|
3784
|
-
ratePolicy: {
|
|
3785
|
-
kind: "lte_primary",
|
|
3786
|
-
thresholdPct: Math.max(0, (_c = rp.pct) != null ? _c : 0)
|
|
3787
|
-
},
|
|
3788
|
-
requireConstraintFit
|
|
3789
|
-
};
|
|
3790
|
-
case "at_least_pct_lower":
|
|
3791
|
-
return {
|
|
3792
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3793
|
-
requireConstraintFit
|
|
3794
|
-
};
|
|
3795
|
-
default:
|
|
3796
|
-
return {
|
|
3797
|
-
ratePolicy: { kind: "lte_primary" },
|
|
3798
|
-
requireConstraintFit
|
|
3799
|
-
};
|
|
3800
|
-
}
|
|
4452
|
+
const rp = normalizeRatePolicy(settings.ratePolicy);
|
|
4453
|
+
return { ratePolicy: rp, requireConstraintFit };
|
|
3801
4454
|
}
|
|
3802
4455
|
function getCap2(map, id) {
|
|
3803
|
-
|
|
3804
|
-
if (direct) return direct;
|
|
3805
|
-
const strKey = String(id);
|
|
3806
|
-
const byStr = map[strKey];
|
|
3807
|
-
if (byStr) return byStr;
|
|
3808
|
-
const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
|
|
3809
|
-
if (Number.isFinite(n)) {
|
|
3810
|
-
const byNum = map[n];
|
|
3811
|
-
if (byNum) return byNum;
|
|
3812
|
-
}
|
|
3813
|
-
return void 0;
|
|
4456
|
+
return getServiceCapability(map, id);
|
|
3814
4457
|
}
|
|
3815
4458
|
function resolveMinMax(servicesList, services) {
|
|
3816
4459
|
let min = void 0;
|
|
@@ -4175,9 +4818,11 @@ function mapDiagReason(reason) {
|
|
|
4175
4818
|
createBuilder,
|
|
4176
4819
|
createFallbackEditor,
|
|
4177
4820
|
createNodeIndex,
|
|
4821
|
+
filterServicesForVisibleGroup,
|
|
4178
4822
|
getEligibleFallbacks,
|
|
4179
4823
|
getFallbackRegistrationInfo,
|
|
4180
4824
|
normalise,
|
|
4825
|
+
normalizeFieldValidation,
|
|
4181
4826
|
resolveServiceFallback,
|
|
4182
4827
|
validate,
|
|
4183
4828
|
validateAsync,
|