@timeax/digital-service-engine 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1260,13 +1260,7 @@ function resolveRootTags(tags) {
1260
1260
  const roots = tags.filter((t) => !t.bind_id);
1261
1261
  return roots.length ? roots : tags.slice(0, 1);
1262
1262
  }
1263
- function isEffectfulTrigger(v, trigger) {
1264
- var _a, _b, _c, _d, _e, _f;
1265
- const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
1266
- const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
1267
- return ((_d = (_c = inc[trigger]) == null ? void 0 : _c.length) != null ? _d : 0) > 0 || ((_f = (_e = exc[trigger]) == null ? void 0 : _e.length) != null ? _f : 0) > 0;
1268
- }
1269
- function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
1263
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
1270
1264
  var _a;
1271
1265
  const visible = visibleFieldsUnder(v.props, tagId, {
1272
1266
  selectedKeys
@@ -1275,11 +1269,11 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectfu
1275
1269
  for (const f of visible) {
1276
1270
  if (f.button === true) {
1277
1271
  const t = f.id;
1278
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
1272
+ if (effectfulKeys.has(t)) triggers.push(t);
1279
1273
  }
1280
1274
  for (const o of (_a = f.options) != null ? _a : []) {
1281
- const t = `${f.id}::${o.id}`;
1282
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
1275
+ const t = o.id;
1276
+ if (effectfulKeys.has(t)) triggers.push(t);
1283
1277
  }
1284
1278
  }
1285
1279
  triggers.sort();
@@ -1379,20 +1373,38 @@ function dedupeErrorsInPlace(v, startIndex) {
1379
1373
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
1380
1374
  }
1381
1375
  function validateVisibility(v, options = {}) {
1382
- var _a, _b, _c;
1376
+ var _a, _b, _c, _d, _e;
1377
+ v.simulatedVisibilityContexts = [];
1383
1378
  const simulate = options.simulate === true;
1384
1379
  if (!simulate) {
1385
1380
  runVisibilityRulesOnce(v);
1381
+ for (const tag of v.tags) {
1382
+ v.simulatedVisibilityContexts.push({
1383
+ tagId: tag.id,
1384
+ selectedKeys: Array.from(v.selectedKeys),
1385
+ visibleFieldIds: v.fieldsVisibleUnder(tag.id).map((f) => f.id)
1386
+ });
1387
+ }
1386
1388
  return;
1387
1389
  }
1388
1390
  const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
1389
1391
  const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
1390
1392
  const onlyEffectful = options.onlyEffectfulTriggers !== false;
1393
+ const effectfulKeys = /* @__PURE__ */ new Set();
1394
+ if (onlyEffectful) {
1395
+ for (const key of Object.keys((_c = v.props.includes_for_buttons) != null ? _c : {})) {
1396
+ effectfulKeys.add(key);
1397
+ }
1398
+ for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
1399
+ effectfulKeys.add(key);
1400
+ }
1401
+ }
1391
1402
  const roots = resolveRootTags(v.tags);
1392
1403
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
1393
- const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
1404
+ const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
1394
1405
  const errorsStart = v.errors.length;
1395
1406
  const visited = /* @__PURE__ */ new Set();
1407
+ const seenContexts = /* @__PURE__ */ new Set();
1396
1408
  const stack = [];
1397
1409
  for (const rt of rootTags) {
1398
1410
  stack.push({
@@ -1405,10 +1417,27 @@ function validateVisibility(v, options = {}) {
1405
1417
  while (stack.length) {
1406
1418
  if (validatedStates >= maxStates) break;
1407
1419
  const state = stack.pop();
1408
- const sig = stableKeyOfSelection(state.selected);
1420
+ const sig = `${state.rootTagId}::${stableKeyOfSelection(state.selected)}`;
1409
1421
  if (visited.has(sig)) continue;
1410
1422
  visited.add(sig);
1411
1423
  v.selectedKeys = state.selected;
1424
+ const visibleNow = visibleFieldsUnder(v.props, state.rootTagId, {
1425
+ selectedKeys: state.selected
1426
+ }).map((f) => f.id);
1427
+ const context = {
1428
+ tagId: state.rootTagId,
1429
+ selectedKeys: Array.from(state.selected),
1430
+ visibleFieldIds: visibleNow
1431
+ };
1432
+ const contextKey = [
1433
+ context.tagId,
1434
+ [...context.selectedKeys].sort().join("|"),
1435
+ [...context.visibleFieldIds].sort().join("|")
1436
+ ].join("::");
1437
+ if (!seenContexts.has(contextKey)) {
1438
+ seenContexts.add(contextKey);
1439
+ v.simulatedVisibilityContexts.push(context);
1440
+ }
1412
1441
  validatedStates++;
1413
1442
  runVisibilityRulesOnce(v);
1414
1443
  if (state.depth >= maxDepth) continue;
@@ -1416,7 +1445,7 @@ function validateVisibility(v, options = {}) {
1416
1445
  v,
1417
1446
  state.rootTagId,
1418
1447
  state.selected,
1419
- onlyEffectful
1448
+ effectfulKeys
1420
1449
  );
1421
1450
  for (let i = triggers.length - 1; i >= 0; i--) {
1422
1451
  const trig = triggers[i];
@@ -1658,8 +1687,8 @@ function validateOptionMaps(v) {
1658
1687
  };
1659
1688
  }
1660
1689
  if (ref.kind === "field") {
1661
- const isButton2 = ref.node.button === true;
1662
- if (!isButton2)
1690
+ const isButton = ref.node.button === true;
1691
+ if (!isButton)
1663
1692
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
1664
1693
  return { ok: true, nodeId: ref.id, affected: [ref.id] };
1665
1694
  }
@@ -1840,10 +1869,19 @@ function validateOrderKinds(v) {
1840
1869
  }
1841
1870
 
1842
1871
  // src/core/validate/steps/service-vs-input.ts
1872
+ function hasButtonTriggerMap(v, fieldId) {
1873
+ var _a, _b;
1874
+ const includes = (_a = v.props.includes_for_buttons) == null ? void 0 : _a[fieldId];
1875
+ const excludes = (_b = v.props.excludes_for_buttons) == null ? void 0 : _b[fieldId];
1876
+ return Array.isArray(includes) && includes.length > 0 || Array.isArray(excludes) && excludes.length > 0;
1877
+ }
1843
1878
  function validateServiceVsUserInput(v) {
1844
1879
  for (const f of v.fields) {
1845
1880
  const anySvc = hasAnyServiceOption(f);
1846
1881
  const hasName = !!(f.name && f.name.trim());
1882
+ const isButton = f.button === true;
1883
+ const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
1884
+ const hasTriggerMap = isButton && hasButtonTriggerMap(v, f.id);
1847
1885
  if (f.type === "custom" && anySvc) {
1848
1886
  v.errors.push({
1849
1887
  code: "user_input_field_has_service_option",
@@ -1854,14 +1892,15 @@ function validateServiceVsUserInput(v) {
1854
1892
  });
1855
1893
  }
1856
1894
  if (!hasName) {
1857
- if (!anySvc) {
1858
- v.errors.push({
1859
- code: "service_field_missing_service_id",
1860
- severity: "error",
1861
- message: `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
1862
- nodeId: f.id
1863
- });
1895
+ if (hasFieldService || anySvc || hasTriggerMap) {
1896
+ continue;
1864
1897
  }
1898
+ v.errors.push({
1899
+ code: "service_field_missing_service_id",
1900
+ severity: "error",
1901
+ message: isButton ? `Button field "${f.id}" has no "name", no "service_id", and no includes/excludes trigger map. Add a name, attach a service_id, or configure includes_for_buttons/excludes_for_buttons.` : `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
1902
+ nodeId: f.id
1903
+ });
1865
1904
  } else {
1866
1905
  if (anySvc) {
1867
1906
  v.errors.push({
@@ -2178,6 +2217,324 @@ function validateRates(v) {
2178
2217
  }
2179
2218
  }
2180
2219
 
2220
+ // src/core/rate-coherence.ts
2221
+ function buildTriggerEffectMap(props) {
2222
+ var _a, _b;
2223
+ const map = /* @__PURE__ */ new Map();
2224
+ const ensure = (key) => {
2225
+ let item = map.get(key);
2226
+ if (!item) {
2227
+ item = { includes: /* @__PURE__ */ new Set(), excludes: /* @__PURE__ */ new Set() };
2228
+ map.set(key, item);
2229
+ }
2230
+ return item;
2231
+ };
2232
+ for (const [key, ids] of Object.entries((_a = props.includes_for_buttons) != null ? _a : {})) {
2233
+ const item = ensure(key);
2234
+ for (const id of ids != null ? ids : []) item.includes.add(id);
2235
+ }
2236
+ for (const [key, ids] of Object.entries((_b = props.excludes_for_buttons) != null ? _b : {})) {
2237
+ const item = ensure(key);
2238
+ for (const id of ids != null ? ids : []) item.excludes.add(id);
2239
+ }
2240
+ return map;
2241
+ }
2242
+ function isRefExcludedBySelectedKeys(ref, selectedKeys, effectMap) {
2243
+ for (const key of selectedKeys) {
2244
+ const effects = effectMap.get(key);
2245
+ if (!effects) continue;
2246
+ if (ref.fieldId && effects.excludes.has(ref.fieldId) || effects.excludes.has(ref.nodeId)) {
2247
+ return true;
2248
+ }
2249
+ }
2250
+ return false;
2251
+ }
2252
+
2253
+ // src/core/validate/steps/rate-coherence.ts
2254
+ function normalizeRole(role, fallback) {
2255
+ return role === "base" || role === "utility" ? role : fallback;
2256
+ }
2257
+ function uniqueStrings(values) {
2258
+ const out = /* @__PURE__ */ new Set();
2259
+ for (const value of values) {
2260
+ if (!value) continue;
2261
+ out.add(value);
2262
+ }
2263
+ return Array.from(out);
2264
+ }
2265
+ function getRate(serviceMap, serviceId) {
2266
+ const cap = getServiceCapability(serviceMap, serviceId);
2267
+ const rate = cap == null ? void 0 : cap.rate;
2268
+ if (typeof rate !== "number" || !Number.isFinite(rate)) return void 0;
2269
+ return rate;
2270
+ }
2271
+ function collectContextRefs(tag, visibleFields, serviceMap) {
2272
+ var _a, _b, _c, _d, _e;
2273
+ const serviceRefs = [];
2274
+ let tagDefault;
2275
+ if (tag.service_id !== void 0 && tag.service_id !== null) {
2276
+ const tagRate = getRate(serviceMap, tag.service_id);
2277
+ if (tagRate != null) {
2278
+ tagDefault = {
2279
+ key: tag.id,
2280
+ nodeId: tag.id,
2281
+ nodeKind: "tag",
2282
+ serviceId: tag.service_id,
2283
+ rate: tagRate,
2284
+ label: (_a = tag.label) != null ? _a : tag.id,
2285
+ pricingRole: "base"
2286
+ };
2287
+ }
2288
+ }
2289
+ for (const field of visibleFields) {
2290
+ const fieldRole = normalizeRole(field.pricing_role, "base");
2291
+ if (field.service_id !== void 0 && field.service_id !== null) {
2292
+ const rate = getRate(serviceMap, field.service_id);
2293
+ if (rate != null) {
2294
+ serviceRefs.push({
2295
+ key: field.id,
2296
+ nodeId: field.id,
2297
+ fieldId: field.id,
2298
+ nodeKind: "button",
2299
+ serviceId: field.service_id,
2300
+ rate,
2301
+ label: (_b = field.label) != null ? _b : field.id,
2302
+ pricingRole: fieldRole
2303
+ });
2304
+ }
2305
+ }
2306
+ for (const option of (_c = field.options) != null ? _c : []) {
2307
+ if (option.service_id === void 0 || option.service_id === null) continue;
2308
+ const rate = getRate(serviceMap, option.service_id);
2309
+ if (rate == null) continue;
2310
+ serviceRefs.push({
2311
+ key: option.id,
2312
+ nodeId: option.id,
2313
+ fieldId: field.id,
2314
+ nodeKind: "option",
2315
+ serviceId: option.service_id,
2316
+ rate,
2317
+ label: (_d = option.label) != null ? _d : option.id,
2318
+ pricingRole: normalizeRole((_e = option.pricing_role) != null ? _e : field.pricing_role, "base")
2319
+ });
2320
+ }
2321
+ }
2322
+ return { tagDefault, serviceRefs };
2323
+ }
2324
+ function pickHighestRatePrimary(refs) {
2325
+ return refs.reduce((best, cur) => {
2326
+ if (!best) return cur;
2327
+ if (cur.rate > best.rate) return cur;
2328
+ if (cur.rate < best.rate) return best;
2329
+ return cur.nodeId < best.nodeId ? cur : best;
2330
+ }, void 0);
2331
+ }
2332
+ function validateRateCoherenceForVisibleContext(params) {
2333
+ const { v, tagId, selectedKeys, visibleFieldIds, effectMap, seen } = params;
2334
+ const tag = v.tagById.get(tagId);
2335
+ if (!tag) return;
2336
+ const visibleFields = visibleFieldIds.map((id) => v.fieldById.get(id)).filter(Boolean);
2337
+ const { tagDefault, serviceRefs: allServiceRefs } = collectContextRefs(
2338
+ tag,
2339
+ visibleFields,
2340
+ v.serviceMap
2341
+ );
2342
+ const baseRefs = allServiceRefs.filter((ref) => ref.pricingRole === "base");
2343
+ if (baseRefs.length === 0 && !tagDefault) return;
2344
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
2345
+ const visibleInvalidFieldIds = visibleFieldIds.filter(
2346
+ (fieldId) => v.invalidRateFieldIds.has(fieldId)
2347
+ );
2348
+ for (const fieldId of visibleInvalidFieldIds) {
2349
+ const internalKey = [
2350
+ "rate-coherence-internal",
2351
+ tagId,
2352
+ [...selectedKeys].sort().join("|"),
2353
+ fieldId
2354
+ ].join("::");
2355
+ if (seen.has(internalKey)) continue;
2356
+ seen.add(internalKey);
2357
+ v.errors.push({
2358
+ code: "rate_coherence_violation",
2359
+ severity: "error",
2360
+ nodeId: fieldId,
2361
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
2362
+ details: {
2363
+ kind: "internal_field",
2364
+ tagId,
2365
+ selectedKeys: [...selectedKeys],
2366
+ visibleFieldIds: [...visibleFieldIds],
2367
+ fieldId,
2368
+ invalidFieldIds: [fieldId],
2369
+ affectedIds: uniqueStrings([tagId, ...selectedKeys, fieldId])
2370
+ }
2371
+ });
2372
+ }
2373
+ const selectedSet = new Set(selectedKeys);
2374
+ const selectedServiceRefs = baseRefs.filter((ref) => selectedSet.has(ref.key));
2375
+ if (baseRefs.length === 0) return;
2376
+ for (let i = 0; i < baseRefs.length; i++) {
2377
+ for (let j = i + 1; j < baseRefs.length; j++) {
2378
+ const left = baseRefs[i];
2379
+ const right = baseRefs[j];
2380
+ const hypotheticalKeys = [...selectedKeys, left.key, right.key];
2381
+ const survivingRefs = baseRefs.filter(
2382
+ (ref) => !isRefExcludedBySelectedKeys(
2383
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
2384
+ hypotheticalKeys,
2385
+ effectMap
2386
+ )
2387
+ );
2388
+ const survivingSet = new Set(survivingRefs.map((ref) => ref.nodeId));
2389
+ if (!survivingSet.has(left.nodeId) || !survivingSet.has(right.nodeId)) {
2390
+ continue;
2391
+ }
2392
+ if (survivingRefs.length <= 1) continue;
2393
+ const survivingSelected = survivingRefs.filter(
2394
+ (ref) => selectedSet.has(ref.key)
2395
+ );
2396
+ const tagIsCompeting = survivingSelected.length === 0;
2397
+ const primary = pickHighestRatePrimary(survivingRefs);
2398
+ if (!primary) continue;
2399
+ const comparePool = survivingRefs.filter((ref) => ref.nodeId !== primary.nodeId);
2400
+ for (const candidate of comparePool) {
2401
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) continue;
2402
+ const issueKey = [
2403
+ "rate-coherence-context",
2404
+ tagId,
2405
+ [...selectedKeys].sort().join("|"),
2406
+ [...survivingRefs.map((r) => r.nodeId).sort()].join("|"),
2407
+ primary.nodeId,
2408
+ candidate.nodeId,
2409
+ ratePolicy.kind,
2410
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2411
+ ].join("::");
2412
+ if (seen.has(issueKey)) continue;
2413
+ seen.add(issueKey);
2414
+ v.errors.push({
2415
+ code: "rate_coherence_violation",
2416
+ severity: "error",
2417
+ nodeId: candidate.nodeId,
2418
+ message: "Visible service context contains incompatible base service rates.",
2419
+ details: {
2420
+ kind: "selected_context",
2421
+ tagId,
2422
+ selectedKeys: [...selectedKeys],
2423
+ visibleFieldIds: [...visibleFieldIds],
2424
+ primary: {
2425
+ nodeId: primary.nodeId,
2426
+ fieldId: primary.fieldId,
2427
+ service_id: primary.serviceId,
2428
+ serviceId: primary.serviceId,
2429
+ rate: primary.rate
2430
+ },
2431
+ candidate: {
2432
+ nodeId: candidate.nodeId,
2433
+ fieldId: candidate.fieldId,
2434
+ service_id: candidate.serviceId,
2435
+ serviceId: candidate.serviceId,
2436
+ rate: candidate.rate
2437
+ },
2438
+ policy: ratePolicy.kind,
2439
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2440
+ invalidFieldIds: visibleInvalidFieldIds,
2441
+ affectedIds: uniqueStrings([
2442
+ tagId,
2443
+ ...selectedKeys,
2444
+ primary.nodeId,
2445
+ primary.fieldId,
2446
+ candidate.nodeId,
2447
+ candidate.fieldId,
2448
+ tagIsCompeting ? tagDefault == null ? void 0 : tagDefault.nodeId : void 0
2449
+ ]),
2450
+ affectedServiceIds: uniqueStrings([
2451
+ String(primary.serviceId),
2452
+ String(candidate.serviceId)
2453
+ ])
2454
+ }
2455
+ });
2456
+ }
2457
+ }
2458
+ }
2459
+ if (selectedServiceRefs.length === 0 && tagDefault && baseRefs.length > 0) {
2460
+ const survivingByDefault = baseRefs.filter(
2461
+ (ref) => !isRefExcludedBySelectedKeys(
2462
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
2463
+ selectedKeys,
2464
+ effectMap
2465
+ )
2466
+ );
2467
+ for (const candidate of survivingByDefault) {
2468
+ if (passesRatePolicy(ratePolicy, tagDefault.rate, candidate.rate)) continue;
2469
+ const issueKey = [
2470
+ "rate-coherence-default",
2471
+ tagId,
2472
+ [...selectedKeys].sort().join("|"),
2473
+ tagDefault.nodeId,
2474
+ candidate.nodeId,
2475
+ ratePolicy.kind,
2476
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2477
+ ].join("::");
2478
+ if (seen.has(issueKey)) continue;
2479
+ seen.add(issueKey);
2480
+ v.errors.push({
2481
+ code: "rate_coherence_violation",
2482
+ severity: "error",
2483
+ nodeId: candidate.nodeId,
2484
+ message: "Visible service context contains incompatible base service rates.",
2485
+ details: {
2486
+ kind: "selected_context",
2487
+ tagId,
2488
+ selectedKeys: [...selectedKeys],
2489
+ visibleFieldIds: [...visibleFieldIds],
2490
+ primary: {
2491
+ nodeId: tagDefault.nodeId,
2492
+ service_id: tagDefault.serviceId,
2493
+ serviceId: tagDefault.serviceId,
2494
+ rate: tagDefault.rate
2495
+ },
2496
+ candidate: {
2497
+ nodeId: candidate.nodeId,
2498
+ fieldId: candidate.fieldId,
2499
+ service_id: candidate.serviceId,
2500
+ serviceId: candidate.serviceId,
2501
+ rate: candidate.rate
2502
+ },
2503
+ policy: ratePolicy.kind,
2504
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2505
+ invalidFieldIds: visibleInvalidFieldIds,
2506
+ affectedIds: uniqueStrings([
2507
+ tagId,
2508
+ ...selectedKeys,
2509
+ tagDefault.nodeId,
2510
+ candidate.nodeId,
2511
+ candidate.fieldId
2512
+ ]),
2513
+ affectedServiceIds: uniqueStrings([
2514
+ String(tagDefault.serviceId),
2515
+ String(candidate.serviceId)
2516
+ ])
2517
+ }
2518
+ });
2519
+ }
2520
+ }
2521
+ }
2522
+ function validateRateCoherence(v) {
2523
+ if (Object.keys(v.serviceMap).length === 0 || v.tags.length === 0) return;
2524
+ const effectMap = buildTriggerEffectMap(v.props);
2525
+ const seen = /* @__PURE__ */ new Set();
2526
+ for (const context of v.simulatedVisibilityContexts) {
2527
+ validateRateCoherenceForVisibleContext({
2528
+ v,
2529
+ tagId: context.tagId,
2530
+ selectedKeys: context.selectedKeys,
2531
+ visibleFieldIds: context.visibleFieldIds,
2532
+ effectMap,
2533
+ seen
2534
+ });
2535
+ }
2536
+ }
2537
+
2181
2538
  // src/core/validate/steps/constraints.ts
2182
2539
  function constraintKeysInChain(v, tagId) {
2183
2540
  const keys = [];
@@ -2934,6 +3291,84 @@ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2934
3291
  };
2935
3292
  }
2936
3293
 
3294
+ // src/core/validate/index.ts
3295
+ function readVisibilitySimOpts(ctx) {
3296
+ const c = ctx;
3297
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
3298
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
3299
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
3300
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
3301
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
3302
+ return {
3303
+ simulate,
3304
+ maxStates,
3305
+ maxDepth,
3306
+ simulateAllRoots,
3307
+ onlyEffectfulTriggers
3308
+ };
3309
+ }
3310
+ function validate(props, ctx = {}) {
3311
+ var _a, _b, _c;
3312
+ const options = mergeValidatorOptions({}, ctx);
3313
+ const fallbackSettings = resolveFallbackSettings(options);
3314
+ const ratePolicy = resolveGlobalRatePolicy(options);
3315
+ const errors = [];
3316
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3317
+ const selectedKeys = new Set(
3318
+ (_b = options.selectedOptionKeys) != null ? _b : []
3319
+ );
3320
+ const tags = Array.isArray(props.filters) ? props.filters : [];
3321
+ const fields = Array.isArray(props.fields) ? props.fields : [];
3322
+ const tagById = /* @__PURE__ */ new Map();
3323
+ const fieldById = /* @__PURE__ */ new Map();
3324
+ for (const t of tags) tagById.set(t.id, t);
3325
+ for (const f of fields) fieldById.set(f.id, f);
3326
+ const v = {
3327
+ props,
3328
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3329
+ options: {
3330
+ ...options,
3331
+ ratePolicy,
3332
+ fallbackSettings
3333
+ },
3334
+ errors,
3335
+ serviceMap,
3336
+ selectedKeys,
3337
+ tags,
3338
+ fields,
3339
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
3340
+ tagById,
3341
+ fieldById,
3342
+ fieldsVisibleUnder: (_tagId) => [],
3343
+ simulatedVisibilityContexts: []
3344
+ };
3345
+ validateStructure(v);
3346
+ validateIdentity(v);
3347
+ validateOptionMaps(v);
3348
+ validateOrderKinds(v);
3349
+ v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3350
+ const visSim = readVisibilitySimOpts(options);
3351
+ validateVisibility(v, visSim);
3352
+ applyPolicies(
3353
+ v.errors,
3354
+ v.props,
3355
+ v.serviceMap,
3356
+ v.options.policies,
3357
+ v.fieldsVisibleUnder,
3358
+ v.tags
3359
+ );
3360
+ validateServiceVsUserInput(v);
3361
+ validateUtilityMarkers(v);
3362
+ validateRates(v);
3363
+ validateRateCoherence(v);
3364
+ validateConstraints(v);
3365
+ validateCustomFields(v);
3366
+ validateGlobalUtilityGuard(v);
3367
+ validateUnboundFields(v);
3368
+ validateFallbacks(v);
3369
+ return v.errors;
3370
+ }
3371
+
2937
3372
  // src/core/builder.ts
2938
3373
  import { cloneDeep as cloneDeep2 } from "lodash-es";
2939
3374
  function createBuilder(opts = {}) {
@@ -3216,344 +3651,6 @@ function toStringSet(v) {
3216
3651
  return new Set(v.map(String));
3217
3652
  }
3218
3653
 
3219
- // src/core/rate-coherence.ts
3220
- function validateRateCoherenceDeep(params) {
3221
- var _a, _b, _c;
3222
- const { builder, services, tagId } = params;
3223
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
3224
- const props = builder.getProps();
3225
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
3226
- const fields = (_b = props.fields) != null ? _b : [];
3227
- const fieldById = new Map(fields.map((f) => [f.id, f]));
3228
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
3229
- const tag = tagById.get(tagId);
3230
- const baselineFieldIds = builder.visibleFields(tagId, []);
3231
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
3232
- const anchors = collectAnchors(baselineFields);
3233
- const diagnostics = [];
3234
- const seen = /* @__PURE__ */ new Set();
3235
- for (const anchor of anchors) {
3236
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
3237
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
3238
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
3239
- for (const fieldId of visibleInvalidFieldIds) {
3240
- const key = `internal|${tagId}|${fieldId}`;
3241
- if (seen.has(key)) continue;
3242
- seen.add(key);
3243
- diagnostics.push({
3244
- kind: "internal_field",
3245
- scope: "visible_group",
3246
- tagId,
3247
- fieldId,
3248
- nodeId: fieldId,
3249
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
3250
- simulationAnchor: {
3251
- kind: anchor.kind,
3252
- id: anchor.id,
3253
- fieldId: anchor.fieldId,
3254
- label: anchor.label
3255
- },
3256
- invalidFieldIds: [fieldId]
3257
- });
3258
- }
3259
- const references = visibleFields.flatMap(
3260
- (field) => collectFieldReferences(field, services)
3261
- );
3262
- if (references.length <= 1) continue;
3263
- const primary = references.reduce((best, current) => {
3264
- if (current.rate !== best.rate) {
3265
- return current.rate > best.rate ? current : best;
3266
- }
3267
- const bestKey = `${best.fieldId}|${best.nodeId}`;
3268
- const currentKey = `${current.fieldId}|${current.nodeId}`;
3269
- return currentKey < bestKey ? current : best;
3270
- });
3271
- for (const candidate of references) {
3272
- if (candidate.nodeId === primary.nodeId) continue;
3273
- if (candidate.fieldId === primary.fieldId) continue;
3274
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
3275
- continue;
3276
- }
3277
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
3278
- if (seen.has(key)) continue;
3279
- seen.add(key);
3280
- diagnostics.push({
3281
- kind: "contextual",
3282
- scope: "visible_group",
3283
- tagId,
3284
- nodeId: candidate.nodeId,
3285
- primary: toDiagnosticRef(primary),
3286
- offender: toDiagnosticRef(candidate),
3287
- policy: ratePolicy.kind,
3288
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
3289
- message: explainRateMismatch(
3290
- ratePolicy,
3291
- primary,
3292
- candidate,
3293
- describeLabel(tag)
3294
- ),
3295
- simulationAnchor: {
3296
- kind: anchor.kind,
3297
- id: anchor.id,
3298
- fieldId: anchor.fieldId,
3299
- label: anchor.label
3300
- },
3301
- invalidFieldIds: visibleInvalidFieldIds
3302
- });
3303
- }
3304
- }
3305
- return diagnostics;
3306
- }
3307
- function collectAnchors(fields) {
3308
- var _a, _b;
3309
- const anchors = [];
3310
- for (const field of fields) {
3311
- if (!isButton(field)) continue;
3312
- if (Array.isArray(field.options) && field.options.length > 0) {
3313
- for (const option of field.options) {
3314
- anchors.push({
3315
- kind: "option",
3316
- id: option.id,
3317
- fieldId: field.id,
3318
- label: (_a = option.label) != null ? _a : option.id
3319
- });
3320
- }
3321
- continue;
3322
- }
3323
- anchors.push({
3324
- kind: "field",
3325
- id: field.id,
3326
- fieldId: field.id,
3327
- label: (_b = field.label) != null ? _b : field.id
3328
- });
3329
- }
3330
- return anchors;
3331
- }
3332
- function collectFieldReferences(field, services) {
3333
- var _a;
3334
- const members = collectBaseMembers(field, services);
3335
- if (members.length === 0) return [];
3336
- if (isMultiField(field)) {
3337
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
3338
- return [
3339
- {
3340
- refKind: "multi",
3341
- nodeId: field.id,
3342
- fieldId: field.id,
3343
- label: (_a = field.label) != null ? _a : field.id,
3344
- rate: averageRate,
3345
- members
3346
- }
3347
- ];
3348
- }
3349
- return members.map((member) => ({
3350
- refKind: "single",
3351
- nodeId: member.id,
3352
- fieldId: field.id,
3353
- label: member.label,
3354
- rate: member.rate,
3355
- service_id: member.service_id,
3356
- members: [member]
3357
- }));
3358
- }
3359
- function collectBaseMembers(field, services) {
3360
- var _a, _b, _c;
3361
- const members = [];
3362
- if (Array.isArray(field.options) && field.options.length > 0) {
3363
- for (const option of field.options) {
3364
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
3365
- if (role2 !== "base") continue;
3366
- if (option.service_id === void 0 || option.service_id === null) {
3367
- continue;
3368
- }
3369
- const cap2 = getServiceCapability(services, option.service_id);
3370
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
3371
- continue;
3372
- }
3373
- members.push({
3374
- kind: "option",
3375
- id: option.id,
3376
- fieldId: field.id,
3377
- label: (_b = option.label) != null ? _b : option.id,
3378
- service_id: option.service_id,
3379
- rate: cap2.rate
3380
- });
3381
- }
3382
- return members;
3383
- }
3384
- const role = normalizeRole(field.pricing_role, "base");
3385
- if (role !== "base") return members;
3386
- if (field.service_id === void 0 || field.service_id === null) return members;
3387
- const cap = getServiceCapability(services, field.service_id);
3388
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
3389
- return members;
3390
- }
3391
- members.push({
3392
- kind: "field",
3393
- id: field.id,
3394
- fieldId: field.id,
3395
- label: (_c = field.label) != null ? _c : field.id,
3396
- service_id: field.service_id,
3397
- rate: cap.rate
3398
- });
3399
- return members;
3400
- }
3401
- function isButton(field) {
3402
- if (field.button === true) return true;
3403
- return Array.isArray(field.options) && field.options.length > 0;
3404
- }
3405
- function normalizeRole(role, fallback) {
3406
- return role === "base" || role === "utility" ? role : fallback;
3407
- }
3408
- function toDiagnosticRef(reference) {
3409
- return {
3410
- nodeId: reference.nodeId,
3411
- fieldId: reference.fieldId,
3412
- label: reference.label,
3413
- refKind: reference.refKind,
3414
- service_id: reference.service_id,
3415
- rate: reference.rate
3416
- };
3417
- }
3418
- function contextualKey(tagId, primary, candidate, ratePolicy) {
3419
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
3420
- return [
3421
- "contextual",
3422
- tagId,
3423
- primary.fieldId,
3424
- primary.nodeId,
3425
- candidate.fieldId,
3426
- candidate.nodeId,
3427
- `${ratePolicy.kind}${pctKey}`
3428
- ].join("|");
3429
- }
3430
- function describeLabel(tag) {
3431
- var _a, _b;
3432
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
3433
- }
3434
- function explainRateMismatch(policy, primary, candidate, where) {
3435
- var _a, _b;
3436
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
3437
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
3438
- switch (policy.kind) {
3439
- case "eq_primary":
3440
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
3441
- case "lte_primary":
3442
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
3443
- case "within_pct":
3444
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
3445
- case "at_least_pct_lower":
3446
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
3447
- }
3448
- }
3449
-
3450
- // src/core/validate/index.ts
3451
- function readVisibilitySimOpts(ctx) {
3452
- const c = ctx;
3453
- const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
3454
- const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
3455
- const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
3456
- const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
3457
- const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
3458
- return {
3459
- simulate,
3460
- maxStates,
3461
- maxDepth,
3462
- simulateAllRoots,
3463
- onlyEffectfulTriggers
3464
- };
3465
- }
3466
- function validate(props, ctx = {}) {
3467
- var _a, _b, _c;
3468
- const options = mergeValidatorOptions({}, ctx);
3469
- const fallbackSettings = resolveFallbackSettings(options);
3470
- const ratePolicy = resolveGlobalRatePolicy(options);
3471
- const errors = [];
3472
- const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3473
- const selectedKeys = new Set(
3474
- (_b = options.selectedOptionKeys) != null ? _b : []
3475
- );
3476
- const tags = Array.isArray(props.filters) ? props.filters : [];
3477
- const fields = Array.isArray(props.fields) ? props.fields : [];
3478
- const tagById = /* @__PURE__ */ new Map();
3479
- const fieldById = /* @__PURE__ */ new Map();
3480
- for (const t of tags) tagById.set(t.id, t);
3481
- for (const f of fields) fieldById.set(f.id, f);
3482
- const v = {
3483
- props,
3484
- nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3485
- options: {
3486
- ...options,
3487
- ratePolicy,
3488
- fallbackSettings
3489
- },
3490
- errors,
3491
- serviceMap,
3492
- selectedKeys,
3493
- tags,
3494
- fields,
3495
- invalidRateFieldIds: /* @__PURE__ */ new Set(),
3496
- tagById,
3497
- fieldById,
3498
- fieldsVisibleUnder: (_tagId) => []
3499
- };
3500
- validateStructure(v);
3501
- validateIdentity(v);
3502
- validateOptionMaps(v);
3503
- validateOrderKinds(v);
3504
- v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3505
- const visSim = readVisibilitySimOpts(options);
3506
- validateVisibility(v, visSim);
3507
- applyPolicies(
3508
- v.errors,
3509
- v.props,
3510
- v.serviceMap,
3511
- v.options.policies,
3512
- v.fieldsVisibleUnder,
3513
- v.tags
3514
- );
3515
- validateServiceVsUserInput(v);
3516
- validateUtilityMarkers(v);
3517
- validateRates(v);
3518
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
3519
- const builder = createBuilder({ serviceMap });
3520
- builder.load(props);
3521
- for (const tag of tags) {
3522
- const diags = validateRateCoherenceDeep({
3523
- builder,
3524
- services: serviceMap,
3525
- tagId: tag.id,
3526
- ratePolicy,
3527
- invalidFieldIds: v.invalidRateFieldIds
3528
- });
3529
- for (const diag of diags) {
3530
- if (diag.kind !== "contextual") continue;
3531
- errors.push({
3532
- code: "rate_coherence_violation",
3533
- severity: "error",
3534
- message: diag.message,
3535
- nodeId: diag.nodeId,
3536
- details: {
3537
- tagId: diag.tagId,
3538
- simulationAnchor: diag.simulationAnchor,
3539
- primary: diag.primary,
3540
- offender: diag.offender,
3541
- policy: diag.policy,
3542
- policyPct: diag.policyPct,
3543
- invalidFieldIds: diag.invalidFieldIds
3544
- }
3545
- });
3546
- }
3547
- }
3548
- }
3549
- validateConstraints(v);
3550
- validateCustomFields(v);
3551
- validateGlobalUtilityGuard(v);
3552
- validateUnboundFields(v);
3553
- validateFallbacks(v);
3554
- return v.errors;
3555
- }
3556
-
3557
3654
  // src/core/fallback.ts
3558
3655
  var DEFAULT_SETTINGS = {
3559
3656
  requireConstraintFit: true,
@@ -4132,9 +4229,9 @@ function createNodeIndex(builder) {
4132
4229
  if (cached) return cached;
4133
4230
  const raw = fieldById.get(id);
4134
4231
  if (!raw) return void 0;
4135
- const isButton2 = raw.button === true;
4136
- const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
4137
- const excludes = isButton2 ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
4232
+ const isButton = raw.button === true;
4233
+ const includes = isButton ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
4234
+ const excludes = isButton ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
4138
4235
  const bindIds = () => {
4139
4236
  const cachedBind = fieldBindIdsCache.get(id);
4140
4237
  if (cachedBind) return cachedBind;
@@ -4171,7 +4268,7 @@ function createNodeIndex(builder) {
4171
4268
  );
4172
4269
  },
4173
4270
  getDescendants(tagId) {
4174
- return resolveDescendants(id, includes, tagId, !isButton2);
4271
+ return resolveDescendants(id, includes, tagId, !isButton);
4175
4272
  }
4176
4273
  };
4177
4274
  fieldNodeCache.set(id, node);
@@ -4508,22 +4605,15 @@ function compilePolicies(raw) {
4508
4605
 
4509
4606
  // src/core/service-filter.ts
4510
4607
  function filterServicesForVisibleGroup(input, deps) {
4511
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4608
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
4512
4609
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
4513
4610
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
4514
4611
  const { context } = input;
4515
4612
  const usedSet = new Set(context.usedServiceIds.map(String));
4516
- const primary = context.usedServiceIds[0];
4517
4613
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
4518
4614
  const resolvedRatePolicy = normalizeRatePolicy(
4519
4615
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
4520
4616
  );
4521
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
4522
- const fb = {
4523
- ...DEFAULT_FALLBACK_SETTINGS,
4524
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
4525
- ratePolicy: resolvedRatePolicy
4526
- };
4527
4617
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
4528
4618
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
4529
4619
  deps.builder,
@@ -4551,7 +4641,15 @@ function filterServicesForVisibleGroup(input, deps) {
4551
4641
  cap.id,
4552
4642
  (_k = context.effectiveConstraints) != null ? _k : {}
4553
4643
  );
4554
- const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
4644
+ const passesRate2 = candidatePassesRateCoherence(
4645
+ deps.builder,
4646
+ svcMap,
4647
+ context.tagId,
4648
+ (_l = context.selectedButtons) != null ? _l : [],
4649
+ context.usedServiceIds,
4650
+ id,
4651
+ resolvedRatePolicy
4652
+ );
4555
4653
  const polRes = evaluatePoliciesRaw(
4556
4654
  policySource,
4557
4655
  [...context.usedServiceIds, id],
@@ -4670,7 +4768,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
4670
4768
  const fields = (_b = props.fields) != null ? _b : [];
4671
4769
  const tag = tags.find((t) => t.id === tagId);
4672
4770
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
4673
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
4771
+ const visibleFieldIds = new Set(
4772
+ builder.visibleFields(tagId, selectedButtons)
4773
+ );
4674
4774
  for (const field of fields) {
4675
4775
  if (!visibleFieldIds.has(field.id)) continue;
4676
4776
  if (field.service_id != null) {
@@ -4693,8 +4793,7 @@ function matchesRuleFilter(cap, rule, tagId) {
4693
4793
  if (!cap) return false;
4694
4794
  const f = rule.filter;
4695
4795
  if (!f) return true;
4696
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
4697
- return true;
4796
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
4698
4797
  }
4699
4798
  function toStrSet(v) {
4700
4799
  const arr = Array.isArray(v) ? v : [v];
@@ -4702,6 +4801,107 @@ function toStrSet(v) {
4702
4801
  for (const x of arr) s.add(String(x));
4703
4802
  return s;
4704
4803
  }
4804
+ function candidatePassesRateCoherence(builder, serviceMap, tagId, selectedKeys, usedServiceIds, candidateId, ratePolicy) {
4805
+ var _a, _b, _c, _d;
4806
+ if (usedServiceIds.length === 0) return true;
4807
+ const props = builder.getProps();
4808
+ const baseFields = (_a = props.fields) != null ? _a : [];
4809
+ const candidateFieldId = syntheticServiceFieldId("candidate", candidateId, 0);
4810
+ const syntheticFields = [
4811
+ ...usedServiceIds.map((serviceId, index) => ({
4812
+ id: syntheticServiceFieldId("used", serviceId, index),
4813
+ label: `Used service ${String(serviceId)}`,
4814
+ type: "custom",
4815
+ button: true,
4816
+ service_id: serviceId,
4817
+ pricing_role: "base"
4818
+ })),
4819
+ {
4820
+ id: candidateFieldId,
4821
+ label: `Candidate ${String(candidateId)}`,
4822
+ type: "custom",
4823
+ button: true,
4824
+ service_id: candidateId,
4825
+ pricing_role: "base"
4826
+ }
4827
+ ];
4828
+ const fields = [...baseFields, ...syntheticFields];
4829
+ const visibleFieldIds = [
4830
+ ...builder.visibleFields(tagId, selectedKeys),
4831
+ ...syntheticFields.map((field) => field.id)
4832
+ ];
4833
+ const anchoredFilters = ((_b = props.filters) != null ? _b : []).map(
4834
+ (tag) => tag.id === tagId && usedServiceIds[0] != null ? { ...tag, service_id: usedServiceIds[0] } : tag
4835
+ );
4836
+ const validationProps = {
4837
+ ...props,
4838
+ filters: anchoredFilters,
4839
+ fields
4840
+ };
4841
+ const errors = [];
4842
+ const tags = (_c = validationProps.filters) != null ? _c : [];
4843
+ const fieldById = new Map(fields.map((field) => [field.id, field]));
4844
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
4845
+ const v = {
4846
+ props: validationProps,
4847
+ nodeMap: buildNodeMap(validationProps),
4848
+ options: {
4849
+ ...(_d = builder.getOptions) == null ? void 0 : _d.call(builder),
4850
+ serviceMap,
4851
+ ratePolicy
4852
+ },
4853
+ errors,
4854
+ serviceMap,
4855
+ selectedKeys: new Set(selectedKeys),
4856
+ tags,
4857
+ fields,
4858
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
4859
+ tagById,
4860
+ fieldById,
4861
+ fieldsVisibleUnder: () => [],
4862
+ simulatedVisibilityContexts: []
4863
+ };
4864
+ validateRateCoherenceForVisibleContext({
4865
+ v,
4866
+ tagId,
4867
+ selectedKeys,
4868
+ visibleFieldIds,
4869
+ effectMap: buildTriggerEffectMap(validationProps),
4870
+ seen: /* @__PURE__ */ new Set()
4871
+ });
4872
+ return !errors.some(
4873
+ (error) => rateIssueAffectsCandidate(
4874
+ error,
4875
+ candidateId,
4876
+ candidateFieldId,
4877
+ usedServiceIds[0]
4878
+ )
4879
+ );
4880
+ }
4881
+ function syntheticServiceFieldId(kind, serviceId, index) {
4882
+ return `__service_filter_${kind}__:${index}:${String(serviceId)}`;
4883
+ }
4884
+ function rateIssueAffectsCandidate(error, candidateId, candidateFieldId, primaryAnchorId) {
4885
+ var _a, _b, _c, _d;
4886
+ if (error.code !== "rate_coherence_violation") return false;
4887
+ const candidateKey = String(candidateId);
4888
+ const details = (_a = error.details) != null ? _a : {};
4889
+ const anchorKey = primaryAnchorId == null ? void 0 : String(primaryAnchorId);
4890
+ const primaryMatchesAnchor = anchorKey == null || String((_b = details.primary) == null ? void 0 : _b.serviceId) === anchorKey || String((_c = details.primary) == null ? void 0 : _c.service_id) === anchorKey;
4891
+ if (primaryMatchesAnchor && ((_d = details.affectedServiceIds) == null ? void 0 : _d.some(
4892
+ (serviceId) => String(serviceId) === candidateKey
4893
+ ))) {
4894
+ return true;
4895
+ }
4896
+ if (primaryMatchesAnchor && String(error.nodeId) === candidateFieldId) {
4897
+ return true;
4898
+ }
4899
+ return [details.primary, details.candidate].some((ref) => {
4900
+ if (!ref) return false;
4901
+ if (!primaryMatchesAnchor) return false;
4902
+ return String(ref.serviceId) === candidateKey || String(ref.service_id) === candidateKey || String(ref.fieldId) === candidateFieldId || String(ref.nodeId) === candidateFieldId;
4903
+ });
4904
+ }
4705
4905
 
4706
4906
  // src/utils/prune-fallbacks.ts
4707
4907
  function pruneInvalidNodeFallbacks(props, services, settings) {
@@ -6432,7 +6632,7 @@ function setService(ctx, id, input) {
6432
6632
  );
6433
6633
  }
6434
6634
  const isOptionBased2 = Array.isArray(f.options) && f.options.length > 0;
6435
- const isButton2 = !!f.button;
6635
+ const isButton = !!f.button;
6436
6636
  if (nextRole) {
6437
6637
  f.pricing_role = nextRole;
6438
6638
  }
@@ -6448,7 +6648,7 @@ function setService(ctx, id, input) {
6448
6648
  if ("service_id" in f) delete f.service_id;
6449
6649
  return;
6450
6650
  }
6451
- if (!isButton2) {
6651
+ if (!isButton) {
6452
6652
  if (hasSidKey) {
6453
6653
  ctx.api.emit("error", {
6454
6654
  message: "Only button fields (without options) can have a service_id.",