@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.
@@ -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 = toNumberOrUndefined(src.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 field_service_id_raw = toNumberOrUndefined(src.service_id);
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 = toNumberOrUndefined(src.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 toNumberOrUndefined(v) {
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
- const n = Number(v);
287
- return Number.isFinite(n) ? n : void 0;
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) => isFiniteNumber(o.service_id));
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 (!isFiniteNumber(o.service_id)) continue;
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 = isFiniteNumber(o.service_id);
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 = /* @__PURE__ */ new Set();
1184
- const contributingOptionIds = /* @__PURE__ */ new Set();
1185
- for (const o of (_a = f.options) != null ? _a : []) {
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 (!isFiniteNumber(sid)) continue;
1190
- const rate = (_d = v.serviceMap[sid]) == null ? void 0 : _d.rate;
1191
- if (isFiniteNumber(rate)) {
1192
- baseRates.add(Number(rate));
1193
- contributingOptionIds.add(o.id);
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.size > 1) {
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
- ...Array.from(contributingOptionIds)
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}" resolve to different service rates.`,
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
- rates: Array.from(baseRates.values()),
1210
- optionIds: Array.from(contributingOptionIds.values())
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 (!isFiniteNumber(o.service_id)) continue;
1273
- const svc = v.serviceMap[o.service_id];
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 (!isFiniteNumber(sid)) continue;
1300
- const svc = v.serviceMap[Number(sid)];
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 (!isFiniteNumber(o.service_id)) continue;
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 isServiceIdRef(v) {
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 (!isServiceIdRef(sid)) continue;
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 && isServiceIdRef(sid)) {
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 (isServiceIdRef(fSid)) {
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 (!isServiceIdRef(oSid)) continue;
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 (!isServiceIdRef(cand)) continue;
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 (!isServiceIdRef(cand)) continue;
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/fallback.ts
2333
- var DEFAULT_SETTINGS = {
2334
- requireConstraintFit: true,
2335
- ratePolicy: { kind: "lte_primary" },
2336
- selectionStrategy: "priority",
2337
- mode: "strict"
2338
- };
2339
- function resolveServiceFallback(params) {
2340
- var _a, _b, _c, _d, _e;
2341
- const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
2342
- const { primary, nodeId, tagId, services } = params;
2343
- const fb = (_b = params.fallbacks) != null ? _b : {};
2344
- const tried = [];
2345
- const lists = [];
2346
- if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
2347
- if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
2348
- const primaryRate = rateOf(services, primary);
2349
- for (const list of lists) {
2350
- for (const cand of list) {
2351
- if (tried.includes(cand)) continue;
2352
- tried.push(cand);
2353
- const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
2354
- if (!candCap) continue;
2355
- if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
2356
- if (s.requireConstraintFit && tagId) {
2357
- const ok = satisfiesTagConstraints(tagId, params, candCap);
2358
- if (!ok) continue;
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
- return cand;
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 null;
2441
+ return diagnostics;
2364
2442
  }
2365
- function collectFailedFallbacks(props, services, settings) {
2366
- var _a, _b, _c;
2367
- const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
2368
- const out = [];
2369
- const fb = (_a = props.fallbacks) != null ? _a : {};
2370
- const primaryRate = (p) => rateOf(services, p);
2371
- for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
2372
- const { primary, tagContexts } = primaryForNode(props, nodeId);
2373
- if (!primary) {
2374
- out.push({
2375
- scope: "node",
2376
- nodeId,
2377
- primary: "",
2378
- candidate: "",
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
- for (const cand of list) {
2384
- const cap = getCap(services, cand);
2385
- if (!cap) {
2386
- out.push({
2387
- scope: "node",
2388
- nodeId,
2389
- primary,
2390
- candidate: cand,
2391
- reason: "unknown_service"
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
- if (String(cand) === String(primary)) {
2396
- out.push({
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
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
2406
- out.push({
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
- switch (policy.kind) {
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
- const direct = map[id];
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, _d2, _e;
3114
- if (typeof input !== "string") {
3115
- if ("bind_id" in input && !("type" in input))
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
- return (_e = (_d2 = nodeMap.get(input)) == null ? void 0 : _d2.node) != null ? _e : mkUnknown(input);
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 = isFiniteNumber3(primary) ? Number(primary) : primary;
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 isFiniteNumber3(v) {
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" || src.valueBy === "eval")
4356
+ if (src.valueBy === "value" || src.valueBy === "length")
3685
4357
  out.valueBy = src.valueBy;
3686
- if (src.code && typeof src.code === "string") out.code = src.code;
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, _b, _c;
4450
+ var _a;
3774
4451
  const requireConstraintFit = (_a = settings.requireConstraintFit) != null ? _a : true;
3775
- const rp = (_b = settings.ratePolicy) != null ? _b : { kind: "lte_primary" };
3776
- switch (rp.kind) {
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
- const direct = map[id];
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,