@timeax/digital-service-engine 0.2.7 → 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
  }
@@ -4668,9 +4697,9 @@ function validateServiceVsUserInput(v) {
4668
4697
  for (const f of v.fields) {
4669
4698
  const anySvc = hasAnyServiceOption(f);
4670
4699
  const hasName = !!(f.name && f.name.trim());
4671
- const isButton2 = f.button === true;
4700
+ const isButton = f.button === true;
4672
4701
  const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
4673
- const hasTriggerMap = isButton2 && hasButtonTriggerMap(v, f.id);
4702
+ const hasTriggerMap = isButton && hasButtonTriggerMap(v, f.id);
4674
4703
  if (f.type === "custom" && anySvc) {
4675
4704
  v.errors.push({
4676
4705
  code: "user_input_field_has_service_option",
@@ -4687,7 +4716,7 @@ function validateServiceVsUserInput(v) {
4687
4716
  v.errors.push({
4688
4717
  code: "service_field_missing_service_id",
4689
4718
  severity: "error",
4690
- message: isButton2 ? `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.`,
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.`,
4691
4720
  nodeId: f.id
4692
4721
  });
4693
4722
  } else {
@@ -4865,15 +4894,6 @@ function passesRatePolicy(policy, primaryRate, candidateRate) {
4865
4894
  return candidateRate <= primaryRate * (1 - rp.pct / 100);
4866
4895
  }
4867
4896
  }
4868
- function rateOk(svcMap, candidate, primary, policy) {
4869
- const cand = getServiceCapability(svcMap, candidate);
4870
- const prim = getServiceCapability(svcMap, primary);
4871
- if (!cand || !prim) return false;
4872
- const cRate = toFiniteNumber(cand.rate);
4873
- const pRate = toFiniteNumber(prim.rate);
4874
- if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
4875
- return passesRatePolicy(policy.ratePolicy, pRate, cRate);
4876
- }
4877
4897
  function getServiceCapabilityEntry(svcMap, candidate) {
4878
4898
  if (candidate === void 0 || candidate === null) return void 0;
4879
4899
  const direct = svcMap[candidate];
@@ -5001,6 +5021,324 @@ function validateRates(v) {
5001
5021
  }
5002
5022
  }
5003
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
+
5004
5342
  // src/core/validate/steps/constraints.ts
5005
5343
  function constraintKeysInChain(v, tagId) {
5006
5344
  const keys = [];
@@ -5757,6 +6095,84 @@ function mergeValidatorOptions(defaults = {}, overrides = {}) {
5757
6095
  };
5758
6096
  }
5759
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
+
5760
6176
  // src/core/builder.ts
5761
6177
  import { cloneDeep as cloneDeep2 } from "lodash-es";
5762
6178
  function createBuilder(opts = {}) {
@@ -6039,344 +6455,6 @@ function toStringSet(v) {
6039
6455
  return new Set(v.map(String));
6040
6456
  }
6041
6457
 
6042
- // src/core/rate-coherence.ts
6043
- function validateRateCoherenceDeep(params) {
6044
- var _a, _b, _c;
6045
- const { builder, services, tagId } = params;
6046
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
6047
- const props = builder.getProps();
6048
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
6049
- const fields = (_b = props.fields) != null ? _b : [];
6050
- const fieldById = new Map(fields.map((f) => [f.id, f]));
6051
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
6052
- const tag = tagById.get(tagId);
6053
- const baselineFieldIds = builder.visibleFields(tagId, []);
6054
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
6055
- const anchors = collectAnchors(baselineFields);
6056
- const diagnostics = [];
6057
- const seen = /* @__PURE__ */ new Set();
6058
- for (const anchor of anchors) {
6059
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
6060
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
6061
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
6062
- for (const fieldId of visibleInvalidFieldIds) {
6063
- const key = `internal|${tagId}|${fieldId}`;
6064
- if (seen.has(key)) continue;
6065
- seen.add(key);
6066
- diagnostics.push({
6067
- kind: "internal_field",
6068
- scope: "visible_group",
6069
- tagId,
6070
- fieldId,
6071
- nodeId: fieldId,
6072
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
6073
- simulationAnchor: {
6074
- kind: anchor.kind,
6075
- id: anchor.id,
6076
- fieldId: anchor.fieldId,
6077
- label: anchor.label
6078
- },
6079
- invalidFieldIds: [fieldId]
6080
- });
6081
- }
6082
- const references = visibleFields.flatMap(
6083
- (field) => collectFieldReferences(field, services)
6084
- );
6085
- if (references.length <= 1) continue;
6086
- const primary = references.reduce((best, current) => {
6087
- if (current.rate !== best.rate) {
6088
- return current.rate > best.rate ? current : best;
6089
- }
6090
- const bestKey = `${best.fieldId}|${best.nodeId}`;
6091
- const currentKey = `${current.fieldId}|${current.nodeId}`;
6092
- return currentKey < bestKey ? current : best;
6093
- });
6094
- for (const candidate of references) {
6095
- if (candidate.nodeId === primary.nodeId) continue;
6096
- if (candidate.fieldId === primary.fieldId) continue;
6097
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
6098
- continue;
6099
- }
6100
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
6101
- if (seen.has(key)) continue;
6102
- seen.add(key);
6103
- diagnostics.push({
6104
- kind: "contextual",
6105
- scope: "visible_group",
6106
- tagId,
6107
- nodeId: candidate.nodeId,
6108
- primary: toDiagnosticRef(primary),
6109
- offender: toDiagnosticRef(candidate),
6110
- policy: ratePolicy.kind,
6111
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
6112
- message: explainRateMismatch(
6113
- ratePolicy,
6114
- primary,
6115
- candidate,
6116
- describeLabel(tag)
6117
- ),
6118
- simulationAnchor: {
6119
- kind: anchor.kind,
6120
- id: anchor.id,
6121
- fieldId: anchor.fieldId,
6122
- label: anchor.label
6123
- },
6124
- invalidFieldIds: visibleInvalidFieldIds
6125
- });
6126
- }
6127
- }
6128
- return diagnostics;
6129
- }
6130
- function collectAnchors(fields) {
6131
- var _a, _b;
6132
- const anchors = [];
6133
- for (const field of fields) {
6134
- if (!isButton(field)) continue;
6135
- if (Array.isArray(field.options) && field.options.length > 0) {
6136
- for (const option of field.options) {
6137
- anchors.push({
6138
- kind: "option",
6139
- id: option.id,
6140
- fieldId: field.id,
6141
- label: (_a = option.label) != null ? _a : option.id
6142
- });
6143
- }
6144
- continue;
6145
- }
6146
- anchors.push({
6147
- kind: "field",
6148
- id: field.id,
6149
- fieldId: field.id,
6150
- label: (_b = field.label) != null ? _b : field.id
6151
- });
6152
- }
6153
- return anchors;
6154
- }
6155
- function collectFieldReferences(field, services) {
6156
- var _a;
6157
- const members = collectBaseMembers(field, services);
6158
- if (members.length === 0) return [];
6159
- if (isMultiField(field)) {
6160
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
6161
- return [
6162
- {
6163
- refKind: "multi",
6164
- nodeId: field.id,
6165
- fieldId: field.id,
6166
- label: (_a = field.label) != null ? _a : field.id,
6167
- rate: averageRate,
6168
- members
6169
- }
6170
- ];
6171
- }
6172
- return members.map((member) => ({
6173
- refKind: "single",
6174
- nodeId: member.id,
6175
- fieldId: field.id,
6176
- label: member.label,
6177
- rate: member.rate,
6178
- service_id: member.service_id,
6179
- members: [member]
6180
- }));
6181
- }
6182
- function collectBaseMembers(field, services) {
6183
- var _a, _b, _c;
6184
- const members = [];
6185
- if (Array.isArray(field.options) && field.options.length > 0) {
6186
- for (const option of field.options) {
6187
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
6188
- if (role2 !== "base") continue;
6189
- if (option.service_id === void 0 || option.service_id === null) {
6190
- continue;
6191
- }
6192
- const cap2 = getServiceCapability(services, option.service_id);
6193
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
6194
- continue;
6195
- }
6196
- members.push({
6197
- kind: "option",
6198
- id: option.id,
6199
- fieldId: field.id,
6200
- label: (_b = option.label) != null ? _b : option.id,
6201
- service_id: option.service_id,
6202
- rate: cap2.rate
6203
- });
6204
- }
6205
- return members;
6206
- }
6207
- const role = normalizeRole(field.pricing_role, "base");
6208
- if (role !== "base") return members;
6209
- if (field.service_id === void 0 || field.service_id === null) return members;
6210
- const cap = getServiceCapability(services, field.service_id);
6211
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
6212
- return members;
6213
- }
6214
- members.push({
6215
- kind: "field",
6216
- id: field.id,
6217
- fieldId: field.id,
6218
- label: (_c = field.label) != null ? _c : field.id,
6219
- service_id: field.service_id,
6220
- rate: cap.rate
6221
- });
6222
- return members;
6223
- }
6224
- function isButton(field) {
6225
- if (field.button === true) return true;
6226
- return Array.isArray(field.options) && field.options.length > 0;
6227
- }
6228
- function normalizeRole(role, fallback) {
6229
- return role === "base" || role === "utility" ? role : fallback;
6230
- }
6231
- function toDiagnosticRef(reference) {
6232
- return {
6233
- nodeId: reference.nodeId,
6234
- fieldId: reference.fieldId,
6235
- label: reference.label,
6236
- refKind: reference.refKind,
6237
- service_id: reference.service_id,
6238
- rate: reference.rate
6239
- };
6240
- }
6241
- function contextualKey(tagId, primary, candidate, ratePolicy) {
6242
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
6243
- return [
6244
- "contextual",
6245
- tagId,
6246
- primary.fieldId,
6247
- primary.nodeId,
6248
- candidate.fieldId,
6249
- candidate.nodeId,
6250
- `${ratePolicy.kind}${pctKey}`
6251
- ].join("|");
6252
- }
6253
- function describeLabel(tag) {
6254
- var _a, _b;
6255
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
6256
- }
6257
- function explainRateMismatch(policy, primary, candidate, where) {
6258
- var _a, _b;
6259
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
6260
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
6261
- switch (policy.kind) {
6262
- case "eq_primary":
6263
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
6264
- case "lte_primary":
6265
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
6266
- case "within_pct":
6267
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
6268
- case "at_least_pct_lower":
6269
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
6270
- }
6271
- }
6272
-
6273
- // src/core/validate/index.ts
6274
- function readVisibilitySimOpts(ctx) {
6275
- const c = ctx;
6276
- const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
6277
- const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
6278
- const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
6279
- const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
6280
- const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
6281
- return {
6282
- simulate,
6283
- maxStates,
6284
- maxDepth,
6285
- simulateAllRoots,
6286
- onlyEffectfulTriggers
6287
- };
6288
- }
6289
- function validate(props, ctx = {}) {
6290
- var _a, _b, _c;
6291
- const options = mergeValidatorOptions({}, ctx);
6292
- const fallbackSettings = resolveFallbackSettings(options);
6293
- const ratePolicy = resolveGlobalRatePolicy(options);
6294
- const errors = [];
6295
- const serviceMap = (_a = options.serviceMap) != null ? _a : {};
6296
- const selectedKeys = new Set(
6297
- (_b = options.selectedOptionKeys) != null ? _b : []
6298
- );
6299
- const tags = Array.isArray(props.filters) ? props.filters : [];
6300
- const fields = Array.isArray(props.fields) ? props.fields : [];
6301
- const tagById = /* @__PURE__ */ new Map();
6302
- const fieldById = /* @__PURE__ */ new Map();
6303
- for (const t of tags) tagById.set(t.id, t);
6304
- for (const f of fields) fieldById.set(f.id, f);
6305
- const v = {
6306
- props,
6307
- nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
6308
- options: {
6309
- ...options,
6310
- ratePolicy,
6311
- fallbackSettings
6312
- },
6313
- errors,
6314
- serviceMap,
6315
- selectedKeys,
6316
- tags,
6317
- fields,
6318
- invalidRateFieldIds: /* @__PURE__ */ new Set(),
6319
- tagById,
6320
- fieldById,
6321
- fieldsVisibleUnder: (_tagId) => []
6322
- };
6323
- validateStructure(v);
6324
- validateIdentity(v);
6325
- validateOptionMaps(v);
6326
- validateOrderKinds(v);
6327
- v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6328
- const visSim = readVisibilitySimOpts(options);
6329
- validateVisibility(v, visSim);
6330
- applyPolicies(
6331
- v.errors,
6332
- v.props,
6333
- v.serviceMap,
6334
- v.options.policies,
6335
- v.fieldsVisibleUnder,
6336
- v.tags
6337
- );
6338
- validateServiceVsUserInput(v);
6339
- validateUtilityMarkers(v);
6340
- validateRates(v);
6341
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
6342
- const builder = createBuilder({ serviceMap });
6343
- builder.load(props);
6344
- for (const tag of tags) {
6345
- const diags = validateRateCoherenceDeep({
6346
- builder,
6347
- services: serviceMap,
6348
- tagId: tag.id,
6349
- ratePolicy,
6350
- invalidFieldIds: v.invalidRateFieldIds
6351
- });
6352
- for (const diag of diags) {
6353
- if (diag.kind !== "contextual") continue;
6354
- errors.push({
6355
- code: "rate_coherence_violation",
6356
- severity: "error",
6357
- message: diag.message,
6358
- nodeId: diag.nodeId,
6359
- details: {
6360
- tagId: diag.tagId,
6361
- simulationAnchor: diag.simulationAnchor,
6362
- primary: diag.primary,
6363
- offender: diag.offender,
6364
- policy: diag.policy,
6365
- policyPct: diag.policyPct,
6366
- invalidFieldIds: diag.invalidFieldIds
6367
- }
6368
- });
6369
- }
6370
- }
6371
- }
6372
- validateConstraints(v);
6373
- validateCustomFields(v);
6374
- validateGlobalUtilityGuard(v);
6375
- validateUnboundFields(v);
6376
- validateFallbacks(v);
6377
- return v.errors;
6378
- }
6379
-
6380
6458
  // src/core/fallback.ts
6381
6459
  var DEFAULT_SETTINGS = {
6382
6460
  requireConstraintFit: true,
@@ -6798,9 +6876,9 @@ function createNodeIndex(builder) {
6798
6876
  if (cached) return cached;
6799
6877
  const raw = fieldById.get(id);
6800
6878
  if (!raw) return void 0;
6801
- const isButton2 = raw.button === true;
6802
- const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
6803
- 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;
6804
6882
  const bindIds = () => {
6805
6883
  const cachedBind = fieldBindIdsCache.get(id);
6806
6884
  if (cachedBind) return cachedBind;
@@ -6837,7 +6915,7 @@ function createNodeIndex(builder) {
6837
6915
  );
6838
6916
  },
6839
6917
  getDescendants(tagId) {
6840
- return resolveDescendants(id, includes, tagId, !isButton2);
6918
+ return resolveDescendants(id, includes, tagId, !isButton);
6841
6919
  }
6842
6920
  };
6843
6921
  fieldNodeCache.set(id, node);
@@ -6921,22 +6999,15 @@ function createNodeIndex(builder) {
6921
6999
 
6922
7000
  // src/core/service-filter.ts
6923
7001
  function filterServicesForVisibleGroup(input, deps) {
6924
- 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;
6925
7003
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
6926
7004
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
6927
7005
  const { context } = input;
6928
7006
  const usedSet = new Set(context.usedServiceIds.map(String));
6929
- const primary = context.usedServiceIds[0];
6930
7007
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
6931
7008
  const resolvedRatePolicy = normalizeRatePolicy(
6932
7009
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
6933
7010
  );
6934
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
6935
- const fb = {
6936
- ...DEFAULT_FALLBACK_SETTINGS,
6937
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
6938
- ratePolicy: resolvedRatePolicy
6939
- };
6940
7011
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
6941
7012
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
6942
7013
  deps.builder,
@@ -6964,7 +7035,15 @@ function filterServicesForVisibleGroup(input, deps) {
6964
7035
  cap.id,
6965
7036
  (_k = context.effectiveConstraints) != null ? _k : {}
6966
7037
  );
6967
- 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
+ );
6968
7047
  const polRes = evaluatePoliciesRaw(
6969
7048
  policySource,
6970
7049
  [...context.usedServiceIds, id],
@@ -7083,7 +7162,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
7083
7162
  const fields = (_b = props.fields) != null ? _b : [];
7084
7163
  const tag = tags.find((t) => t.id === tagId);
7085
7164
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
7086
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
7165
+ const visibleFieldIds = new Set(
7166
+ builder.visibleFields(tagId, selectedButtons)
7167
+ );
7087
7168
  for (const field of fields) {
7088
7169
  if (!visibleFieldIds.has(field.id)) continue;
7089
7170
  if (field.service_id != null) {
@@ -7106,8 +7187,7 @@ function matchesRuleFilter(cap, rule, tagId) {
7106
7187
  if (!cap) return false;
7107
7188
  const f = rule.filter;
7108
7189
  if (!f) return true;
7109
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
7110
- return true;
7190
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
7111
7191
  }
7112
7192
  function toStrSet(v) {
7113
7193
  const arr = Array.isArray(v) ? v : [v];
@@ -7115,6 +7195,107 @@ function toStrSet(v) {
7115
7195
  for (const x of arr) s.add(String(x));
7116
7196
  return s;
7117
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
+ }
7118
7299
 
7119
7300
  // src/react/canvas/editor/editor-ids.ts
7120
7301
  function uniqueId(ctx, base) {
@@ -7744,7 +7925,7 @@ function setService(ctx, id, input) {
7744
7925
  );
7745
7926
  }
7746
7927
  const isOptionBased = Array.isArray(f.options) && f.options.length > 0;
7747
- const isButton2 = !!f.button;
7928
+ const isButton = !!f.button;
7748
7929
  if (nextRole) {
7749
7930
  f.pricing_role = nextRole;
7750
7931
  }
@@ -7760,7 +7941,7 @@ function setService(ctx, id, input) {
7760
7941
  if ("service_id" in f) delete f.service_id;
7761
7942
  return;
7762
7943
  }
7763
- if (!isButton2) {
7944
+ if (!isButton) {
7764
7945
  if (hasSidKey) {
7765
7946
  ctx.api.emit("error", {
7766
7947
  message: "Only button fields (without options) can have a service_id.",
@@ -10942,18 +11123,23 @@ function useErrors(opts = {}) {
10942
11123
  setValidating(true);
10943
11124
  schedule(
10944
11125
  () => {
10945
- var _a2, _b2, _c2, _d2, _e2;
11126
+ var _a2, _b2, _c2, _d2, _e2, _f, _g;
10946
11127
  if (token !== runTokenRef.current) return;
10947
11128
  try {
10948
11129
  const props = api.editor.getProps();
10949
- 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
+ });
10950
11136
  if (token !== runTokenRef.current) return;
10951
11137
  setValidation(toValidationRows(res != null ? res : []));
10952
11138
  } catch (err) {
10953
11139
  if (token !== runTokenRef.current) return;
10954
11140
  pushLog({
10955
- message: (_d2 = err == null ? void 0 : err.message) != null ? _d2 : "validate() threw",
10956
- 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",
10957
11143
  meta: err
10958
11144
  });
10959
11145
  setValidation([]);