@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.
@@ -4078,13 +4078,7 @@ function resolveRootTags(tags) {
4078
4078
  const roots = tags.filter((t) => !t.bind_id);
4079
4079
  return roots.length ? roots : tags.slice(0, 1);
4080
4080
  }
4081
- function isEffectfulTrigger(v, trigger) {
4082
- var _a, _b, _c, _d, _e, _f;
4083
- const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
4084
- const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
4085
- 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;
4086
- }
4087
- function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
4081
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
4088
4082
  var _a;
4089
4083
  const visible = visibleFieldsUnder(v.props, tagId, {
4090
4084
  selectedKeys
@@ -4093,11 +4087,11 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectfu
4093
4087
  for (const f of visible) {
4094
4088
  if (f.button === true) {
4095
4089
  const t = f.id;
4096
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
4090
+ if (effectfulKeys.has(t)) triggers.push(t);
4097
4091
  }
4098
4092
  for (const o of (_a = f.options) != null ? _a : []) {
4099
- const t = `${f.id}::${o.id}`;
4100
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
4093
+ const t = o.id;
4094
+ if (effectfulKeys.has(t)) triggers.push(t);
4101
4095
  }
4102
4096
  }
4103
4097
  triggers.sort();
@@ -4197,20 +4191,38 @@ function dedupeErrorsInPlace(v, startIndex) {
4197
4191
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
4198
4192
  }
4199
4193
  function validateVisibility(v, options = {}) {
4200
- var _a, _b, _c;
4194
+ var _a, _b, _c, _d, _e;
4195
+ v.simulatedVisibilityContexts = [];
4201
4196
  const simulate = options.simulate === true;
4202
4197
  if (!simulate) {
4203
4198
  runVisibilityRulesOnce(v);
4199
+ for (const tag of v.tags) {
4200
+ v.simulatedVisibilityContexts.push({
4201
+ tagId: tag.id,
4202
+ selectedKeys: Array.from(v.selectedKeys),
4203
+ visibleFieldIds: v.fieldsVisibleUnder(tag.id).map((f) => f.id)
4204
+ });
4205
+ }
4204
4206
  return;
4205
4207
  }
4206
4208
  const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
4207
4209
  const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
4208
4210
  const onlyEffectful = options.onlyEffectfulTriggers !== false;
4211
+ const effectfulKeys = /* @__PURE__ */ new Set();
4212
+ if (onlyEffectful) {
4213
+ for (const key of Object.keys((_c = v.props.includes_for_buttons) != null ? _c : {})) {
4214
+ effectfulKeys.add(key);
4215
+ }
4216
+ for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
4217
+ effectfulKeys.add(key);
4218
+ }
4219
+ }
4209
4220
  const roots = resolveRootTags(v.tags);
4210
4221
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
4211
- const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
4222
+ const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
4212
4223
  const errorsStart = v.errors.length;
4213
4224
  const visited = /* @__PURE__ */ new Set();
4225
+ const seenContexts = /* @__PURE__ */ new Set();
4214
4226
  const stack = [];
4215
4227
  for (const rt of rootTags) {
4216
4228
  stack.push({
@@ -4223,10 +4235,27 @@ function validateVisibility(v, options = {}) {
4223
4235
  while (stack.length) {
4224
4236
  if (validatedStates >= maxStates) break;
4225
4237
  const state = stack.pop();
4226
- const sig = stableKeyOfSelection(state.selected);
4238
+ const sig = `${state.rootTagId}::${stableKeyOfSelection(state.selected)}`;
4227
4239
  if (visited.has(sig)) continue;
4228
4240
  visited.add(sig);
4229
4241
  v.selectedKeys = state.selected;
4242
+ const visibleNow = visibleFieldsUnder(v.props, state.rootTagId, {
4243
+ selectedKeys: state.selected
4244
+ }).map((f) => f.id);
4245
+ const context = {
4246
+ tagId: state.rootTagId,
4247
+ selectedKeys: Array.from(state.selected),
4248
+ visibleFieldIds: visibleNow
4249
+ };
4250
+ const contextKey = [
4251
+ context.tagId,
4252
+ [...context.selectedKeys].sort().join("|"),
4253
+ [...context.visibleFieldIds].sort().join("|")
4254
+ ].join("::");
4255
+ if (!seenContexts.has(contextKey)) {
4256
+ seenContexts.add(contextKey);
4257
+ v.simulatedVisibilityContexts.push(context);
4258
+ }
4230
4259
  validatedStates++;
4231
4260
  runVisibilityRulesOnce(v);
4232
4261
  if (state.depth >= maxDepth) continue;
@@ -4234,7 +4263,7 @@ function validateVisibility(v, options = {}) {
4234
4263
  v,
4235
4264
  state.rootTagId,
4236
4265
  state.selected,
4237
- onlyEffectful
4266
+ effectfulKeys
4238
4267
  );
4239
4268
  for (let i = triggers.length - 1; i >= 0; i--) {
4240
4269
  const trig = triggers[i];
@@ -4476,8 +4505,8 @@ function validateOptionMaps(v) {
4476
4505
  };
4477
4506
  }
4478
4507
  if (ref.kind === "field") {
4479
- const isButton2 = ref.node.button === true;
4480
- if (!isButton2)
4508
+ const isButton = ref.node.button === true;
4509
+ if (!isButton)
4481
4510
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
4482
4511
  return { ok: true, nodeId: ref.id, affected: [ref.id] };
4483
4512
  }
@@ -4658,10 +4687,19 @@ function validateOrderKinds(v) {
4658
4687
  }
4659
4688
 
4660
4689
  // src/core/validate/steps/service-vs-input.ts
4690
+ function hasButtonTriggerMap(v, fieldId) {
4691
+ var _a, _b;
4692
+ const includes = (_a = v.props.includes_for_buttons) == null ? void 0 : _a[fieldId];
4693
+ const excludes = (_b = v.props.excludes_for_buttons) == null ? void 0 : _b[fieldId];
4694
+ return Array.isArray(includes) && includes.length > 0 || Array.isArray(excludes) && excludes.length > 0;
4695
+ }
4661
4696
  function validateServiceVsUserInput(v) {
4662
4697
  for (const f of v.fields) {
4663
4698
  const anySvc = hasAnyServiceOption(f);
4664
4699
  const hasName = !!(f.name && f.name.trim());
4700
+ const isButton = f.button === true;
4701
+ const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
4702
+ const hasTriggerMap = isButton && hasButtonTriggerMap(v, f.id);
4665
4703
  if (f.type === "custom" && anySvc) {
4666
4704
  v.errors.push({
4667
4705
  code: "user_input_field_has_service_option",
@@ -4672,14 +4710,15 @@ function validateServiceVsUserInput(v) {
4672
4710
  });
4673
4711
  }
4674
4712
  if (!hasName) {
4675
- if (!anySvc) {
4676
- v.errors.push({
4677
- code: "service_field_missing_service_id",
4678
- severity: "error",
4679
- message: `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
4680
- nodeId: f.id
4681
- });
4713
+ if (hasFieldService || anySvc || hasTriggerMap) {
4714
+ continue;
4682
4715
  }
4716
+ v.errors.push({
4717
+ code: "service_field_missing_service_id",
4718
+ severity: "error",
4719
+ 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.`,
4720
+ nodeId: f.id
4721
+ });
4683
4722
  } else {
4684
4723
  if (anySvc) {
4685
4724
  v.errors.push({
@@ -4855,15 +4894,6 @@ function passesRatePolicy(policy, primaryRate, candidateRate) {
4855
4894
  return candidateRate <= primaryRate * (1 - rp.pct / 100);
4856
4895
  }
4857
4896
  }
4858
- function rateOk(svcMap, candidate, primary, policy) {
4859
- const cand = getServiceCapability(svcMap, candidate);
4860
- const prim = getServiceCapability(svcMap, primary);
4861
- if (!cand || !prim) return false;
4862
- const cRate = toFiniteNumber(cand.rate);
4863
- const pRate = toFiniteNumber(prim.rate);
4864
- if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
4865
- return passesRatePolicy(policy.ratePolicy, pRate, cRate);
4866
- }
4867
4897
  function getServiceCapabilityEntry(svcMap, candidate) {
4868
4898
  if (candidate === void 0 || candidate === null) return void 0;
4869
4899
  const direct = svcMap[candidate];
@@ -4991,6 +5021,324 @@ function validateRates(v) {
4991
5021
  }
4992
5022
  }
4993
5023
 
5024
+ // src/core/rate-coherence.ts
5025
+ function buildTriggerEffectMap(props) {
5026
+ var _a, _b;
5027
+ const map = /* @__PURE__ */ new Map();
5028
+ const ensure = (key) => {
5029
+ let item = map.get(key);
5030
+ if (!item) {
5031
+ item = { includes: /* @__PURE__ */ new Set(), excludes: /* @__PURE__ */ new Set() };
5032
+ map.set(key, item);
5033
+ }
5034
+ return item;
5035
+ };
5036
+ for (const [key, ids] of Object.entries((_a = props.includes_for_buttons) != null ? _a : {})) {
5037
+ const item = ensure(key);
5038
+ for (const id of ids != null ? ids : []) item.includes.add(id);
5039
+ }
5040
+ for (const [key, ids] of Object.entries((_b = props.excludes_for_buttons) != null ? _b : {})) {
5041
+ const item = ensure(key);
5042
+ for (const id of ids != null ? ids : []) item.excludes.add(id);
5043
+ }
5044
+ return map;
5045
+ }
5046
+ function isRefExcludedBySelectedKeys(ref, selectedKeys, effectMap) {
5047
+ for (const key of selectedKeys) {
5048
+ const effects = effectMap.get(key);
5049
+ if (!effects) continue;
5050
+ if (ref.fieldId && effects.excludes.has(ref.fieldId) || effects.excludes.has(ref.nodeId)) {
5051
+ return true;
5052
+ }
5053
+ }
5054
+ return false;
5055
+ }
5056
+
5057
+ // src/core/validate/steps/rate-coherence.ts
5058
+ function normalizeRole(role, fallback) {
5059
+ return role === "base" || role === "utility" ? role : fallback;
5060
+ }
5061
+ function uniqueStrings(values) {
5062
+ const out = /* @__PURE__ */ new Set();
5063
+ for (const value of values) {
5064
+ if (!value) continue;
5065
+ out.add(value);
5066
+ }
5067
+ return Array.from(out);
5068
+ }
5069
+ function getRate(serviceMap, serviceId) {
5070
+ const cap = getServiceCapability(serviceMap, serviceId);
5071
+ const rate = cap == null ? void 0 : cap.rate;
5072
+ if (typeof rate !== "number" || !Number.isFinite(rate)) return void 0;
5073
+ return rate;
5074
+ }
5075
+ function collectContextRefs(tag, visibleFields, serviceMap) {
5076
+ var _a, _b, _c, _d, _e;
5077
+ const serviceRefs = [];
5078
+ let tagDefault;
5079
+ if (tag.service_id !== void 0 && tag.service_id !== null) {
5080
+ const tagRate = getRate(serviceMap, tag.service_id);
5081
+ if (tagRate != null) {
5082
+ tagDefault = {
5083
+ key: tag.id,
5084
+ nodeId: tag.id,
5085
+ nodeKind: "tag",
5086
+ serviceId: tag.service_id,
5087
+ rate: tagRate,
5088
+ label: (_a = tag.label) != null ? _a : tag.id,
5089
+ pricingRole: "base"
5090
+ };
5091
+ }
5092
+ }
5093
+ for (const field of visibleFields) {
5094
+ const fieldRole = normalizeRole(field.pricing_role, "base");
5095
+ if (field.service_id !== void 0 && field.service_id !== null) {
5096
+ const rate = getRate(serviceMap, field.service_id);
5097
+ if (rate != null) {
5098
+ serviceRefs.push({
5099
+ key: field.id,
5100
+ nodeId: field.id,
5101
+ fieldId: field.id,
5102
+ nodeKind: "button",
5103
+ serviceId: field.service_id,
5104
+ rate,
5105
+ label: (_b = field.label) != null ? _b : field.id,
5106
+ pricingRole: fieldRole
5107
+ });
5108
+ }
5109
+ }
5110
+ for (const option of (_c = field.options) != null ? _c : []) {
5111
+ if (option.service_id === void 0 || option.service_id === null) continue;
5112
+ const rate = getRate(serviceMap, option.service_id);
5113
+ if (rate == null) continue;
5114
+ serviceRefs.push({
5115
+ key: option.id,
5116
+ nodeId: option.id,
5117
+ fieldId: field.id,
5118
+ nodeKind: "option",
5119
+ serviceId: option.service_id,
5120
+ rate,
5121
+ label: (_d = option.label) != null ? _d : option.id,
5122
+ pricingRole: normalizeRole((_e = option.pricing_role) != null ? _e : field.pricing_role, "base")
5123
+ });
5124
+ }
5125
+ }
5126
+ return { tagDefault, serviceRefs };
5127
+ }
5128
+ function pickHighestRatePrimary(refs) {
5129
+ return refs.reduce((best, cur) => {
5130
+ if (!best) return cur;
5131
+ if (cur.rate > best.rate) return cur;
5132
+ if (cur.rate < best.rate) return best;
5133
+ return cur.nodeId < best.nodeId ? cur : best;
5134
+ }, void 0);
5135
+ }
5136
+ function validateRateCoherenceForVisibleContext(params) {
5137
+ const { v, tagId, selectedKeys, visibleFieldIds, effectMap, seen } = params;
5138
+ const tag = v.tagById.get(tagId);
5139
+ if (!tag) return;
5140
+ const visibleFields = visibleFieldIds.map((id) => v.fieldById.get(id)).filter(Boolean);
5141
+ const { tagDefault, serviceRefs: allServiceRefs } = collectContextRefs(
5142
+ tag,
5143
+ visibleFields,
5144
+ v.serviceMap
5145
+ );
5146
+ const baseRefs = allServiceRefs.filter((ref) => ref.pricingRole === "base");
5147
+ if (baseRefs.length === 0 && !tagDefault) return;
5148
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
5149
+ const visibleInvalidFieldIds = visibleFieldIds.filter(
5150
+ (fieldId) => v.invalidRateFieldIds.has(fieldId)
5151
+ );
5152
+ for (const fieldId of visibleInvalidFieldIds) {
5153
+ const internalKey = [
5154
+ "rate-coherence-internal",
5155
+ tagId,
5156
+ [...selectedKeys].sort().join("|"),
5157
+ fieldId
5158
+ ].join("::");
5159
+ if (seen.has(internalKey)) continue;
5160
+ seen.add(internalKey);
5161
+ v.errors.push({
5162
+ code: "rate_coherence_violation",
5163
+ severity: "error",
5164
+ nodeId: fieldId,
5165
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
5166
+ details: {
5167
+ kind: "internal_field",
5168
+ tagId,
5169
+ selectedKeys: [...selectedKeys],
5170
+ visibleFieldIds: [...visibleFieldIds],
5171
+ fieldId,
5172
+ invalidFieldIds: [fieldId],
5173
+ affectedIds: uniqueStrings([tagId, ...selectedKeys, fieldId])
5174
+ }
5175
+ });
5176
+ }
5177
+ const selectedSet = new Set(selectedKeys);
5178
+ const selectedServiceRefs = baseRefs.filter((ref) => selectedSet.has(ref.key));
5179
+ if (baseRefs.length === 0) return;
5180
+ for (let i = 0; i < baseRefs.length; i++) {
5181
+ for (let j = i + 1; j < baseRefs.length; j++) {
5182
+ const left = baseRefs[i];
5183
+ const right = baseRefs[j];
5184
+ const hypotheticalKeys = [...selectedKeys, left.key, right.key];
5185
+ const survivingRefs = baseRefs.filter(
5186
+ (ref) => !isRefExcludedBySelectedKeys(
5187
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
5188
+ hypotheticalKeys,
5189
+ effectMap
5190
+ )
5191
+ );
5192
+ const survivingSet = new Set(survivingRefs.map((ref) => ref.nodeId));
5193
+ if (!survivingSet.has(left.nodeId) || !survivingSet.has(right.nodeId)) {
5194
+ continue;
5195
+ }
5196
+ if (survivingRefs.length <= 1) continue;
5197
+ const survivingSelected = survivingRefs.filter(
5198
+ (ref) => selectedSet.has(ref.key)
5199
+ );
5200
+ const tagIsCompeting = survivingSelected.length === 0;
5201
+ const primary = pickHighestRatePrimary(survivingRefs);
5202
+ if (!primary) continue;
5203
+ const comparePool = survivingRefs.filter((ref) => ref.nodeId !== primary.nodeId);
5204
+ for (const candidate of comparePool) {
5205
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) continue;
5206
+ const issueKey = [
5207
+ "rate-coherence-context",
5208
+ tagId,
5209
+ [...selectedKeys].sort().join("|"),
5210
+ [...survivingRefs.map((r) => r.nodeId).sort()].join("|"),
5211
+ primary.nodeId,
5212
+ candidate.nodeId,
5213
+ ratePolicy.kind,
5214
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
5215
+ ].join("::");
5216
+ if (seen.has(issueKey)) continue;
5217
+ seen.add(issueKey);
5218
+ v.errors.push({
5219
+ code: "rate_coherence_violation",
5220
+ severity: "error",
5221
+ nodeId: candidate.nodeId,
5222
+ message: "Visible service context contains incompatible base service rates.",
5223
+ details: {
5224
+ kind: "selected_context",
5225
+ tagId,
5226
+ selectedKeys: [...selectedKeys],
5227
+ visibleFieldIds: [...visibleFieldIds],
5228
+ primary: {
5229
+ nodeId: primary.nodeId,
5230
+ fieldId: primary.fieldId,
5231
+ service_id: primary.serviceId,
5232
+ serviceId: primary.serviceId,
5233
+ rate: primary.rate
5234
+ },
5235
+ candidate: {
5236
+ nodeId: candidate.nodeId,
5237
+ fieldId: candidate.fieldId,
5238
+ service_id: candidate.serviceId,
5239
+ serviceId: candidate.serviceId,
5240
+ rate: candidate.rate
5241
+ },
5242
+ policy: ratePolicy.kind,
5243
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
5244
+ invalidFieldIds: visibleInvalidFieldIds,
5245
+ affectedIds: uniqueStrings([
5246
+ tagId,
5247
+ ...selectedKeys,
5248
+ primary.nodeId,
5249
+ primary.fieldId,
5250
+ candidate.nodeId,
5251
+ candidate.fieldId,
5252
+ tagIsCompeting ? tagDefault == null ? void 0 : tagDefault.nodeId : void 0
5253
+ ]),
5254
+ affectedServiceIds: uniqueStrings([
5255
+ String(primary.serviceId),
5256
+ String(candidate.serviceId)
5257
+ ])
5258
+ }
5259
+ });
5260
+ }
5261
+ }
5262
+ }
5263
+ if (selectedServiceRefs.length === 0 && tagDefault && baseRefs.length > 0) {
5264
+ const survivingByDefault = baseRefs.filter(
5265
+ (ref) => !isRefExcludedBySelectedKeys(
5266
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
5267
+ selectedKeys,
5268
+ effectMap
5269
+ )
5270
+ );
5271
+ for (const candidate of survivingByDefault) {
5272
+ if (passesRatePolicy(ratePolicy, tagDefault.rate, candidate.rate)) continue;
5273
+ const issueKey = [
5274
+ "rate-coherence-default",
5275
+ tagId,
5276
+ [...selectedKeys].sort().join("|"),
5277
+ tagDefault.nodeId,
5278
+ candidate.nodeId,
5279
+ ratePolicy.kind,
5280
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
5281
+ ].join("::");
5282
+ if (seen.has(issueKey)) continue;
5283
+ seen.add(issueKey);
5284
+ v.errors.push({
5285
+ code: "rate_coherence_violation",
5286
+ severity: "error",
5287
+ nodeId: candidate.nodeId,
5288
+ message: "Visible service context contains incompatible base service rates.",
5289
+ details: {
5290
+ kind: "selected_context",
5291
+ tagId,
5292
+ selectedKeys: [...selectedKeys],
5293
+ visibleFieldIds: [...visibleFieldIds],
5294
+ primary: {
5295
+ nodeId: tagDefault.nodeId,
5296
+ service_id: tagDefault.serviceId,
5297
+ serviceId: tagDefault.serviceId,
5298
+ rate: tagDefault.rate
5299
+ },
5300
+ candidate: {
5301
+ nodeId: candidate.nodeId,
5302
+ fieldId: candidate.fieldId,
5303
+ service_id: candidate.serviceId,
5304
+ serviceId: candidate.serviceId,
5305
+ rate: candidate.rate
5306
+ },
5307
+ policy: ratePolicy.kind,
5308
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
5309
+ invalidFieldIds: visibleInvalidFieldIds,
5310
+ affectedIds: uniqueStrings([
5311
+ tagId,
5312
+ ...selectedKeys,
5313
+ tagDefault.nodeId,
5314
+ candidate.nodeId,
5315
+ candidate.fieldId
5316
+ ]),
5317
+ affectedServiceIds: uniqueStrings([
5318
+ String(tagDefault.serviceId),
5319
+ String(candidate.serviceId)
5320
+ ])
5321
+ }
5322
+ });
5323
+ }
5324
+ }
5325
+ }
5326
+ function validateRateCoherence(v) {
5327
+ if (Object.keys(v.serviceMap).length === 0 || v.tags.length === 0) return;
5328
+ const effectMap = buildTriggerEffectMap(v.props);
5329
+ const seen = /* @__PURE__ */ new Set();
5330
+ for (const context of v.simulatedVisibilityContexts) {
5331
+ validateRateCoherenceForVisibleContext({
5332
+ v,
5333
+ tagId: context.tagId,
5334
+ selectedKeys: context.selectedKeys,
5335
+ visibleFieldIds: context.visibleFieldIds,
5336
+ effectMap,
5337
+ seen
5338
+ });
5339
+ }
5340
+ }
5341
+
4994
5342
  // src/core/validate/steps/constraints.ts
4995
5343
  function constraintKeysInChain(v, tagId) {
4996
5344
  const keys = [];
@@ -5747,6 +6095,84 @@ function mergeValidatorOptions(defaults = {}, overrides = {}) {
5747
6095
  };
5748
6096
  }
5749
6097
 
6098
+ // src/core/validate/index.ts
6099
+ function readVisibilitySimOpts(ctx) {
6100
+ const c = ctx;
6101
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
6102
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
6103
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
6104
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
6105
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
6106
+ return {
6107
+ simulate,
6108
+ maxStates,
6109
+ maxDepth,
6110
+ simulateAllRoots,
6111
+ onlyEffectfulTriggers
6112
+ };
6113
+ }
6114
+ function validate(props, ctx = {}) {
6115
+ var _a, _b, _c;
6116
+ const options = mergeValidatorOptions({}, ctx);
6117
+ const fallbackSettings = resolveFallbackSettings(options);
6118
+ const ratePolicy = resolveGlobalRatePolicy(options);
6119
+ const errors = [];
6120
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
6121
+ const selectedKeys = new Set(
6122
+ (_b = options.selectedOptionKeys) != null ? _b : []
6123
+ );
6124
+ const tags = Array.isArray(props.filters) ? props.filters : [];
6125
+ const fields = Array.isArray(props.fields) ? props.fields : [];
6126
+ const tagById = /* @__PURE__ */ new Map();
6127
+ const fieldById = /* @__PURE__ */ new Map();
6128
+ for (const t of tags) tagById.set(t.id, t);
6129
+ for (const f of fields) fieldById.set(f.id, f);
6130
+ const v = {
6131
+ props,
6132
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
6133
+ options: {
6134
+ ...options,
6135
+ ratePolicy,
6136
+ fallbackSettings
6137
+ },
6138
+ errors,
6139
+ serviceMap,
6140
+ selectedKeys,
6141
+ tags,
6142
+ fields,
6143
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
6144
+ tagById,
6145
+ fieldById,
6146
+ fieldsVisibleUnder: (_tagId) => [],
6147
+ simulatedVisibilityContexts: []
6148
+ };
6149
+ validateStructure(v);
6150
+ validateIdentity(v);
6151
+ validateOptionMaps(v);
6152
+ validateOrderKinds(v);
6153
+ v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6154
+ const visSim = readVisibilitySimOpts(options);
6155
+ validateVisibility(v, visSim);
6156
+ applyPolicies(
6157
+ v.errors,
6158
+ v.props,
6159
+ v.serviceMap,
6160
+ v.options.policies,
6161
+ v.fieldsVisibleUnder,
6162
+ v.tags
6163
+ );
6164
+ validateServiceVsUserInput(v);
6165
+ validateUtilityMarkers(v);
6166
+ validateRates(v);
6167
+ validateRateCoherence(v);
6168
+ validateConstraints(v);
6169
+ validateCustomFields(v);
6170
+ validateGlobalUtilityGuard(v);
6171
+ validateUnboundFields(v);
6172
+ validateFallbacks(v);
6173
+ return v.errors;
6174
+ }
6175
+
5750
6176
  // src/core/builder.ts
5751
6177
  import { cloneDeep as cloneDeep2 } from "lodash-es";
5752
6178
  function createBuilder(opts = {}) {
@@ -6029,344 +6455,6 @@ function toStringSet(v) {
6029
6455
  return new Set(v.map(String));
6030
6456
  }
6031
6457
 
6032
- // src/core/rate-coherence.ts
6033
- function validateRateCoherenceDeep(params) {
6034
- var _a, _b, _c;
6035
- const { builder, services, tagId } = params;
6036
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
6037
- const props = builder.getProps();
6038
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
6039
- const fields = (_b = props.fields) != null ? _b : [];
6040
- const fieldById = new Map(fields.map((f) => [f.id, f]));
6041
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
6042
- const tag = tagById.get(tagId);
6043
- const baselineFieldIds = builder.visibleFields(tagId, []);
6044
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
6045
- const anchors = collectAnchors(baselineFields);
6046
- const diagnostics = [];
6047
- const seen = /* @__PURE__ */ new Set();
6048
- for (const anchor of anchors) {
6049
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
6050
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
6051
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
6052
- for (const fieldId of visibleInvalidFieldIds) {
6053
- const key = `internal|${tagId}|${fieldId}`;
6054
- if (seen.has(key)) continue;
6055
- seen.add(key);
6056
- diagnostics.push({
6057
- kind: "internal_field",
6058
- scope: "visible_group",
6059
- tagId,
6060
- fieldId,
6061
- nodeId: fieldId,
6062
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
6063
- simulationAnchor: {
6064
- kind: anchor.kind,
6065
- id: anchor.id,
6066
- fieldId: anchor.fieldId,
6067
- label: anchor.label
6068
- },
6069
- invalidFieldIds: [fieldId]
6070
- });
6071
- }
6072
- const references = visibleFields.flatMap(
6073
- (field) => collectFieldReferences(field, services)
6074
- );
6075
- if (references.length <= 1) continue;
6076
- const primary = references.reduce((best, current) => {
6077
- if (current.rate !== best.rate) {
6078
- return current.rate > best.rate ? current : best;
6079
- }
6080
- const bestKey = `${best.fieldId}|${best.nodeId}`;
6081
- const currentKey = `${current.fieldId}|${current.nodeId}`;
6082
- return currentKey < bestKey ? current : best;
6083
- });
6084
- for (const candidate of references) {
6085
- if (candidate.nodeId === primary.nodeId) continue;
6086
- if (candidate.fieldId === primary.fieldId) continue;
6087
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
6088
- continue;
6089
- }
6090
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
6091
- if (seen.has(key)) continue;
6092
- seen.add(key);
6093
- diagnostics.push({
6094
- kind: "contextual",
6095
- scope: "visible_group",
6096
- tagId,
6097
- nodeId: candidate.nodeId,
6098
- primary: toDiagnosticRef(primary),
6099
- offender: toDiagnosticRef(candidate),
6100
- policy: ratePolicy.kind,
6101
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
6102
- message: explainRateMismatch(
6103
- ratePolicy,
6104
- primary,
6105
- candidate,
6106
- describeLabel(tag)
6107
- ),
6108
- simulationAnchor: {
6109
- kind: anchor.kind,
6110
- id: anchor.id,
6111
- fieldId: anchor.fieldId,
6112
- label: anchor.label
6113
- },
6114
- invalidFieldIds: visibleInvalidFieldIds
6115
- });
6116
- }
6117
- }
6118
- return diagnostics;
6119
- }
6120
- function collectAnchors(fields) {
6121
- var _a, _b;
6122
- const anchors = [];
6123
- for (const field of fields) {
6124
- if (!isButton(field)) continue;
6125
- if (Array.isArray(field.options) && field.options.length > 0) {
6126
- for (const option of field.options) {
6127
- anchors.push({
6128
- kind: "option",
6129
- id: option.id,
6130
- fieldId: field.id,
6131
- label: (_a = option.label) != null ? _a : option.id
6132
- });
6133
- }
6134
- continue;
6135
- }
6136
- anchors.push({
6137
- kind: "field",
6138
- id: field.id,
6139
- fieldId: field.id,
6140
- label: (_b = field.label) != null ? _b : field.id
6141
- });
6142
- }
6143
- return anchors;
6144
- }
6145
- function collectFieldReferences(field, services) {
6146
- var _a;
6147
- const members = collectBaseMembers(field, services);
6148
- if (members.length === 0) return [];
6149
- if (isMultiField(field)) {
6150
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
6151
- return [
6152
- {
6153
- refKind: "multi",
6154
- nodeId: field.id,
6155
- fieldId: field.id,
6156
- label: (_a = field.label) != null ? _a : field.id,
6157
- rate: averageRate,
6158
- members
6159
- }
6160
- ];
6161
- }
6162
- return members.map((member) => ({
6163
- refKind: "single",
6164
- nodeId: member.id,
6165
- fieldId: field.id,
6166
- label: member.label,
6167
- rate: member.rate,
6168
- service_id: member.service_id,
6169
- members: [member]
6170
- }));
6171
- }
6172
- function collectBaseMembers(field, services) {
6173
- var _a, _b, _c;
6174
- const members = [];
6175
- if (Array.isArray(field.options) && field.options.length > 0) {
6176
- for (const option of field.options) {
6177
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
6178
- if (role2 !== "base") continue;
6179
- if (option.service_id === void 0 || option.service_id === null) {
6180
- continue;
6181
- }
6182
- const cap2 = getServiceCapability(services, option.service_id);
6183
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
6184
- continue;
6185
- }
6186
- members.push({
6187
- kind: "option",
6188
- id: option.id,
6189
- fieldId: field.id,
6190
- label: (_b = option.label) != null ? _b : option.id,
6191
- service_id: option.service_id,
6192
- rate: cap2.rate
6193
- });
6194
- }
6195
- return members;
6196
- }
6197
- const role = normalizeRole(field.pricing_role, "base");
6198
- if (role !== "base") return members;
6199
- if (field.service_id === void 0 || field.service_id === null) return members;
6200
- const cap = getServiceCapability(services, field.service_id);
6201
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
6202
- return members;
6203
- }
6204
- members.push({
6205
- kind: "field",
6206
- id: field.id,
6207
- fieldId: field.id,
6208
- label: (_c = field.label) != null ? _c : field.id,
6209
- service_id: field.service_id,
6210
- rate: cap.rate
6211
- });
6212
- return members;
6213
- }
6214
- function isButton(field) {
6215
- if (field.button === true) return true;
6216
- return Array.isArray(field.options) && field.options.length > 0;
6217
- }
6218
- function normalizeRole(role, fallback) {
6219
- return role === "base" || role === "utility" ? role : fallback;
6220
- }
6221
- function toDiagnosticRef(reference) {
6222
- return {
6223
- nodeId: reference.nodeId,
6224
- fieldId: reference.fieldId,
6225
- label: reference.label,
6226
- refKind: reference.refKind,
6227
- service_id: reference.service_id,
6228
- rate: reference.rate
6229
- };
6230
- }
6231
- function contextualKey(tagId, primary, candidate, ratePolicy) {
6232
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
6233
- return [
6234
- "contextual",
6235
- tagId,
6236
- primary.fieldId,
6237
- primary.nodeId,
6238
- candidate.fieldId,
6239
- candidate.nodeId,
6240
- `${ratePolicy.kind}${pctKey}`
6241
- ].join("|");
6242
- }
6243
- function describeLabel(tag) {
6244
- var _a, _b;
6245
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
6246
- }
6247
- function explainRateMismatch(policy, primary, candidate, where) {
6248
- var _a, _b;
6249
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
6250
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
6251
- switch (policy.kind) {
6252
- case "eq_primary":
6253
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
6254
- case "lte_primary":
6255
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
6256
- case "within_pct":
6257
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
6258
- case "at_least_pct_lower":
6259
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
6260
- }
6261
- }
6262
-
6263
- // src/core/validate/index.ts
6264
- function readVisibilitySimOpts(ctx) {
6265
- const c = ctx;
6266
- const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
6267
- const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
6268
- const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
6269
- const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
6270
- const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
6271
- return {
6272
- simulate,
6273
- maxStates,
6274
- maxDepth,
6275
- simulateAllRoots,
6276
- onlyEffectfulTriggers
6277
- };
6278
- }
6279
- function validate(props, ctx = {}) {
6280
- var _a, _b, _c;
6281
- const options = mergeValidatorOptions({}, ctx);
6282
- const fallbackSettings = resolveFallbackSettings(options);
6283
- const ratePolicy = resolveGlobalRatePolicy(options);
6284
- const errors = [];
6285
- const serviceMap = (_a = options.serviceMap) != null ? _a : {};
6286
- const selectedKeys = new Set(
6287
- (_b = options.selectedOptionKeys) != null ? _b : []
6288
- );
6289
- const tags = Array.isArray(props.filters) ? props.filters : [];
6290
- const fields = Array.isArray(props.fields) ? props.fields : [];
6291
- const tagById = /* @__PURE__ */ new Map();
6292
- const fieldById = /* @__PURE__ */ new Map();
6293
- for (const t of tags) tagById.set(t.id, t);
6294
- for (const f of fields) fieldById.set(f.id, f);
6295
- const v = {
6296
- props,
6297
- nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
6298
- options: {
6299
- ...options,
6300
- ratePolicy,
6301
- fallbackSettings
6302
- },
6303
- errors,
6304
- serviceMap,
6305
- selectedKeys,
6306
- tags,
6307
- fields,
6308
- invalidRateFieldIds: /* @__PURE__ */ new Set(),
6309
- tagById,
6310
- fieldById,
6311
- fieldsVisibleUnder: (_tagId) => []
6312
- };
6313
- validateStructure(v);
6314
- validateIdentity(v);
6315
- validateOptionMaps(v);
6316
- validateOrderKinds(v);
6317
- v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6318
- const visSim = readVisibilitySimOpts(options);
6319
- validateVisibility(v, visSim);
6320
- applyPolicies(
6321
- v.errors,
6322
- v.props,
6323
- v.serviceMap,
6324
- v.options.policies,
6325
- v.fieldsVisibleUnder,
6326
- v.tags
6327
- );
6328
- validateServiceVsUserInput(v);
6329
- validateUtilityMarkers(v);
6330
- validateRates(v);
6331
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
6332
- const builder = createBuilder({ serviceMap });
6333
- builder.load(props);
6334
- for (const tag of tags) {
6335
- const diags = validateRateCoherenceDeep({
6336
- builder,
6337
- services: serviceMap,
6338
- tagId: tag.id,
6339
- ratePolicy,
6340
- invalidFieldIds: v.invalidRateFieldIds
6341
- });
6342
- for (const diag of diags) {
6343
- if (diag.kind !== "contextual") continue;
6344
- errors.push({
6345
- code: "rate_coherence_violation",
6346
- severity: "error",
6347
- message: diag.message,
6348
- nodeId: diag.nodeId,
6349
- details: {
6350
- tagId: diag.tagId,
6351
- simulationAnchor: diag.simulationAnchor,
6352
- primary: diag.primary,
6353
- offender: diag.offender,
6354
- policy: diag.policy,
6355
- policyPct: diag.policyPct,
6356
- invalidFieldIds: diag.invalidFieldIds
6357
- }
6358
- });
6359
- }
6360
- }
6361
- }
6362
- validateConstraints(v);
6363
- validateCustomFields(v);
6364
- validateGlobalUtilityGuard(v);
6365
- validateUnboundFields(v);
6366
- validateFallbacks(v);
6367
- return v.errors;
6368
- }
6369
-
6370
6458
  // src/core/fallback.ts
6371
6459
  var DEFAULT_SETTINGS = {
6372
6460
  requireConstraintFit: true,
@@ -6788,9 +6876,9 @@ function createNodeIndex(builder) {
6788
6876
  if (cached) return cached;
6789
6877
  const raw = fieldById.get(id);
6790
6878
  if (!raw) return void 0;
6791
- const isButton2 = raw.button === true;
6792
- const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
6793
- const excludes = isButton2 ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
6879
+ const isButton = raw.button === true;
6880
+ const includes = isButton ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
6881
+ const excludes = isButton ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
6794
6882
  const bindIds = () => {
6795
6883
  const cachedBind = fieldBindIdsCache.get(id);
6796
6884
  if (cachedBind) return cachedBind;
@@ -6827,7 +6915,7 @@ function createNodeIndex(builder) {
6827
6915
  );
6828
6916
  },
6829
6917
  getDescendants(tagId) {
6830
- return resolveDescendants(id, includes, tagId, !isButton2);
6918
+ return resolveDescendants(id, includes, tagId, !isButton);
6831
6919
  }
6832
6920
  };
6833
6921
  fieldNodeCache.set(id, node);
@@ -6911,22 +6999,15 @@ function createNodeIndex(builder) {
6911
6999
 
6912
7000
  // src/core/service-filter.ts
6913
7001
  function filterServicesForVisibleGroup(input, deps) {
6914
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
7002
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
6915
7003
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
6916
7004
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
6917
7005
  const { context } = input;
6918
7006
  const usedSet = new Set(context.usedServiceIds.map(String));
6919
- const primary = context.usedServiceIds[0];
6920
7007
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
6921
7008
  const resolvedRatePolicy = normalizeRatePolicy(
6922
7009
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
6923
7010
  );
6924
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
6925
- const fb = {
6926
- ...DEFAULT_FALLBACK_SETTINGS,
6927
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
6928
- ratePolicy: resolvedRatePolicy
6929
- };
6930
7011
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
6931
7012
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
6932
7013
  deps.builder,
@@ -6954,7 +7035,15 @@ function filterServicesForVisibleGroup(input, deps) {
6954
7035
  cap.id,
6955
7036
  (_k = context.effectiveConstraints) != null ? _k : {}
6956
7037
  );
6957
- const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
7038
+ const passesRate2 = candidatePassesRateCoherence(
7039
+ deps.builder,
7040
+ svcMap,
7041
+ context.tagId,
7042
+ (_l = context.selectedButtons) != null ? _l : [],
7043
+ context.usedServiceIds,
7044
+ id,
7045
+ resolvedRatePolicy
7046
+ );
6958
7047
  const polRes = evaluatePoliciesRaw(
6959
7048
  policySource,
6960
7049
  [...context.usedServiceIds, id],
@@ -7073,7 +7162,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
7073
7162
  const fields = (_b = props.fields) != null ? _b : [];
7074
7163
  const tag = tags.find((t) => t.id === tagId);
7075
7164
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
7076
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
7165
+ const visibleFieldIds = new Set(
7166
+ builder.visibleFields(tagId, selectedButtons)
7167
+ );
7077
7168
  for (const field of fields) {
7078
7169
  if (!visibleFieldIds.has(field.id)) continue;
7079
7170
  if (field.service_id != null) {
@@ -7096,8 +7187,7 @@ function matchesRuleFilter(cap, rule, tagId) {
7096
7187
  if (!cap) return false;
7097
7188
  const f = rule.filter;
7098
7189
  if (!f) return true;
7099
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
7100
- return true;
7190
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
7101
7191
  }
7102
7192
  function toStrSet(v) {
7103
7193
  const arr = Array.isArray(v) ? v : [v];
@@ -7105,6 +7195,107 @@ function toStrSet(v) {
7105
7195
  for (const x of arr) s.add(String(x));
7106
7196
  return s;
7107
7197
  }
7198
+ function candidatePassesRateCoherence(builder, serviceMap, tagId, selectedKeys, usedServiceIds, candidateId, ratePolicy) {
7199
+ var _a, _b, _c, _d;
7200
+ if (usedServiceIds.length === 0) return true;
7201
+ const props = builder.getProps();
7202
+ const baseFields = (_a = props.fields) != null ? _a : [];
7203
+ const candidateFieldId = syntheticServiceFieldId("candidate", candidateId, 0);
7204
+ const syntheticFields = [
7205
+ ...usedServiceIds.map((serviceId, index) => ({
7206
+ id: syntheticServiceFieldId("used", serviceId, index),
7207
+ label: `Used service ${String(serviceId)}`,
7208
+ type: "custom",
7209
+ button: true,
7210
+ service_id: serviceId,
7211
+ pricing_role: "base"
7212
+ })),
7213
+ {
7214
+ id: candidateFieldId,
7215
+ label: `Candidate ${String(candidateId)}`,
7216
+ type: "custom",
7217
+ button: true,
7218
+ service_id: candidateId,
7219
+ pricing_role: "base"
7220
+ }
7221
+ ];
7222
+ const fields = [...baseFields, ...syntheticFields];
7223
+ const visibleFieldIds = [
7224
+ ...builder.visibleFields(tagId, selectedKeys),
7225
+ ...syntheticFields.map((field) => field.id)
7226
+ ];
7227
+ const anchoredFilters = ((_b = props.filters) != null ? _b : []).map(
7228
+ (tag) => tag.id === tagId && usedServiceIds[0] != null ? { ...tag, service_id: usedServiceIds[0] } : tag
7229
+ );
7230
+ const validationProps = {
7231
+ ...props,
7232
+ filters: anchoredFilters,
7233
+ fields
7234
+ };
7235
+ const errors = [];
7236
+ const tags = (_c = validationProps.filters) != null ? _c : [];
7237
+ const fieldById = new Map(fields.map((field) => [field.id, field]));
7238
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
7239
+ const v = {
7240
+ props: validationProps,
7241
+ nodeMap: buildNodeMap(validationProps),
7242
+ options: {
7243
+ ...(_d = builder.getOptions) == null ? void 0 : _d.call(builder),
7244
+ serviceMap,
7245
+ ratePolicy
7246
+ },
7247
+ errors,
7248
+ serviceMap,
7249
+ selectedKeys: new Set(selectedKeys),
7250
+ tags,
7251
+ fields,
7252
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
7253
+ tagById,
7254
+ fieldById,
7255
+ fieldsVisibleUnder: () => [],
7256
+ simulatedVisibilityContexts: []
7257
+ };
7258
+ validateRateCoherenceForVisibleContext({
7259
+ v,
7260
+ tagId,
7261
+ selectedKeys,
7262
+ visibleFieldIds,
7263
+ effectMap: buildTriggerEffectMap(validationProps),
7264
+ seen: /* @__PURE__ */ new Set()
7265
+ });
7266
+ return !errors.some(
7267
+ (error) => rateIssueAffectsCandidate(
7268
+ error,
7269
+ candidateId,
7270
+ candidateFieldId,
7271
+ usedServiceIds[0]
7272
+ )
7273
+ );
7274
+ }
7275
+ function syntheticServiceFieldId(kind, serviceId, index) {
7276
+ return `__service_filter_${kind}__:${index}:${String(serviceId)}`;
7277
+ }
7278
+ function rateIssueAffectsCandidate(error, candidateId, candidateFieldId, primaryAnchorId) {
7279
+ var _a, _b, _c, _d;
7280
+ if (error.code !== "rate_coherence_violation") return false;
7281
+ const candidateKey = String(candidateId);
7282
+ const details = (_a = error.details) != null ? _a : {};
7283
+ const anchorKey = primaryAnchorId == null ? void 0 : String(primaryAnchorId);
7284
+ 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;
7285
+ if (primaryMatchesAnchor && ((_d = details.affectedServiceIds) == null ? void 0 : _d.some(
7286
+ (serviceId) => String(serviceId) === candidateKey
7287
+ ))) {
7288
+ return true;
7289
+ }
7290
+ if (primaryMatchesAnchor && String(error.nodeId) === candidateFieldId) {
7291
+ return true;
7292
+ }
7293
+ return [details.primary, details.candidate].some((ref) => {
7294
+ if (!ref) return false;
7295
+ if (!primaryMatchesAnchor) return false;
7296
+ return String(ref.serviceId) === candidateKey || String(ref.service_id) === candidateKey || String(ref.fieldId) === candidateFieldId || String(ref.nodeId) === candidateFieldId;
7297
+ });
7298
+ }
7108
7299
 
7109
7300
  // src/react/canvas/editor/editor-ids.ts
7110
7301
  function uniqueId(ctx, base) {
@@ -7734,7 +7925,7 @@ function setService(ctx, id, input) {
7734
7925
  );
7735
7926
  }
7736
7927
  const isOptionBased = Array.isArray(f.options) && f.options.length > 0;
7737
- const isButton2 = !!f.button;
7928
+ const isButton = !!f.button;
7738
7929
  if (nextRole) {
7739
7930
  f.pricing_role = nextRole;
7740
7931
  }
@@ -7750,7 +7941,7 @@ function setService(ctx, id, input) {
7750
7941
  if ("service_id" in f) delete f.service_id;
7751
7942
  return;
7752
7943
  }
7753
- if (!isButton2) {
7944
+ if (!isButton) {
7754
7945
  if (hasSidKey) {
7755
7946
  ctx.api.emit("error", {
7756
7947
  message: "Only button fields (without options) can have a service_id.",
@@ -10932,18 +11123,23 @@ function useErrors(opts = {}) {
10932
11123
  setValidating(true);
10933
11124
  schedule(
10934
11125
  () => {
10935
- var _a2, _b2, _c2, _d2, _e2;
11126
+ var _a2, _b2, _c2, _d2, _e2, _f, _g;
10936
11127
  if (token !== runTokenRef.current) return;
10937
11128
  try {
10938
11129
  const props = api.editor.getProps();
10939
- const res = validate(props, (_c2 = (_b2 = (_a2 = api.builder).getOptions) == null ? void 0 : _b2.call(_a2)) != null ? _c2 : {});
11130
+ const builderOptions = (_c2 = (_b2 = (_a2 = api.builder).getOptions) == null ? void 0 : _b2.call(_a2)) != null ? _c2 : {};
11131
+ const res = validate(props, {
11132
+ ...builderOptions,
11133
+ simulateVisibility: (_d2 = builderOptions.simulateVisibility) != null ? _d2 : true,
11134
+ visibilityOnlyEffectfulTriggers: (_e2 = builderOptions.visibilityOnlyEffectfulTriggers) != null ? _e2 : true
11135
+ });
10940
11136
  if (token !== runTokenRef.current) return;
10941
11137
  setValidation(toValidationRows(res != null ? res : []));
10942
11138
  } catch (err) {
10943
11139
  if (token !== runTokenRef.current) return;
10944
11140
  pushLog({
10945
- message: (_d2 = err == null ? void 0 : err.message) != null ? _d2 : "validate() threw",
10946
- code: (_e2 = err == null ? void 0 : err.code) != null ? _e2 : "validate_throw",
11141
+ message: (_f = err == null ? void 0 : err.message) != null ? _f : "validate() threw",
11142
+ code: (_g = err == null ? void 0 : err.code) != null ? _g : "validate_throw",
10947
11143
  meta: err
10948
11144
  });
10949
11145
  setValidation([]);