@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.
@@ -1324,13 +1324,7 @@ function resolveRootTags(tags) {
1324
1324
  const roots = tags.filter((t) => !t.bind_id);
1325
1325
  return roots.length ? roots : tags.slice(0, 1);
1326
1326
  }
1327
- function isEffectfulTrigger(v, trigger) {
1328
- var _a, _b, _c, _d, _e, _f;
1329
- const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
1330
- const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
1331
- 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;
1332
- }
1333
- function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
1327
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
1334
1328
  var _a;
1335
1329
  const visible = visibleFieldsUnder(v.props, tagId, {
1336
1330
  selectedKeys
@@ -1339,11 +1333,11 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectfu
1339
1333
  for (const f of visible) {
1340
1334
  if (f.button === true) {
1341
1335
  const t = f.id;
1342
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
1336
+ if (effectfulKeys.has(t)) triggers.push(t);
1343
1337
  }
1344
1338
  for (const o of (_a = f.options) != null ? _a : []) {
1345
- const t = `${f.id}::${o.id}`;
1346
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
1339
+ const t = o.id;
1340
+ if (effectfulKeys.has(t)) triggers.push(t);
1347
1341
  }
1348
1342
  }
1349
1343
  triggers.sort();
@@ -1443,20 +1437,38 @@ function dedupeErrorsInPlace(v, startIndex) {
1443
1437
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
1444
1438
  }
1445
1439
  function validateVisibility(v, options = {}) {
1446
- var _a, _b, _c;
1440
+ var _a, _b, _c, _d, _e;
1441
+ v.simulatedVisibilityContexts = [];
1447
1442
  const simulate = options.simulate === true;
1448
1443
  if (!simulate) {
1449
1444
  runVisibilityRulesOnce(v);
1445
+ for (const tag of v.tags) {
1446
+ v.simulatedVisibilityContexts.push({
1447
+ tagId: tag.id,
1448
+ selectedKeys: Array.from(v.selectedKeys),
1449
+ visibleFieldIds: v.fieldsVisibleUnder(tag.id).map((f) => f.id)
1450
+ });
1451
+ }
1450
1452
  return;
1451
1453
  }
1452
1454
  const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
1453
1455
  const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
1454
1456
  const onlyEffectful = options.onlyEffectfulTriggers !== false;
1457
+ const effectfulKeys = /* @__PURE__ */ new Set();
1458
+ if (onlyEffectful) {
1459
+ for (const key of Object.keys((_c = v.props.includes_for_buttons) != null ? _c : {})) {
1460
+ effectfulKeys.add(key);
1461
+ }
1462
+ for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
1463
+ effectfulKeys.add(key);
1464
+ }
1465
+ }
1455
1466
  const roots = resolveRootTags(v.tags);
1456
1467
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
1457
- const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
1468
+ const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
1458
1469
  const errorsStart = v.errors.length;
1459
1470
  const visited = /* @__PURE__ */ new Set();
1471
+ const seenContexts = /* @__PURE__ */ new Set();
1460
1472
  const stack = [];
1461
1473
  for (const rt of rootTags) {
1462
1474
  stack.push({
@@ -1469,10 +1481,27 @@ function validateVisibility(v, options = {}) {
1469
1481
  while (stack.length) {
1470
1482
  if (validatedStates >= maxStates) break;
1471
1483
  const state = stack.pop();
1472
- const sig = stableKeyOfSelection(state.selected);
1484
+ const sig = `${state.rootTagId}::${stableKeyOfSelection(state.selected)}`;
1473
1485
  if (visited.has(sig)) continue;
1474
1486
  visited.add(sig);
1475
1487
  v.selectedKeys = state.selected;
1488
+ const visibleNow = visibleFieldsUnder(v.props, state.rootTagId, {
1489
+ selectedKeys: state.selected
1490
+ }).map((f) => f.id);
1491
+ const context = {
1492
+ tagId: state.rootTagId,
1493
+ selectedKeys: Array.from(state.selected),
1494
+ visibleFieldIds: visibleNow
1495
+ };
1496
+ const contextKey = [
1497
+ context.tagId,
1498
+ [...context.selectedKeys].sort().join("|"),
1499
+ [...context.visibleFieldIds].sort().join("|")
1500
+ ].join("::");
1501
+ if (!seenContexts.has(contextKey)) {
1502
+ seenContexts.add(contextKey);
1503
+ v.simulatedVisibilityContexts.push(context);
1504
+ }
1476
1505
  validatedStates++;
1477
1506
  runVisibilityRulesOnce(v);
1478
1507
  if (state.depth >= maxDepth) continue;
@@ -1480,7 +1509,7 @@ function validateVisibility(v, options = {}) {
1480
1509
  v,
1481
1510
  state.rootTagId,
1482
1511
  state.selected,
1483
- onlyEffectful
1512
+ effectfulKeys
1484
1513
  );
1485
1514
  for (let i = triggers.length - 1; i >= 0; i--) {
1486
1515
  const trig = triggers[i];
@@ -1722,8 +1751,8 @@ function validateOptionMaps(v) {
1722
1751
  };
1723
1752
  }
1724
1753
  if (ref.kind === "field") {
1725
- const isButton2 = ref.node.button === true;
1726
- if (!isButton2)
1754
+ const isButton = ref.node.button === true;
1755
+ if (!isButton)
1727
1756
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
1728
1757
  return { ok: true, nodeId: ref.id, affected: [ref.id] };
1729
1758
  }
@@ -1904,10 +1933,19 @@ function validateOrderKinds(v) {
1904
1933
  }
1905
1934
 
1906
1935
  // src/core/validate/steps/service-vs-input.ts
1936
+ function hasButtonTriggerMap(v, fieldId) {
1937
+ var _a, _b;
1938
+ const includes = (_a = v.props.includes_for_buttons) == null ? void 0 : _a[fieldId];
1939
+ const excludes = (_b = v.props.excludes_for_buttons) == null ? void 0 : _b[fieldId];
1940
+ return Array.isArray(includes) && includes.length > 0 || Array.isArray(excludes) && excludes.length > 0;
1941
+ }
1907
1942
  function validateServiceVsUserInput(v) {
1908
1943
  for (const f of v.fields) {
1909
1944
  const anySvc = hasAnyServiceOption(f);
1910
1945
  const hasName = !!(f.name && f.name.trim());
1946
+ const isButton = f.button === true;
1947
+ const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
1948
+ const hasTriggerMap = isButton && hasButtonTriggerMap(v, f.id);
1911
1949
  if (f.type === "custom" && anySvc) {
1912
1950
  v.errors.push({
1913
1951
  code: "user_input_field_has_service_option",
@@ -1918,14 +1956,15 @@ function validateServiceVsUserInput(v) {
1918
1956
  });
1919
1957
  }
1920
1958
  if (!hasName) {
1921
- if (!anySvc) {
1922
- v.errors.push({
1923
- code: "service_field_missing_service_id",
1924
- severity: "error",
1925
- message: `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
1926
- nodeId: f.id
1927
- });
1959
+ if (hasFieldService || anySvc || hasTriggerMap) {
1960
+ continue;
1928
1961
  }
1962
+ v.errors.push({
1963
+ code: "service_field_missing_service_id",
1964
+ severity: "error",
1965
+ 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.`,
1966
+ nodeId: f.id
1967
+ });
1929
1968
  } else {
1930
1969
  if (anySvc) {
1931
1970
  v.errors.push({
@@ -2242,6 +2281,324 @@ function validateRates(v) {
2242
2281
  }
2243
2282
  }
2244
2283
 
2284
+ // src/core/rate-coherence.ts
2285
+ function buildTriggerEffectMap(props) {
2286
+ var _a, _b;
2287
+ const map = /* @__PURE__ */ new Map();
2288
+ const ensure = (key) => {
2289
+ let item = map.get(key);
2290
+ if (!item) {
2291
+ item = { includes: /* @__PURE__ */ new Set(), excludes: /* @__PURE__ */ new Set() };
2292
+ map.set(key, item);
2293
+ }
2294
+ return item;
2295
+ };
2296
+ for (const [key, ids] of Object.entries((_a = props.includes_for_buttons) != null ? _a : {})) {
2297
+ const item = ensure(key);
2298
+ for (const id of ids != null ? ids : []) item.includes.add(id);
2299
+ }
2300
+ for (const [key, ids] of Object.entries((_b = props.excludes_for_buttons) != null ? _b : {})) {
2301
+ const item = ensure(key);
2302
+ for (const id of ids != null ? ids : []) item.excludes.add(id);
2303
+ }
2304
+ return map;
2305
+ }
2306
+ function isRefExcludedBySelectedKeys(ref, selectedKeys, effectMap) {
2307
+ for (const key of selectedKeys) {
2308
+ const effects = effectMap.get(key);
2309
+ if (!effects) continue;
2310
+ if (ref.fieldId && effects.excludes.has(ref.fieldId) || effects.excludes.has(ref.nodeId)) {
2311
+ return true;
2312
+ }
2313
+ }
2314
+ return false;
2315
+ }
2316
+
2317
+ // src/core/validate/steps/rate-coherence.ts
2318
+ function normalizeRole(role, fallback) {
2319
+ return role === "base" || role === "utility" ? role : fallback;
2320
+ }
2321
+ function uniqueStrings(values) {
2322
+ const out = /* @__PURE__ */ new Set();
2323
+ for (const value of values) {
2324
+ if (!value) continue;
2325
+ out.add(value);
2326
+ }
2327
+ return Array.from(out);
2328
+ }
2329
+ function getRate(serviceMap, serviceId) {
2330
+ const cap = getServiceCapability(serviceMap, serviceId);
2331
+ const rate = cap == null ? void 0 : cap.rate;
2332
+ if (typeof rate !== "number" || !Number.isFinite(rate)) return void 0;
2333
+ return rate;
2334
+ }
2335
+ function collectContextRefs(tag, visibleFields, serviceMap) {
2336
+ var _a, _b, _c, _d, _e;
2337
+ const serviceRefs = [];
2338
+ let tagDefault;
2339
+ if (tag.service_id !== void 0 && tag.service_id !== null) {
2340
+ const tagRate = getRate(serviceMap, tag.service_id);
2341
+ if (tagRate != null) {
2342
+ tagDefault = {
2343
+ key: tag.id,
2344
+ nodeId: tag.id,
2345
+ nodeKind: "tag",
2346
+ serviceId: tag.service_id,
2347
+ rate: tagRate,
2348
+ label: (_a = tag.label) != null ? _a : tag.id,
2349
+ pricingRole: "base"
2350
+ };
2351
+ }
2352
+ }
2353
+ for (const field of visibleFields) {
2354
+ const fieldRole = normalizeRole(field.pricing_role, "base");
2355
+ if (field.service_id !== void 0 && field.service_id !== null) {
2356
+ const rate = getRate(serviceMap, field.service_id);
2357
+ if (rate != null) {
2358
+ serviceRefs.push({
2359
+ key: field.id,
2360
+ nodeId: field.id,
2361
+ fieldId: field.id,
2362
+ nodeKind: "button",
2363
+ serviceId: field.service_id,
2364
+ rate,
2365
+ label: (_b = field.label) != null ? _b : field.id,
2366
+ pricingRole: fieldRole
2367
+ });
2368
+ }
2369
+ }
2370
+ for (const option of (_c = field.options) != null ? _c : []) {
2371
+ if (option.service_id === void 0 || option.service_id === null) continue;
2372
+ const rate = getRate(serviceMap, option.service_id);
2373
+ if (rate == null) continue;
2374
+ serviceRefs.push({
2375
+ key: option.id,
2376
+ nodeId: option.id,
2377
+ fieldId: field.id,
2378
+ nodeKind: "option",
2379
+ serviceId: option.service_id,
2380
+ rate,
2381
+ label: (_d = option.label) != null ? _d : option.id,
2382
+ pricingRole: normalizeRole((_e = option.pricing_role) != null ? _e : field.pricing_role, "base")
2383
+ });
2384
+ }
2385
+ }
2386
+ return { tagDefault, serviceRefs };
2387
+ }
2388
+ function pickHighestRatePrimary(refs) {
2389
+ return refs.reduce((best, cur) => {
2390
+ if (!best) return cur;
2391
+ if (cur.rate > best.rate) return cur;
2392
+ if (cur.rate < best.rate) return best;
2393
+ return cur.nodeId < best.nodeId ? cur : best;
2394
+ }, void 0);
2395
+ }
2396
+ function validateRateCoherenceForVisibleContext(params) {
2397
+ const { v, tagId, selectedKeys, visibleFieldIds, effectMap, seen } = params;
2398
+ const tag = v.tagById.get(tagId);
2399
+ if (!tag) return;
2400
+ const visibleFields = visibleFieldIds.map((id) => v.fieldById.get(id)).filter(Boolean);
2401
+ const { tagDefault, serviceRefs: allServiceRefs } = collectContextRefs(
2402
+ tag,
2403
+ visibleFields,
2404
+ v.serviceMap
2405
+ );
2406
+ const baseRefs = allServiceRefs.filter((ref) => ref.pricingRole === "base");
2407
+ if (baseRefs.length === 0 && !tagDefault) return;
2408
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
2409
+ const visibleInvalidFieldIds = visibleFieldIds.filter(
2410
+ (fieldId) => v.invalidRateFieldIds.has(fieldId)
2411
+ );
2412
+ for (const fieldId of visibleInvalidFieldIds) {
2413
+ const internalKey = [
2414
+ "rate-coherence-internal",
2415
+ tagId,
2416
+ [...selectedKeys].sort().join("|"),
2417
+ fieldId
2418
+ ].join("::");
2419
+ if (seen.has(internalKey)) continue;
2420
+ seen.add(internalKey);
2421
+ v.errors.push({
2422
+ code: "rate_coherence_violation",
2423
+ severity: "error",
2424
+ nodeId: fieldId,
2425
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
2426
+ details: {
2427
+ kind: "internal_field",
2428
+ tagId,
2429
+ selectedKeys: [...selectedKeys],
2430
+ visibleFieldIds: [...visibleFieldIds],
2431
+ fieldId,
2432
+ invalidFieldIds: [fieldId],
2433
+ affectedIds: uniqueStrings([tagId, ...selectedKeys, fieldId])
2434
+ }
2435
+ });
2436
+ }
2437
+ const selectedSet = new Set(selectedKeys);
2438
+ const selectedServiceRefs = baseRefs.filter((ref) => selectedSet.has(ref.key));
2439
+ if (baseRefs.length === 0) return;
2440
+ for (let i = 0; i < baseRefs.length; i++) {
2441
+ for (let j = i + 1; j < baseRefs.length; j++) {
2442
+ const left = baseRefs[i];
2443
+ const right = baseRefs[j];
2444
+ const hypotheticalKeys = [...selectedKeys, left.key, right.key];
2445
+ const survivingRefs = baseRefs.filter(
2446
+ (ref) => !isRefExcludedBySelectedKeys(
2447
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
2448
+ hypotheticalKeys,
2449
+ effectMap
2450
+ )
2451
+ );
2452
+ const survivingSet = new Set(survivingRefs.map((ref) => ref.nodeId));
2453
+ if (!survivingSet.has(left.nodeId) || !survivingSet.has(right.nodeId)) {
2454
+ continue;
2455
+ }
2456
+ if (survivingRefs.length <= 1) continue;
2457
+ const survivingSelected = survivingRefs.filter(
2458
+ (ref) => selectedSet.has(ref.key)
2459
+ );
2460
+ const tagIsCompeting = survivingSelected.length === 0;
2461
+ const primary = pickHighestRatePrimary(survivingRefs);
2462
+ if (!primary) continue;
2463
+ const comparePool = survivingRefs.filter((ref) => ref.nodeId !== primary.nodeId);
2464
+ for (const candidate of comparePool) {
2465
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) continue;
2466
+ const issueKey = [
2467
+ "rate-coherence-context",
2468
+ tagId,
2469
+ [...selectedKeys].sort().join("|"),
2470
+ [...survivingRefs.map((r) => r.nodeId).sort()].join("|"),
2471
+ primary.nodeId,
2472
+ candidate.nodeId,
2473
+ ratePolicy.kind,
2474
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2475
+ ].join("::");
2476
+ if (seen.has(issueKey)) continue;
2477
+ seen.add(issueKey);
2478
+ v.errors.push({
2479
+ code: "rate_coherence_violation",
2480
+ severity: "error",
2481
+ nodeId: candidate.nodeId,
2482
+ message: "Visible service context contains incompatible base service rates.",
2483
+ details: {
2484
+ kind: "selected_context",
2485
+ tagId,
2486
+ selectedKeys: [...selectedKeys],
2487
+ visibleFieldIds: [...visibleFieldIds],
2488
+ primary: {
2489
+ nodeId: primary.nodeId,
2490
+ fieldId: primary.fieldId,
2491
+ service_id: primary.serviceId,
2492
+ serviceId: primary.serviceId,
2493
+ rate: primary.rate
2494
+ },
2495
+ candidate: {
2496
+ nodeId: candidate.nodeId,
2497
+ fieldId: candidate.fieldId,
2498
+ service_id: candidate.serviceId,
2499
+ serviceId: candidate.serviceId,
2500
+ rate: candidate.rate
2501
+ },
2502
+ policy: ratePolicy.kind,
2503
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2504
+ invalidFieldIds: visibleInvalidFieldIds,
2505
+ affectedIds: uniqueStrings([
2506
+ tagId,
2507
+ ...selectedKeys,
2508
+ primary.nodeId,
2509
+ primary.fieldId,
2510
+ candidate.nodeId,
2511
+ candidate.fieldId,
2512
+ tagIsCompeting ? tagDefault == null ? void 0 : tagDefault.nodeId : void 0
2513
+ ]),
2514
+ affectedServiceIds: uniqueStrings([
2515
+ String(primary.serviceId),
2516
+ String(candidate.serviceId)
2517
+ ])
2518
+ }
2519
+ });
2520
+ }
2521
+ }
2522
+ }
2523
+ if (selectedServiceRefs.length === 0 && tagDefault && baseRefs.length > 0) {
2524
+ const survivingByDefault = baseRefs.filter(
2525
+ (ref) => !isRefExcludedBySelectedKeys(
2526
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
2527
+ selectedKeys,
2528
+ effectMap
2529
+ )
2530
+ );
2531
+ for (const candidate of survivingByDefault) {
2532
+ if (passesRatePolicy(ratePolicy, tagDefault.rate, candidate.rate)) continue;
2533
+ const issueKey = [
2534
+ "rate-coherence-default",
2535
+ tagId,
2536
+ [...selectedKeys].sort().join("|"),
2537
+ tagDefault.nodeId,
2538
+ candidate.nodeId,
2539
+ ratePolicy.kind,
2540
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
2541
+ ].join("::");
2542
+ if (seen.has(issueKey)) continue;
2543
+ seen.add(issueKey);
2544
+ v.errors.push({
2545
+ code: "rate_coherence_violation",
2546
+ severity: "error",
2547
+ nodeId: candidate.nodeId,
2548
+ message: "Visible service context contains incompatible base service rates.",
2549
+ details: {
2550
+ kind: "selected_context",
2551
+ tagId,
2552
+ selectedKeys: [...selectedKeys],
2553
+ visibleFieldIds: [...visibleFieldIds],
2554
+ primary: {
2555
+ nodeId: tagDefault.nodeId,
2556
+ service_id: tagDefault.serviceId,
2557
+ serviceId: tagDefault.serviceId,
2558
+ rate: tagDefault.rate
2559
+ },
2560
+ candidate: {
2561
+ nodeId: candidate.nodeId,
2562
+ fieldId: candidate.fieldId,
2563
+ service_id: candidate.serviceId,
2564
+ serviceId: candidate.serviceId,
2565
+ rate: candidate.rate
2566
+ },
2567
+ policy: ratePolicy.kind,
2568
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
2569
+ invalidFieldIds: visibleInvalidFieldIds,
2570
+ affectedIds: uniqueStrings([
2571
+ tagId,
2572
+ ...selectedKeys,
2573
+ tagDefault.nodeId,
2574
+ candidate.nodeId,
2575
+ candidate.fieldId
2576
+ ]),
2577
+ affectedServiceIds: uniqueStrings([
2578
+ String(tagDefault.serviceId),
2579
+ String(candidate.serviceId)
2580
+ ])
2581
+ }
2582
+ });
2583
+ }
2584
+ }
2585
+ }
2586
+ function validateRateCoherence(v) {
2587
+ if (Object.keys(v.serviceMap).length === 0 || v.tags.length === 0) return;
2588
+ const effectMap = buildTriggerEffectMap(v.props);
2589
+ const seen = /* @__PURE__ */ new Set();
2590
+ for (const context of v.simulatedVisibilityContexts) {
2591
+ validateRateCoherenceForVisibleContext({
2592
+ v,
2593
+ tagId: context.tagId,
2594
+ selectedKeys: context.selectedKeys,
2595
+ visibleFieldIds: context.visibleFieldIds,
2596
+ effectMap,
2597
+ seen
2598
+ });
2599
+ }
2600
+ }
2601
+
2245
2602
  // src/core/validate/steps/constraints.ts
2246
2603
  function constraintKeysInChain(v, tagId) {
2247
2604
  const keys = [];
@@ -2998,6 +3355,84 @@ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2998
3355
  };
2999
3356
  }
3000
3357
 
3358
+ // src/core/validate/index.ts
3359
+ function readVisibilitySimOpts(ctx) {
3360
+ const c = ctx;
3361
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
3362
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
3363
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
3364
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
3365
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
3366
+ return {
3367
+ simulate,
3368
+ maxStates,
3369
+ maxDepth,
3370
+ simulateAllRoots,
3371
+ onlyEffectfulTriggers
3372
+ };
3373
+ }
3374
+ function validate(props, ctx = {}) {
3375
+ var _a, _b, _c;
3376
+ const options = mergeValidatorOptions({}, ctx);
3377
+ const fallbackSettings = resolveFallbackSettings(options);
3378
+ const ratePolicy = resolveGlobalRatePolicy(options);
3379
+ const errors = [];
3380
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3381
+ const selectedKeys = new Set(
3382
+ (_b = options.selectedOptionKeys) != null ? _b : []
3383
+ );
3384
+ const tags = Array.isArray(props.filters) ? props.filters : [];
3385
+ const fields = Array.isArray(props.fields) ? props.fields : [];
3386
+ const tagById = /* @__PURE__ */ new Map();
3387
+ const fieldById = /* @__PURE__ */ new Map();
3388
+ for (const t of tags) tagById.set(t.id, t);
3389
+ for (const f of fields) fieldById.set(f.id, f);
3390
+ const v = {
3391
+ props,
3392
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3393
+ options: {
3394
+ ...options,
3395
+ ratePolicy,
3396
+ fallbackSettings
3397
+ },
3398
+ errors,
3399
+ serviceMap,
3400
+ selectedKeys,
3401
+ tags,
3402
+ fields,
3403
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
3404
+ tagById,
3405
+ fieldById,
3406
+ fieldsVisibleUnder: (_tagId) => [],
3407
+ simulatedVisibilityContexts: []
3408
+ };
3409
+ validateStructure(v);
3410
+ validateIdentity(v);
3411
+ validateOptionMaps(v);
3412
+ validateOrderKinds(v);
3413
+ v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3414
+ const visSim = readVisibilitySimOpts(options);
3415
+ validateVisibility(v, visSim);
3416
+ applyPolicies(
3417
+ v.errors,
3418
+ v.props,
3419
+ v.serviceMap,
3420
+ v.options.policies,
3421
+ v.fieldsVisibleUnder,
3422
+ v.tags
3423
+ );
3424
+ validateServiceVsUserInput(v);
3425
+ validateUtilityMarkers(v);
3426
+ validateRates(v);
3427
+ validateRateCoherence(v);
3428
+ validateConstraints(v);
3429
+ validateCustomFields(v);
3430
+ validateGlobalUtilityGuard(v);
3431
+ validateUnboundFields(v);
3432
+ validateFallbacks(v);
3433
+ return v.errors;
3434
+ }
3435
+
3001
3436
  // src/core/builder.ts
3002
3437
  var import_lodash_es2 = require("lodash-es");
3003
3438
  function createBuilder(opts = {}) {
@@ -3280,344 +3715,6 @@ function toStringSet(v) {
3280
3715
  return new Set(v.map(String));
3281
3716
  }
3282
3717
 
3283
- // src/core/rate-coherence.ts
3284
- function validateRateCoherenceDeep(params) {
3285
- var _a, _b, _c;
3286
- const { builder, services, tagId } = params;
3287
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
3288
- const props = builder.getProps();
3289
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
3290
- const fields = (_b = props.fields) != null ? _b : [];
3291
- const fieldById = new Map(fields.map((f) => [f.id, f]));
3292
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
3293
- const tag = tagById.get(tagId);
3294
- const baselineFieldIds = builder.visibleFields(tagId, []);
3295
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
3296
- const anchors = collectAnchors(baselineFields);
3297
- const diagnostics = [];
3298
- const seen = /* @__PURE__ */ new Set();
3299
- for (const anchor of anchors) {
3300
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
3301
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
3302
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
3303
- for (const fieldId of visibleInvalidFieldIds) {
3304
- const key = `internal|${tagId}|${fieldId}`;
3305
- if (seen.has(key)) continue;
3306
- seen.add(key);
3307
- diagnostics.push({
3308
- kind: "internal_field",
3309
- scope: "visible_group",
3310
- tagId,
3311
- fieldId,
3312
- nodeId: fieldId,
3313
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
3314
- simulationAnchor: {
3315
- kind: anchor.kind,
3316
- id: anchor.id,
3317
- fieldId: anchor.fieldId,
3318
- label: anchor.label
3319
- },
3320
- invalidFieldIds: [fieldId]
3321
- });
3322
- }
3323
- const references = visibleFields.flatMap(
3324
- (field) => collectFieldReferences(field, services)
3325
- );
3326
- if (references.length <= 1) continue;
3327
- const primary = references.reduce((best, current) => {
3328
- if (current.rate !== best.rate) {
3329
- return current.rate > best.rate ? current : best;
3330
- }
3331
- const bestKey = `${best.fieldId}|${best.nodeId}`;
3332
- const currentKey = `${current.fieldId}|${current.nodeId}`;
3333
- return currentKey < bestKey ? current : best;
3334
- });
3335
- for (const candidate of references) {
3336
- if (candidate.nodeId === primary.nodeId) continue;
3337
- if (candidate.fieldId === primary.fieldId) continue;
3338
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
3339
- continue;
3340
- }
3341
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
3342
- if (seen.has(key)) continue;
3343
- seen.add(key);
3344
- diagnostics.push({
3345
- kind: "contextual",
3346
- scope: "visible_group",
3347
- tagId,
3348
- nodeId: candidate.nodeId,
3349
- primary: toDiagnosticRef(primary),
3350
- offender: toDiagnosticRef(candidate),
3351
- policy: ratePolicy.kind,
3352
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
3353
- message: explainRateMismatch(
3354
- ratePolicy,
3355
- primary,
3356
- candidate,
3357
- describeLabel(tag)
3358
- ),
3359
- simulationAnchor: {
3360
- kind: anchor.kind,
3361
- id: anchor.id,
3362
- fieldId: anchor.fieldId,
3363
- label: anchor.label
3364
- },
3365
- invalidFieldIds: visibleInvalidFieldIds
3366
- });
3367
- }
3368
- }
3369
- return diagnostics;
3370
- }
3371
- function collectAnchors(fields) {
3372
- var _a, _b;
3373
- const anchors = [];
3374
- for (const field of fields) {
3375
- if (!isButton(field)) continue;
3376
- if (Array.isArray(field.options) && field.options.length > 0) {
3377
- for (const option of field.options) {
3378
- anchors.push({
3379
- kind: "option",
3380
- id: option.id,
3381
- fieldId: field.id,
3382
- label: (_a = option.label) != null ? _a : option.id
3383
- });
3384
- }
3385
- continue;
3386
- }
3387
- anchors.push({
3388
- kind: "field",
3389
- id: field.id,
3390
- fieldId: field.id,
3391
- label: (_b = field.label) != null ? _b : field.id
3392
- });
3393
- }
3394
- return anchors;
3395
- }
3396
- function collectFieldReferences(field, services) {
3397
- var _a;
3398
- const members = collectBaseMembers(field, services);
3399
- if (members.length === 0) return [];
3400
- if (isMultiField(field)) {
3401
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
3402
- return [
3403
- {
3404
- refKind: "multi",
3405
- nodeId: field.id,
3406
- fieldId: field.id,
3407
- label: (_a = field.label) != null ? _a : field.id,
3408
- rate: averageRate,
3409
- members
3410
- }
3411
- ];
3412
- }
3413
- return members.map((member) => ({
3414
- refKind: "single",
3415
- nodeId: member.id,
3416
- fieldId: field.id,
3417
- label: member.label,
3418
- rate: member.rate,
3419
- service_id: member.service_id,
3420
- members: [member]
3421
- }));
3422
- }
3423
- function collectBaseMembers(field, services) {
3424
- var _a, _b, _c;
3425
- const members = [];
3426
- if (Array.isArray(field.options) && field.options.length > 0) {
3427
- for (const option of field.options) {
3428
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
3429
- if (role2 !== "base") continue;
3430
- if (option.service_id === void 0 || option.service_id === null) {
3431
- continue;
3432
- }
3433
- const cap2 = getServiceCapability(services, option.service_id);
3434
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
3435
- continue;
3436
- }
3437
- members.push({
3438
- kind: "option",
3439
- id: option.id,
3440
- fieldId: field.id,
3441
- label: (_b = option.label) != null ? _b : option.id,
3442
- service_id: option.service_id,
3443
- rate: cap2.rate
3444
- });
3445
- }
3446
- return members;
3447
- }
3448
- const role = normalizeRole(field.pricing_role, "base");
3449
- if (role !== "base") return members;
3450
- if (field.service_id === void 0 || field.service_id === null) return members;
3451
- const cap = getServiceCapability(services, field.service_id);
3452
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
3453
- return members;
3454
- }
3455
- members.push({
3456
- kind: "field",
3457
- id: field.id,
3458
- fieldId: field.id,
3459
- label: (_c = field.label) != null ? _c : field.id,
3460
- service_id: field.service_id,
3461
- rate: cap.rate
3462
- });
3463
- return members;
3464
- }
3465
- function isButton(field) {
3466
- if (field.button === true) return true;
3467
- return Array.isArray(field.options) && field.options.length > 0;
3468
- }
3469
- function normalizeRole(role, fallback) {
3470
- return role === "base" || role === "utility" ? role : fallback;
3471
- }
3472
- function toDiagnosticRef(reference) {
3473
- return {
3474
- nodeId: reference.nodeId,
3475
- fieldId: reference.fieldId,
3476
- label: reference.label,
3477
- refKind: reference.refKind,
3478
- service_id: reference.service_id,
3479
- rate: reference.rate
3480
- };
3481
- }
3482
- function contextualKey(tagId, primary, candidate, ratePolicy) {
3483
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
3484
- return [
3485
- "contextual",
3486
- tagId,
3487
- primary.fieldId,
3488
- primary.nodeId,
3489
- candidate.fieldId,
3490
- candidate.nodeId,
3491
- `${ratePolicy.kind}${pctKey}`
3492
- ].join("|");
3493
- }
3494
- function describeLabel(tag) {
3495
- var _a, _b;
3496
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
3497
- }
3498
- function explainRateMismatch(policy, primary, candidate, where) {
3499
- var _a, _b;
3500
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
3501
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
3502
- switch (policy.kind) {
3503
- case "eq_primary":
3504
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
3505
- case "lte_primary":
3506
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
3507
- case "within_pct":
3508
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
3509
- case "at_least_pct_lower":
3510
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
3511
- }
3512
- }
3513
-
3514
- // src/core/validate/index.ts
3515
- function readVisibilitySimOpts(ctx) {
3516
- const c = ctx;
3517
- const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
3518
- const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
3519
- const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
3520
- const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
3521
- const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
3522
- return {
3523
- simulate,
3524
- maxStates,
3525
- maxDepth,
3526
- simulateAllRoots,
3527
- onlyEffectfulTriggers
3528
- };
3529
- }
3530
- function validate(props, ctx = {}) {
3531
- var _a, _b, _c;
3532
- const options = mergeValidatorOptions({}, ctx);
3533
- const fallbackSettings = resolveFallbackSettings(options);
3534
- const ratePolicy = resolveGlobalRatePolicy(options);
3535
- const errors = [];
3536
- const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3537
- const selectedKeys = new Set(
3538
- (_b = options.selectedOptionKeys) != null ? _b : []
3539
- );
3540
- const tags = Array.isArray(props.filters) ? props.filters : [];
3541
- const fields = Array.isArray(props.fields) ? props.fields : [];
3542
- const tagById = /* @__PURE__ */ new Map();
3543
- const fieldById = /* @__PURE__ */ new Map();
3544
- for (const t of tags) tagById.set(t.id, t);
3545
- for (const f of fields) fieldById.set(f.id, f);
3546
- const v = {
3547
- props,
3548
- nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3549
- options: {
3550
- ...options,
3551
- ratePolicy,
3552
- fallbackSettings
3553
- },
3554
- errors,
3555
- serviceMap,
3556
- selectedKeys,
3557
- tags,
3558
- fields,
3559
- invalidRateFieldIds: /* @__PURE__ */ new Set(),
3560
- tagById,
3561
- fieldById,
3562
- fieldsVisibleUnder: (_tagId) => []
3563
- };
3564
- validateStructure(v);
3565
- validateIdentity(v);
3566
- validateOptionMaps(v);
3567
- validateOrderKinds(v);
3568
- v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3569
- const visSim = readVisibilitySimOpts(options);
3570
- validateVisibility(v, visSim);
3571
- applyPolicies(
3572
- v.errors,
3573
- v.props,
3574
- v.serviceMap,
3575
- v.options.policies,
3576
- v.fieldsVisibleUnder,
3577
- v.tags
3578
- );
3579
- validateServiceVsUserInput(v);
3580
- validateUtilityMarkers(v);
3581
- validateRates(v);
3582
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
3583
- const builder = createBuilder({ serviceMap });
3584
- builder.load(props);
3585
- for (const tag of tags) {
3586
- const diags = validateRateCoherenceDeep({
3587
- builder,
3588
- services: serviceMap,
3589
- tagId: tag.id,
3590
- ratePolicy,
3591
- invalidFieldIds: v.invalidRateFieldIds
3592
- });
3593
- for (const diag of diags) {
3594
- if (diag.kind !== "contextual") continue;
3595
- errors.push({
3596
- code: "rate_coherence_violation",
3597
- severity: "error",
3598
- message: diag.message,
3599
- nodeId: diag.nodeId,
3600
- details: {
3601
- tagId: diag.tagId,
3602
- simulationAnchor: diag.simulationAnchor,
3603
- primary: diag.primary,
3604
- offender: diag.offender,
3605
- policy: diag.policy,
3606
- policyPct: diag.policyPct,
3607
- invalidFieldIds: diag.invalidFieldIds
3608
- }
3609
- });
3610
- }
3611
- }
3612
- }
3613
- validateConstraints(v);
3614
- validateCustomFields(v);
3615
- validateGlobalUtilityGuard(v);
3616
- validateUnboundFields(v);
3617
- validateFallbacks(v);
3618
- return v.errors;
3619
- }
3620
-
3621
3718
  // src/core/fallback.ts
3622
3719
  var DEFAULT_SETTINGS = {
3623
3720
  requireConstraintFit: true,
@@ -4196,9 +4293,9 @@ function createNodeIndex(builder) {
4196
4293
  if (cached) return cached;
4197
4294
  const raw = fieldById.get(id);
4198
4295
  if (!raw) return void 0;
4199
- const isButton2 = raw.button === true;
4200
- const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
4201
- const excludes = isButton2 ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
4296
+ const isButton = raw.button === true;
4297
+ const includes = isButton ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
4298
+ const excludes = isButton ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
4202
4299
  const bindIds = () => {
4203
4300
  const cachedBind = fieldBindIdsCache.get(id);
4204
4301
  if (cachedBind) return cachedBind;
@@ -4235,7 +4332,7 @@ function createNodeIndex(builder) {
4235
4332
  );
4236
4333
  },
4237
4334
  getDescendants(tagId) {
4238
- return resolveDescendants(id, includes, tagId, !isButton2);
4335
+ return resolveDescendants(id, includes, tagId, !isButton);
4239
4336
  }
4240
4337
  };
4241
4338
  fieldNodeCache.set(id, node);
@@ -4572,22 +4669,15 @@ function compilePolicies(raw) {
4572
4669
 
4573
4670
  // src/core/service-filter.ts
4574
4671
  function filterServicesForVisibleGroup(input, deps) {
4575
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4672
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
4576
4673
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
4577
4674
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
4578
4675
  const { context } = input;
4579
4676
  const usedSet = new Set(context.usedServiceIds.map(String));
4580
- const primary = context.usedServiceIds[0];
4581
4677
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
4582
4678
  const resolvedRatePolicy = normalizeRatePolicy(
4583
4679
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
4584
4680
  );
4585
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
4586
- const fb = {
4587
- ...DEFAULT_FALLBACK_SETTINGS,
4588
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
4589
- ratePolicy: resolvedRatePolicy
4590
- };
4591
4681
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
4592
4682
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
4593
4683
  deps.builder,
@@ -4615,7 +4705,15 @@ function filterServicesForVisibleGroup(input, deps) {
4615
4705
  cap.id,
4616
4706
  (_k = context.effectiveConstraints) != null ? _k : {}
4617
4707
  );
4618
- const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
4708
+ const passesRate2 = candidatePassesRateCoherence(
4709
+ deps.builder,
4710
+ svcMap,
4711
+ context.tagId,
4712
+ (_l = context.selectedButtons) != null ? _l : [],
4713
+ context.usedServiceIds,
4714
+ id,
4715
+ resolvedRatePolicy
4716
+ );
4619
4717
  const polRes = evaluatePoliciesRaw(
4620
4718
  policySource,
4621
4719
  [...context.usedServiceIds, id],
@@ -4734,7 +4832,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
4734
4832
  const fields = (_b = props.fields) != null ? _b : [];
4735
4833
  const tag = tags.find((t) => t.id === tagId);
4736
4834
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
4737
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
4835
+ const visibleFieldIds = new Set(
4836
+ builder.visibleFields(tagId, selectedButtons)
4837
+ );
4738
4838
  for (const field of fields) {
4739
4839
  if (!visibleFieldIds.has(field.id)) continue;
4740
4840
  if (field.service_id != null) {
@@ -4757,8 +4857,7 @@ function matchesRuleFilter(cap, rule, tagId) {
4757
4857
  if (!cap) return false;
4758
4858
  const f = rule.filter;
4759
4859
  if (!f) return true;
4760
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
4761
- return true;
4860
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
4762
4861
  }
4763
4862
  function toStrSet(v) {
4764
4863
  const arr = Array.isArray(v) ? v : [v];
@@ -4766,6 +4865,107 @@ function toStrSet(v) {
4766
4865
  for (const x of arr) s.add(String(x));
4767
4866
  return s;
4768
4867
  }
4868
+ function candidatePassesRateCoherence(builder, serviceMap, tagId, selectedKeys, usedServiceIds, candidateId, ratePolicy) {
4869
+ var _a, _b, _c, _d;
4870
+ if (usedServiceIds.length === 0) return true;
4871
+ const props = builder.getProps();
4872
+ const baseFields = (_a = props.fields) != null ? _a : [];
4873
+ const candidateFieldId = syntheticServiceFieldId("candidate", candidateId, 0);
4874
+ const syntheticFields = [
4875
+ ...usedServiceIds.map((serviceId, index) => ({
4876
+ id: syntheticServiceFieldId("used", serviceId, index),
4877
+ label: `Used service ${String(serviceId)}`,
4878
+ type: "custom",
4879
+ button: true,
4880
+ service_id: serviceId,
4881
+ pricing_role: "base"
4882
+ })),
4883
+ {
4884
+ id: candidateFieldId,
4885
+ label: `Candidate ${String(candidateId)}`,
4886
+ type: "custom",
4887
+ button: true,
4888
+ service_id: candidateId,
4889
+ pricing_role: "base"
4890
+ }
4891
+ ];
4892
+ const fields = [...baseFields, ...syntheticFields];
4893
+ const visibleFieldIds = [
4894
+ ...builder.visibleFields(tagId, selectedKeys),
4895
+ ...syntheticFields.map((field) => field.id)
4896
+ ];
4897
+ const anchoredFilters = ((_b = props.filters) != null ? _b : []).map(
4898
+ (tag) => tag.id === tagId && usedServiceIds[0] != null ? { ...tag, service_id: usedServiceIds[0] } : tag
4899
+ );
4900
+ const validationProps = {
4901
+ ...props,
4902
+ filters: anchoredFilters,
4903
+ fields
4904
+ };
4905
+ const errors = [];
4906
+ const tags = (_c = validationProps.filters) != null ? _c : [];
4907
+ const fieldById = new Map(fields.map((field) => [field.id, field]));
4908
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
4909
+ const v = {
4910
+ props: validationProps,
4911
+ nodeMap: buildNodeMap(validationProps),
4912
+ options: {
4913
+ ...(_d = builder.getOptions) == null ? void 0 : _d.call(builder),
4914
+ serviceMap,
4915
+ ratePolicy
4916
+ },
4917
+ errors,
4918
+ serviceMap,
4919
+ selectedKeys: new Set(selectedKeys),
4920
+ tags,
4921
+ fields,
4922
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
4923
+ tagById,
4924
+ fieldById,
4925
+ fieldsVisibleUnder: () => [],
4926
+ simulatedVisibilityContexts: []
4927
+ };
4928
+ validateRateCoherenceForVisibleContext({
4929
+ v,
4930
+ tagId,
4931
+ selectedKeys,
4932
+ visibleFieldIds,
4933
+ effectMap: buildTriggerEffectMap(validationProps),
4934
+ seen: /* @__PURE__ */ new Set()
4935
+ });
4936
+ return !errors.some(
4937
+ (error) => rateIssueAffectsCandidate(
4938
+ error,
4939
+ candidateId,
4940
+ candidateFieldId,
4941
+ usedServiceIds[0]
4942
+ )
4943
+ );
4944
+ }
4945
+ function syntheticServiceFieldId(kind, serviceId, index) {
4946
+ return `__service_filter_${kind}__:${index}:${String(serviceId)}`;
4947
+ }
4948
+ function rateIssueAffectsCandidate(error, candidateId, candidateFieldId, primaryAnchorId) {
4949
+ var _a, _b, _c, _d;
4950
+ if (error.code !== "rate_coherence_violation") return false;
4951
+ const candidateKey = String(candidateId);
4952
+ const details = (_a = error.details) != null ? _a : {};
4953
+ const anchorKey = primaryAnchorId == null ? void 0 : String(primaryAnchorId);
4954
+ 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;
4955
+ if (primaryMatchesAnchor && ((_d = details.affectedServiceIds) == null ? void 0 : _d.some(
4956
+ (serviceId) => String(serviceId) === candidateKey
4957
+ ))) {
4958
+ return true;
4959
+ }
4960
+ if (primaryMatchesAnchor && String(error.nodeId) === candidateFieldId) {
4961
+ return true;
4962
+ }
4963
+ return [details.primary, details.candidate].some((ref) => {
4964
+ if (!ref) return false;
4965
+ if (!primaryMatchesAnchor) return false;
4966
+ return String(ref.serviceId) === candidateKey || String(ref.service_id) === candidateKey || String(ref.fieldId) === candidateFieldId || String(ref.nodeId) === candidateFieldId;
4967
+ });
4968
+ }
4769
4969
 
4770
4970
  // src/utils/prune-fallbacks.ts
4771
4971
  function pruneInvalidNodeFallbacks(props, services, settings) {
@@ -6496,7 +6696,7 @@ function setService(ctx, id, input) {
6496
6696
  );
6497
6697
  }
6498
6698
  const isOptionBased2 = Array.isArray(f.options) && f.options.length > 0;
6499
- const isButton2 = !!f.button;
6699
+ const isButton = !!f.button;
6500
6700
  if (nextRole) {
6501
6701
  f.pricing_role = nextRole;
6502
6702
  }
@@ -6512,7 +6712,7 @@ function setService(ctx, id, input) {
6512
6712
  if ("service_id" in f) delete f.service_id;
6513
6713
  return;
6514
6714
  }
6515
- if (!isButton2) {
6715
+ if (!isButton) {
6516
6716
  if (hasSidKey) {
6517
6717
  ctx.api.emit("error", {
6518
6718
  message: "Only button fields (without options) can have a service_id.",