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