@timeax/digital-service-engine 0.2.7 → 0.2.9

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.
@@ -4121,13 +4121,7 @@ function resolveRootTags(tags) {
4121
4121
  const roots = tags.filter((t) => !t.bind_id);
4122
4122
  return roots.length ? roots : tags.slice(0, 1);
4123
4123
  }
4124
- function isEffectfulTrigger(v, trigger) {
4125
- var _a, _b, _c, _d, _e, _f;
4126
- const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
4127
- const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
4128
- 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;
4129
- }
4130
- function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
4124
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
4131
4125
  var _a;
4132
4126
  const visible = visibleFieldsUnder(v.props, tagId, {
4133
4127
  selectedKeys
@@ -4136,11 +4130,11 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectfu
4136
4130
  for (const f of visible) {
4137
4131
  if (f.button === true) {
4138
4132
  const t = f.id;
4139
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
4133
+ if (effectfulKeys.has(t)) triggers.push(t);
4140
4134
  }
4141
4135
  for (const o of (_a = f.options) != null ? _a : []) {
4142
- const t = `${f.id}::${o.id}`;
4143
- if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
4136
+ const t = o.id;
4137
+ if (effectfulKeys.has(t)) triggers.push(t);
4144
4138
  }
4145
4139
  }
4146
4140
  triggers.sort();
@@ -4240,20 +4234,38 @@ function dedupeErrorsInPlace(v, startIndex) {
4240
4234
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
4241
4235
  }
4242
4236
  function validateVisibility(v, options = {}) {
4243
- var _a, _b, _c;
4237
+ var _a, _b, _c, _d, _e;
4238
+ v.simulatedVisibilityContexts = [];
4244
4239
  const simulate = options.simulate === true;
4245
4240
  if (!simulate) {
4246
4241
  runVisibilityRulesOnce(v);
4242
+ for (const tag of v.tags) {
4243
+ v.simulatedVisibilityContexts.push({
4244
+ tagId: tag.id,
4245
+ selectedKeys: Array.from(v.selectedKeys),
4246
+ visibleFieldIds: v.fieldsVisibleUnder(tag.id).map((f) => f.id)
4247
+ });
4248
+ }
4247
4249
  return;
4248
4250
  }
4249
4251
  const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
4250
4252
  const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
4251
4253
  const onlyEffectful = options.onlyEffectfulTriggers !== false;
4254
+ const effectfulKeys = /* @__PURE__ */ new Set();
4255
+ if (onlyEffectful) {
4256
+ for (const key of Object.keys((_c = v.props.includes_for_buttons) != null ? _c : {})) {
4257
+ effectfulKeys.add(key);
4258
+ }
4259
+ for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
4260
+ effectfulKeys.add(key);
4261
+ }
4262
+ }
4252
4263
  const roots = resolveRootTags(v.tags);
4253
4264
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
4254
- const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
4265
+ const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
4255
4266
  const errorsStart = v.errors.length;
4256
4267
  const visited = /* @__PURE__ */ new Set();
4268
+ const seenContexts = /* @__PURE__ */ new Set();
4257
4269
  const stack = [];
4258
4270
  for (const rt of rootTags) {
4259
4271
  stack.push({
@@ -4266,10 +4278,27 @@ function validateVisibility(v, options = {}) {
4266
4278
  while (stack.length) {
4267
4279
  if (validatedStates >= maxStates) break;
4268
4280
  const state = stack.pop();
4269
- const sig = stableKeyOfSelection(state.selected);
4281
+ const sig = `${state.rootTagId}::${stableKeyOfSelection(state.selected)}`;
4270
4282
  if (visited.has(sig)) continue;
4271
4283
  visited.add(sig);
4272
4284
  v.selectedKeys = state.selected;
4285
+ const visibleNow = visibleFieldsUnder(v.props, state.rootTagId, {
4286
+ selectedKeys: state.selected
4287
+ }).map((f) => f.id);
4288
+ const context = {
4289
+ tagId: state.rootTagId,
4290
+ selectedKeys: Array.from(state.selected),
4291
+ visibleFieldIds: visibleNow
4292
+ };
4293
+ const contextKey = [
4294
+ context.tagId,
4295
+ [...context.selectedKeys].sort().join("|"),
4296
+ [...context.visibleFieldIds].sort().join("|")
4297
+ ].join("::");
4298
+ if (!seenContexts.has(contextKey)) {
4299
+ seenContexts.add(contextKey);
4300
+ v.simulatedVisibilityContexts.push(context);
4301
+ }
4273
4302
  validatedStates++;
4274
4303
  runVisibilityRulesOnce(v);
4275
4304
  if (state.depth >= maxDepth) continue;
@@ -4277,7 +4306,7 @@ function validateVisibility(v, options = {}) {
4277
4306
  v,
4278
4307
  state.rootTagId,
4279
4308
  state.selected,
4280
- onlyEffectful
4309
+ effectfulKeys
4281
4310
  );
4282
4311
  for (let i = triggers.length - 1; i >= 0; i--) {
4283
4312
  const trig = triggers[i];
@@ -4519,8 +4548,8 @@ function validateOptionMaps(v) {
4519
4548
  };
4520
4549
  }
4521
4550
  if (ref.kind === "field") {
4522
- const isButton2 = ref.node.button === true;
4523
- if (!isButton2)
4551
+ const isButton = ref.node.button === true;
4552
+ if (!isButton)
4524
4553
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
4525
4554
  return { ok: true, nodeId: ref.id, affected: [ref.id] };
4526
4555
  }
@@ -4711,9 +4740,9 @@ function validateServiceVsUserInput(v) {
4711
4740
  for (const f of v.fields) {
4712
4741
  const anySvc = hasAnyServiceOption(f);
4713
4742
  const hasName = !!(f.name && f.name.trim());
4714
- const isButton2 = f.button === true;
4743
+ const isButton = f.button === true;
4715
4744
  const hasFieldService = f.service_id !== void 0 && f.service_id !== null;
4716
- const hasTriggerMap = isButton2 && hasButtonTriggerMap(v, f.id);
4745
+ const hasTriggerMap = isButton && hasButtonTriggerMap(v, f.id);
4717
4746
  if (f.type === "custom" && anySvc) {
4718
4747
  v.errors.push({
4719
4748
  code: "user_input_field_has_service_option",
@@ -4730,7 +4759,7 @@ function validateServiceVsUserInput(v) {
4730
4759
  v.errors.push({
4731
4760
  code: "service_field_missing_service_id",
4732
4761
  severity: "error",
4733
- 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.`,
4762
+ 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.`,
4734
4763
  nodeId: f.id
4735
4764
  });
4736
4765
  } else {
@@ -4908,15 +4937,6 @@ function passesRatePolicy(policy, primaryRate, candidateRate) {
4908
4937
  return candidateRate <= primaryRate * (1 - rp.pct / 100);
4909
4938
  }
4910
4939
  }
4911
- function rateOk(svcMap, candidate, primary, policy) {
4912
- const cand = getServiceCapability(svcMap, candidate);
4913
- const prim = getServiceCapability(svcMap, primary);
4914
- if (!cand || !prim) return false;
4915
- const cRate = toFiniteNumber(cand.rate);
4916
- const pRate = toFiniteNumber(prim.rate);
4917
- if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
4918
- return passesRatePolicy(policy.ratePolicy, pRate, cRate);
4919
- }
4920
4940
  function getServiceCapabilityEntry(svcMap, candidate) {
4921
4941
  if (candidate === void 0 || candidate === null) return void 0;
4922
4942
  const direct = svcMap[candidate];
@@ -5044,6 +5064,324 @@ function validateRates(v) {
5044
5064
  }
5045
5065
  }
5046
5066
 
5067
+ // src/core/rate-coherence.ts
5068
+ function buildTriggerEffectMap(props) {
5069
+ var _a, _b;
5070
+ const map = /* @__PURE__ */ new Map();
5071
+ const ensure = (key) => {
5072
+ let item = map.get(key);
5073
+ if (!item) {
5074
+ item = { includes: /* @__PURE__ */ new Set(), excludes: /* @__PURE__ */ new Set() };
5075
+ map.set(key, item);
5076
+ }
5077
+ return item;
5078
+ };
5079
+ for (const [key, ids] of Object.entries((_a = props.includes_for_buttons) != null ? _a : {})) {
5080
+ const item = ensure(key);
5081
+ for (const id of ids != null ? ids : []) item.includes.add(id);
5082
+ }
5083
+ for (const [key, ids] of Object.entries((_b = props.excludes_for_buttons) != null ? _b : {})) {
5084
+ const item = ensure(key);
5085
+ for (const id of ids != null ? ids : []) item.excludes.add(id);
5086
+ }
5087
+ return map;
5088
+ }
5089
+ function isRefExcludedBySelectedKeys(ref, selectedKeys, effectMap) {
5090
+ for (const key of selectedKeys) {
5091
+ const effects = effectMap.get(key);
5092
+ if (!effects) continue;
5093
+ if (ref.fieldId && effects.excludes.has(ref.fieldId) || effects.excludes.has(ref.nodeId)) {
5094
+ return true;
5095
+ }
5096
+ }
5097
+ return false;
5098
+ }
5099
+
5100
+ // src/core/validate/steps/rate-coherence.ts
5101
+ function normalizeRole(role, fallback) {
5102
+ return role === "base" || role === "utility" ? role : fallback;
5103
+ }
5104
+ function uniqueStrings(values) {
5105
+ const out = /* @__PURE__ */ new Set();
5106
+ for (const value of values) {
5107
+ if (!value) continue;
5108
+ out.add(value);
5109
+ }
5110
+ return Array.from(out);
5111
+ }
5112
+ function getRate(serviceMap, serviceId) {
5113
+ const cap = getServiceCapability(serviceMap, serviceId);
5114
+ const rate = cap == null ? void 0 : cap.rate;
5115
+ if (typeof rate !== "number" || !Number.isFinite(rate)) return void 0;
5116
+ return rate;
5117
+ }
5118
+ function collectContextRefs(tag, visibleFields, serviceMap) {
5119
+ var _a, _b, _c, _d, _e;
5120
+ const serviceRefs = [];
5121
+ let tagDefault;
5122
+ if (tag.service_id !== void 0 && tag.service_id !== null) {
5123
+ const tagRate = getRate(serviceMap, tag.service_id);
5124
+ if (tagRate != null) {
5125
+ tagDefault = {
5126
+ key: tag.id,
5127
+ nodeId: tag.id,
5128
+ nodeKind: "tag",
5129
+ serviceId: tag.service_id,
5130
+ rate: tagRate,
5131
+ label: (_a = tag.label) != null ? _a : tag.id,
5132
+ pricingRole: "base"
5133
+ };
5134
+ }
5135
+ }
5136
+ for (const field of visibleFields) {
5137
+ const fieldRole = normalizeRole(field.pricing_role, "base");
5138
+ if (field.service_id !== void 0 && field.service_id !== null) {
5139
+ const rate = getRate(serviceMap, field.service_id);
5140
+ if (rate != null) {
5141
+ serviceRefs.push({
5142
+ key: field.id,
5143
+ nodeId: field.id,
5144
+ fieldId: field.id,
5145
+ nodeKind: "button",
5146
+ serviceId: field.service_id,
5147
+ rate,
5148
+ label: (_b = field.label) != null ? _b : field.id,
5149
+ pricingRole: fieldRole
5150
+ });
5151
+ }
5152
+ }
5153
+ for (const option of (_c = field.options) != null ? _c : []) {
5154
+ if (option.service_id === void 0 || option.service_id === null) continue;
5155
+ const rate = getRate(serviceMap, option.service_id);
5156
+ if (rate == null) continue;
5157
+ serviceRefs.push({
5158
+ key: option.id,
5159
+ nodeId: option.id,
5160
+ fieldId: field.id,
5161
+ nodeKind: "option",
5162
+ serviceId: option.service_id,
5163
+ rate,
5164
+ label: (_d = option.label) != null ? _d : option.id,
5165
+ pricingRole: normalizeRole((_e = option.pricing_role) != null ? _e : field.pricing_role, "base")
5166
+ });
5167
+ }
5168
+ }
5169
+ return { tagDefault, serviceRefs };
5170
+ }
5171
+ function pickHighestRatePrimary(refs) {
5172
+ return refs.reduce((best, cur) => {
5173
+ if (!best) return cur;
5174
+ if (cur.rate > best.rate) return cur;
5175
+ if (cur.rate < best.rate) return best;
5176
+ return cur.nodeId < best.nodeId ? cur : best;
5177
+ }, void 0);
5178
+ }
5179
+ function validateRateCoherenceForVisibleContext(params) {
5180
+ const { v, tagId, selectedKeys, visibleFieldIds, effectMap, seen } = params;
5181
+ const tag = v.tagById.get(tagId);
5182
+ if (!tag) return;
5183
+ const visibleFields = visibleFieldIds.map((id) => v.fieldById.get(id)).filter(Boolean);
5184
+ const { tagDefault, serviceRefs: allServiceRefs } = collectContextRefs(
5185
+ tag,
5186
+ visibleFields,
5187
+ v.serviceMap
5188
+ );
5189
+ const baseRefs = allServiceRefs.filter((ref) => ref.pricingRole === "base");
5190
+ if (baseRefs.length === 0 && !tagDefault) return;
5191
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
5192
+ const visibleInvalidFieldIds = visibleFieldIds.filter(
5193
+ (fieldId) => v.invalidRateFieldIds.has(fieldId)
5194
+ );
5195
+ for (const fieldId of visibleInvalidFieldIds) {
5196
+ const internalKey = [
5197
+ "rate-coherence-internal",
5198
+ tagId,
5199
+ [...selectedKeys].sort().join("|"),
5200
+ fieldId
5201
+ ].join("::");
5202
+ if (seen.has(internalKey)) continue;
5203
+ seen.add(internalKey);
5204
+ v.errors.push({
5205
+ code: "rate_coherence_violation",
5206
+ severity: "error",
5207
+ nodeId: fieldId,
5208
+ message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
5209
+ details: {
5210
+ kind: "internal_field",
5211
+ tagId,
5212
+ selectedKeys: [...selectedKeys],
5213
+ visibleFieldIds: [...visibleFieldIds],
5214
+ fieldId,
5215
+ invalidFieldIds: [fieldId],
5216
+ affectedIds: uniqueStrings([tagId, ...selectedKeys, fieldId])
5217
+ }
5218
+ });
5219
+ }
5220
+ const selectedSet = new Set(selectedKeys);
5221
+ const selectedServiceRefs = baseRefs.filter((ref) => selectedSet.has(ref.key));
5222
+ if (baseRefs.length === 0) return;
5223
+ for (let i = 0; i < baseRefs.length; i++) {
5224
+ for (let j = i + 1; j < baseRefs.length; j++) {
5225
+ const left = baseRefs[i];
5226
+ const right = baseRefs[j];
5227
+ const hypotheticalKeys = [...selectedKeys, left.key, right.key];
5228
+ const survivingRefs = baseRefs.filter(
5229
+ (ref) => !isRefExcludedBySelectedKeys(
5230
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
5231
+ hypotheticalKeys,
5232
+ effectMap
5233
+ )
5234
+ );
5235
+ const survivingSet = new Set(survivingRefs.map((ref) => ref.nodeId));
5236
+ if (!survivingSet.has(left.nodeId) || !survivingSet.has(right.nodeId)) {
5237
+ continue;
5238
+ }
5239
+ if (survivingRefs.length <= 1) continue;
5240
+ const survivingSelected = survivingRefs.filter(
5241
+ (ref) => selectedSet.has(ref.key)
5242
+ );
5243
+ const tagIsCompeting = survivingSelected.length === 0;
5244
+ const primary = pickHighestRatePrimary(survivingRefs);
5245
+ if (!primary) continue;
5246
+ const comparePool = survivingRefs.filter((ref) => ref.nodeId !== primary.nodeId);
5247
+ for (const candidate of comparePool) {
5248
+ if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) continue;
5249
+ const issueKey = [
5250
+ "rate-coherence-context",
5251
+ tagId,
5252
+ [...selectedKeys].sort().join("|"),
5253
+ [...survivingRefs.map((r) => r.nodeId).sort()].join("|"),
5254
+ primary.nodeId,
5255
+ candidate.nodeId,
5256
+ ratePolicy.kind,
5257
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
5258
+ ].join("::");
5259
+ if (seen.has(issueKey)) continue;
5260
+ seen.add(issueKey);
5261
+ v.errors.push({
5262
+ code: "rate_coherence_violation",
5263
+ severity: "error",
5264
+ nodeId: candidate.nodeId,
5265
+ message: "Visible service context contains incompatible base service rates.",
5266
+ details: {
5267
+ kind: "selected_context",
5268
+ tagId,
5269
+ selectedKeys: [...selectedKeys],
5270
+ visibleFieldIds: [...visibleFieldIds],
5271
+ primary: {
5272
+ nodeId: primary.nodeId,
5273
+ fieldId: primary.fieldId,
5274
+ service_id: primary.serviceId,
5275
+ serviceId: primary.serviceId,
5276
+ rate: primary.rate
5277
+ },
5278
+ candidate: {
5279
+ nodeId: candidate.nodeId,
5280
+ fieldId: candidate.fieldId,
5281
+ service_id: candidate.serviceId,
5282
+ serviceId: candidate.serviceId,
5283
+ rate: candidate.rate
5284
+ },
5285
+ policy: ratePolicy.kind,
5286
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
5287
+ invalidFieldIds: visibleInvalidFieldIds,
5288
+ affectedIds: uniqueStrings([
5289
+ tagId,
5290
+ ...selectedKeys,
5291
+ primary.nodeId,
5292
+ primary.fieldId,
5293
+ candidate.nodeId,
5294
+ candidate.fieldId,
5295
+ tagIsCompeting ? tagDefault == null ? void 0 : tagDefault.nodeId : void 0
5296
+ ]),
5297
+ affectedServiceIds: uniqueStrings([
5298
+ String(primary.serviceId),
5299
+ String(candidate.serviceId)
5300
+ ])
5301
+ }
5302
+ });
5303
+ }
5304
+ }
5305
+ }
5306
+ if (selectedServiceRefs.length === 0 && tagDefault && baseRefs.length > 0) {
5307
+ const survivingByDefault = baseRefs.filter(
5308
+ (ref) => !isRefExcludedBySelectedKeys(
5309
+ { fieldId: ref.fieldId, nodeId: ref.nodeId },
5310
+ selectedKeys,
5311
+ effectMap
5312
+ )
5313
+ );
5314
+ for (const candidate of survivingByDefault) {
5315
+ if (passesRatePolicy(ratePolicy, tagDefault.rate, candidate.rate)) continue;
5316
+ const issueKey = [
5317
+ "rate-coherence-default",
5318
+ tagId,
5319
+ [...selectedKeys].sort().join("|"),
5320
+ tagDefault.nodeId,
5321
+ candidate.nodeId,
5322
+ ratePolicy.kind,
5323
+ "pct" in ratePolicy ? String(ratePolicy.pct) : ""
5324
+ ].join("::");
5325
+ if (seen.has(issueKey)) continue;
5326
+ seen.add(issueKey);
5327
+ v.errors.push({
5328
+ code: "rate_coherence_violation",
5329
+ severity: "error",
5330
+ nodeId: candidate.nodeId,
5331
+ message: "Visible service context contains incompatible base service rates.",
5332
+ details: {
5333
+ kind: "selected_context",
5334
+ tagId,
5335
+ selectedKeys: [...selectedKeys],
5336
+ visibleFieldIds: [...visibleFieldIds],
5337
+ primary: {
5338
+ nodeId: tagDefault.nodeId,
5339
+ service_id: tagDefault.serviceId,
5340
+ serviceId: tagDefault.serviceId,
5341
+ rate: tagDefault.rate
5342
+ },
5343
+ candidate: {
5344
+ nodeId: candidate.nodeId,
5345
+ fieldId: candidate.fieldId,
5346
+ service_id: candidate.serviceId,
5347
+ serviceId: candidate.serviceId,
5348
+ rate: candidate.rate
5349
+ },
5350
+ policy: ratePolicy.kind,
5351
+ policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
5352
+ invalidFieldIds: visibleInvalidFieldIds,
5353
+ affectedIds: uniqueStrings([
5354
+ tagId,
5355
+ ...selectedKeys,
5356
+ tagDefault.nodeId,
5357
+ candidate.nodeId,
5358
+ candidate.fieldId
5359
+ ]),
5360
+ affectedServiceIds: uniqueStrings([
5361
+ String(tagDefault.serviceId),
5362
+ String(candidate.serviceId)
5363
+ ])
5364
+ }
5365
+ });
5366
+ }
5367
+ }
5368
+ }
5369
+ function validateRateCoherence(v) {
5370
+ if (Object.keys(v.serviceMap).length === 0 || v.tags.length === 0) return;
5371
+ const effectMap = buildTriggerEffectMap(v.props);
5372
+ const seen = /* @__PURE__ */ new Set();
5373
+ for (const context of v.simulatedVisibilityContexts) {
5374
+ validateRateCoherenceForVisibleContext({
5375
+ v,
5376
+ tagId: context.tagId,
5377
+ selectedKeys: context.selectedKeys,
5378
+ visibleFieldIds: context.visibleFieldIds,
5379
+ effectMap,
5380
+ seen
5381
+ });
5382
+ }
5383
+ }
5384
+
5047
5385
  // src/core/validate/steps/constraints.ts
5048
5386
  function constraintKeysInChain(v, tagId) {
5049
5387
  const keys = [];
@@ -5800,6 +6138,84 @@ function mergeValidatorOptions(defaults = {}, overrides = {}) {
5800
6138
  };
5801
6139
  }
5802
6140
 
6141
+ // src/core/validate/index.ts
6142
+ function readVisibilitySimOpts(ctx) {
6143
+ const c = ctx;
6144
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
6145
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
6146
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
6147
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
6148
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
6149
+ return {
6150
+ simulate,
6151
+ maxStates,
6152
+ maxDepth,
6153
+ simulateAllRoots,
6154
+ onlyEffectfulTriggers
6155
+ };
6156
+ }
6157
+ function validate(props, ctx = {}) {
6158
+ var _a, _b, _c;
6159
+ const options = mergeValidatorOptions({}, ctx);
6160
+ const fallbackSettings = resolveFallbackSettings(options);
6161
+ const ratePolicy = resolveGlobalRatePolicy(options);
6162
+ const errors = [];
6163
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
6164
+ const selectedKeys = new Set(
6165
+ (_b = options.selectedOptionKeys) != null ? _b : []
6166
+ );
6167
+ const tags = Array.isArray(props.filters) ? props.filters : [];
6168
+ const fields = Array.isArray(props.fields) ? props.fields : [];
6169
+ const tagById = /* @__PURE__ */ new Map();
6170
+ const fieldById = /* @__PURE__ */ new Map();
6171
+ for (const t of tags) tagById.set(t.id, t);
6172
+ for (const f of fields) fieldById.set(f.id, f);
6173
+ const v = {
6174
+ props,
6175
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
6176
+ options: {
6177
+ ...options,
6178
+ ratePolicy,
6179
+ fallbackSettings
6180
+ },
6181
+ errors,
6182
+ serviceMap,
6183
+ selectedKeys,
6184
+ tags,
6185
+ fields,
6186
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
6187
+ tagById,
6188
+ fieldById,
6189
+ fieldsVisibleUnder: (_tagId) => [],
6190
+ simulatedVisibilityContexts: []
6191
+ };
6192
+ validateStructure(v);
6193
+ validateIdentity(v);
6194
+ validateOptionMaps(v);
6195
+ validateOrderKinds(v);
6196
+ v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6197
+ const visSim = readVisibilitySimOpts(options);
6198
+ validateVisibility(v, visSim);
6199
+ applyPolicies(
6200
+ v.errors,
6201
+ v.props,
6202
+ v.serviceMap,
6203
+ v.options.policies,
6204
+ v.fieldsVisibleUnder,
6205
+ v.tags
6206
+ );
6207
+ validateServiceVsUserInput(v);
6208
+ validateUtilityMarkers(v);
6209
+ validateRates(v);
6210
+ validateRateCoherence(v);
6211
+ validateConstraints(v);
6212
+ validateCustomFields(v);
6213
+ validateGlobalUtilityGuard(v);
6214
+ validateUnboundFields(v);
6215
+ validateFallbacks(v);
6216
+ return v.errors;
6217
+ }
6218
+
5803
6219
  // src/core/builder.ts
5804
6220
  var import_lodash_es2 = require("lodash-es");
5805
6221
  function createBuilder(opts = {}) {
@@ -6082,344 +6498,6 @@ function toStringSet(v) {
6082
6498
  return new Set(v.map(String));
6083
6499
  }
6084
6500
 
6085
- // src/core/rate-coherence.ts
6086
- function validateRateCoherenceDeep(params) {
6087
- var _a, _b, _c;
6088
- const { builder, services, tagId } = params;
6089
- const ratePolicy = normalizeRatePolicy(params.ratePolicy);
6090
- const props = builder.getProps();
6091
- const invalidFieldIds = new Set((_a = params.invalidFieldIds) != null ? _a : []);
6092
- const fields = (_b = props.fields) != null ? _b : [];
6093
- const fieldById = new Map(fields.map((f) => [f.id, f]));
6094
- const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
6095
- const tag = tagById.get(tagId);
6096
- const baselineFieldIds = builder.visibleFields(tagId, []);
6097
- const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
6098
- const anchors = collectAnchors(baselineFields);
6099
- const diagnostics = [];
6100
- const seen = /* @__PURE__ */ new Set();
6101
- for (const anchor of anchors) {
6102
- const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
6103
- const visibleFields = builder.visibleFields(tagId, selectedKeys).map((fid) => fieldById.get(fid)).filter(Boolean);
6104
- const visibleInvalidFieldIds = visibleFields.map((field) => field.id).filter((fieldId) => invalidFieldIds.has(fieldId));
6105
- for (const fieldId of visibleInvalidFieldIds) {
6106
- const key = `internal|${tagId}|${fieldId}`;
6107
- if (seen.has(key)) continue;
6108
- seen.add(key);
6109
- diagnostics.push({
6110
- kind: "internal_field",
6111
- scope: "visible_group",
6112
- tagId,
6113
- fieldId,
6114
- nodeId: fieldId,
6115
- message: `Field "${fieldId}" is internally invalid under rate policy "${ratePolicy.kind}".`,
6116
- simulationAnchor: {
6117
- kind: anchor.kind,
6118
- id: anchor.id,
6119
- fieldId: anchor.fieldId,
6120
- label: anchor.label
6121
- },
6122
- invalidFieldIds: [fieldId]
6123
- });
6124
- }
6125
- const references = visibleFields.flatMap(
6126
- (field) => collectFieldReferences(field, services)
6127
- );
6128
- if (references.length <= 1) continue;
6129
- const primary = references.reduce((best, current) => {
6130
- if (current.rate !== best.rate) {
6131
- return current.rate > best.rate ? current : best;
6132
- }
6133
- const bestKey = `${best.fieldId}|${best.nodeId}`;
6134
- const currentKey = `${current.fieldId}|${current.nodeId}`;
6135
- return currentKey < bestKey ? current : best;
6136
- });
6137
- for (const candidate of references) {
6138
- if (candidate.nodeId === primary.nodeId) continue;
6139
- if (candidate.fieldId === primary.fieldId) continue;
6140
- if (passesRatePolicy(ratePolicy, primary.rate, candidate.rate)) {
6141
- continue;
6142
- }
6143
- const key = contextualKey(tagId, primary, candidate, ratePolicy);
6144
- if (seen.has(key)) continue;
6145
- seen.add(key);
6146
- diagnostics.push({
6147
- kind: "contextual",
6148
- scope: "visible_group",
6149
- tagId,
6150
- nodeId: candidate.nodeId,
6151
- primary: toDiagnosticRef(primary),
6152
- offender: toDiagnosticRef(candidate),
6153
- policy: ratePolicy.kind,
6154
- policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
6155
- message: explainRateMismatch(
6156
- ratePolicy,
6157
- primary,
6158
- candidate,
6159
- describeLabel(tag)
6160
- ),
6161
- simulationAnchor: {
6162
- kind: anchor.kind,
6163
- id: anchor.id,
6164
- fieldId: anchor.fieldId,
6165
- label: anchor.label
6166
- },
6167
- invalidFieldIds: visibleInvalidFieldIds
6168
- });
6169
- }
6170
- }
6171
- return diagnostics;
6172
- }
6173
- function collectAnchors(fields) {
6174
- var _a, _b;
6175
- const anchors = [];
6176
- for (const field of fields) {
6177
- if (!isButton(field)) continue;
6178
- if (Array.isArray(field.options) && field.options.length > 0) {
6179
- for (const option of field.options) {
6180
- anchors.push({
6181
- kind: "option",
6182
- id: option.id,
6183
- fieldId: field.id,
6184
- label: (_a = option.label) != null ? _a : option.id
6185
- });
6186
- }
6187
- continue;
6188
- }
6189
- anchors.push({
6190
- kind: "field",
6191
- id: field.id,
6192
- fieldId: field.id,
6193
- label: (_b = field.label) != null ? _b : field.id
6194
- });
6195
- }
6196
- return anchors;
6197
- }
6198
- function collectFieldReferences(field, services) {
6199
- var _a;
6200
- const members = collectBaseMembers(field, services);
6201
- if (members.length === 0) return [];
6202
- if (isMultiField(field)) {
6203
- const averageRate = members.reduce((sum, member) => sum + member.rate, 0) / members.length;
6204
- return [
6205
- {
6206
- refKind: "multi",
6207
- nodeId: field.id,
6208
- fieldId: field.id,
6209
- label: (_a = field.label) != null ? _a : field.id,
6210
- rate: averageRate,
6211
- members
6212
- }
6213
- ];
6214
- }
6215
- return members.map((member) => ({
6216
- refKind: "single",
6217
- nodeId: member.id,
6218
- fieldId: field.id,
6219
- label: member.label,
6220
- rate: member.rate,
6221
- service_id: member.service_id,
6222
- members: [member]
6223
- }));
6224
- }
6225
- function collectBaseMembers(field, services) {
6226
- var _a, _b, _c;
6227
- const members = [];
6228
- if (Array.isArray(field.options) && field.options.length > 0) {
6229
- for (const option of field.options) {
6230
- const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
6231
- if (role2 !== "base") continue;
6232
- if (option.service_id === void 0 || option.service_id === null) {
6233
- continue;
6234
- }
6235
- const cap2 = getServiceCapability(services, option.service_id);
6236
- if (!cap2 || typeof cap2.rate !== "number" || !Number.isFinite(cap2.rate)) {
6237
- continue;
6238
- }
6239
- members.push({
6240
- kind: "option",
6241
- id: option.id,
6242
- fieldId: field.id,
6243
- label: (_b = option.label) != null ? _b : option.id,
6244
- service_id: option.service_id,
6245
- rate: cap2.rate
6246
- });
6247
- }
6248
- return members;
6249
- }
6250
- const role = normalizeRole(field.pricing_role, "base");
6251
- if (role !== "base") return members;
6252
- if (field.service_id === void 0 || field.service_id === null) return members;
6253
- const cap = getServiceCapability(services, field.service_id);
6254
- if (!cap || typeof cap.rate !== "number" || !Number.isFinite(cap.rate)) {
6255
- return members;
6256
- }
6257
- members.push({
6258
- kind: "field",
6259
- id: field.id,
6260
- fieldId: field.id,
6261
- label: (_c = field.label) != null ? _c : field.id,
6262
- service_id: field.service_id,
6263
- rate: cap.rate
6264
- });
6265
- return members;
6266
- }
6267
- function isButton(field) {
6268
- if (field.button === true) return true;
6269
- return Array.isArray(field.options) && field.options.length > 0;
6270
- }
6271
- function normalizeRole(role, fallback) {
6272
- return role === "base" || role === "utility" ? role : fallback;
6273
- }
6274
- function toDiagnosticRef(reference) {
6275
- return {
6276
- nodeId: reference.nodeId,
6277
- fieldId: reference.fieldId,
6278
- label: reference.label,
6279
- refKind: reference.refKind,
6280
- service_id: reference.service_id,
6281
- rate: reference.rate
6282
- };
6283
- }
6284
- function contextualKey(tagId, primary, candidate, ratePolicy) {
6285
- const pctKey = "pct" in ratePolicy ? `:${ratePolicy.pct}` : "";
6286
- return [
6287
- "contextual",
6288
- tagId,
6289
- primary.fieldId,
6290
- primary.nodeId,
6291
- candidate.fieldId,
6292
- candidate.nodeId,
6293
- `${ratePolicy.kind}${pctKey}`
6294
- ].join("|");
6295
- }
6296
- function describeLabel(tag) {
6297
- var _a, _b;
6298
- return (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
6299
- }
6300
- function explainRateMismatch(policy, primary, candidate, where) {
6301
- var _a, _b;
6302
- const primaryLabel = `${(_a = primary.label) != null ? _a : primary.nodeId} (${primary.rate})`;
6303
- const candidateLabel = `${(_b = candidate.label) != null ? _b : candidate.nodeId} (${candidate.rate})`;
6304
- switch (policy.kind) {
6305
- case "eq_primary":
6306
- return `Rate coherence failed (${where}): ${candidateLabel} must exactly match ${primaryLabel}.`;
6307
- case "lte_primary":
6308
- return `Rate coherence failed (${where}): ${candidateLabel} must stay within ${policy.pct}% below and never above ${primaryLabel}.`;
6309
- case "within_pct":
6310
- return `Rate coherence failed (${where}): ${candidateLabel} must be within ${policy.pct}% of ${primaryLabel}.`;
6311
- case "at_least_pct_lower":
6312
- return `Rate coherence failed (${where}): ${candidateLabel} must be at least ${policy.pct}% lower than ${primaryLabel}.`;
6313
- }
6314
- }
6315
-
6316
- // src/core/validate/index.ts
6317
- function readVisibilitySimOpts(ctx) {
6318
- const c = ctx;
6319
- const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
6320
- const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
6321
- const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
6322
- const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
6323
- const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
6324
- return {
6325
- simulate,
6326
- maxStates,
6327
- maxDepth,
6328
- simulateAllRoots,
6329
- onlyEffectfulTriggers
6330
- };
6331
- }
6332
- function validate(props, ctx = {}) {
6333
- var _a, _b, _c;
6334
- const options = mergeValidatorOptions({}, ctx);
6335
- const fallbackSettings = resolveFallbackSettings(options);
6336
- const ratePolicy = resolveGlobalRatePolicy(options);
6337
- const errors = [];
6338
- const serviceMap = (_a = options.serviceMap) != null ? _a : {};
6339
- const selectedKeys = new Set(
6340
- (_b = options.selectedOptionKeys) != null ? _b : []
6341
- );
6342
- const tags = Array.isArray(props.filters) ? props.filters : [];
6343
- const fields = Array.isArray(props.fields) ? props.fields : [];
6344
- const tagById = /* @__PURE__ */ new Map();
6345
- const fieldById = /* @__PURE__ */ new Map();
6346
- for (const t of tags) tagById.set(t.id, t);
6347
- for (const f of fields) fieldById.set(f.id, f);
6348
- const v = {
6349
- props,
6350
- nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
6351
- options: {
6352
- ...options,
6353
- ratePolicy,
6354
- fallbackSettings
6355
- },
6356
- errors,
6357
- serviceMap,
6358
- selectedKeys,
6359
- tags,
6360
- fields,
6361
- invalidRateFieldIds: /* @__PURE__ */ new Set(),
6362
- tagById,
6363
- fieldById,
6364
- fieldsVisibleUnder: (_tagId) => []
6365
- };
6366
- validateStructure(v);
6367
- validateIdentity(v);
6368
- validateOptionMaps(v);
6369
- validateOrderKinds(v);
6370
- v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6371
- const visSim = readVisibilitySimOpts(options);
6372
- validateVisibility(v, visSim);
6373
- applyPolicies(
6374
- v.errors,
6375
- v.props,
6376
- v.serviceMap,
6377
- v.options.policies,
6378
- v.fieldsVisibleUnder,
6379
- v.tags
6380
- );
6381
- validateServiceVsUserInput(v);
6382
- validateUtilityMarkers(v);
6383
- validateRates(v);
6384
- if (Object.keys(serviceMap).length > 0 && tags.length > 0) {
6385
- const builder = createBuilder({ serviceMap });
6386
- builder.load(props);
6387
- for (const tag of tags) {
6388
- const diags = validateRateCoherenceDeep({
6389
- builder,
6390
- services: serviceMap,
6391
- tagId: tag.id,
6392
- ratePolicy,
6393
- invalidFieldIds: v.invalidRateFieldIds
6394
- });
6395
- for (const diag of diags) {
6396
- if (diag.kind !== "contextual") continue;
6397
- errors.push({
6398
- code: "rate_coherence_violation",
6399
- severity: "error",
6400
- message: diag.message,
6401
- nodeId: diag.nodeId,
6402
- details: {
6403
- tagId: diag.tagId,
6404
- simulationAnchor: diag.simulationAnchor,
6405
- primary: diag.primary,
6406
- offender: diag.offender,
6407
- policy: diag.policy,
6408
- policyPct: diag.policyPct,
6409
- invalidFieldIds: diag.invalidFieldIds
6410
- }
6411
- });
6412
- }
6413
- }
6414
- }
6415
- validateConstraints(v);
6416
- validateCustomFields(v);
6417
- validateGlobalUtilityGuard(v);
6418
- validateUnboundFields(v);
6419
- validateFallbacks(v);
6420
- return v.errors;
6421
- }
6422
-
6423
6501
  // src/core/fallback.ts
6424
6502
  var DEFAULT_SETTINGS = {
6425
6503
  requireConstraintFit: true,
@@ -6841,9 +6919,9 @@ function createNodeIndex(builder) {
6841
6919
  if (cached) return cached;
6842
6920
  const raw = fieldById.get(id);
6843
6921
  if (!raw) return void 0;
6844
- const isButton2 = raw.button === true;
6845
- const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
6846
- const excludes = isButton2 ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
6922
+ const isButton = raw.button === true;
6923
+ const includes = isButton ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
6924
+ const excludes = isButton ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
6847
6925
  const bindIds = () => {
6848
6926
  const cachedBind = fieldBindIdsCache.get(id);
6849
6927
  if (cachedBind) return cachedBind;
@@ -6880,7 +6958,7 @@ function createNodeIndex(builder) {
6880
6958
  );
6881
6959
  },
6882
6960
  getDescendants(tagId) {
6883
- return resolveDescendants(id, includes, tagId, !isButton2);
6961
+ return resolveDescendants(id, includes, tagId, !isButton);
6884
6962
  }
6885
6963
  };
6886
6964
  fieldNodeCache.set(id, node);
@@ -6964,23 +7042,20 @@ function createNodeIndex(builder) {
6964
7042
 
6965
7043
  // src/core/service-filter.ts
6966
7044
  function filterServicesForVisibleGroup(input, deps) {
6967
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
7045
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
6968
7046
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
6969
7047
  const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
6970
7048
  const { context } = input;
6971
7049
  const usedSet = new Set(context.usedServiceIds.map(String));
6972
- const primary = context.usedServiceIds[0];
6973
7050
  const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
6974
7051
  const resolvedRatePolicy = normalizeRatePolicy(
6975
7052
  (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
6976
7053
  );
6977
- const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
6978
- const fb = {
6979
- ...DEFAULT_FALLBACK_SETTINGS,
6980
- ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
6981
- ratePolicy: resolvedRatePolicy
6982
- };
6983
7054
  const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
7055
+ const resolvedCustomPrimaryRate = resolveCustomPrimaryRate(
7056
+ context.rateContext,
7057
+ svcMap
7058
+ );
6984
7059
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
6985
7060
  deps.builder,
6986
7061
  context.tagId,
@@ -7007,7 +7082,19 @@ function filterServicesForVisibleGroup(input, deps) {
7007
7082
  cap.id,
7008
7083
  (_k = context.effectiveConstraints) != null ? _k : {}
7009
7084
  );
7010
- const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
7085
+ const passesRate2 = resolvedCustomPrimaryRate != null ? passesRatePolicy(
7086
+ resolvedRatePolicy,
7087
+ resolvedCustomPrimaryRate,
7088
+ toFiniteNumber(cap.rate)
7089
+ ) : candidatePassesRateCoherence(
7090
+ deps.builder,
7091
+ svcMap,
7092
+ context.tagId,
7093
+ (_l = context.selectedButtons) != null ? _l : [],
7094
+ context.usedServiceIds,
7095
+ id,
7096
+ resolvedRatePolicy
7097
+ );
7011
7098
  const polRes = evaluatePoliciesRaw(
7012
7099
  policySource,
7013
7100
  [...context.usedServiceIds, id],
@@ -7039,6 +7126,17 @@ function filterServicesForVisibleGroup(input, deps) {
7039
7126
  diagnostics: lastDiagnostics && lastDiagnostics.length ? lastDiagnostics : void 0
7040
7127
  };
7041
7128
  }
7129
+ function resolveCustomPrimaryRate(rateContext, serviceMap) {
7130
+ if (!rateContext || rateContext.mode !== "custom_primary_rate") {
7131
+ return void 0;
7132
+ }
7133
+ if (rateContext.source === "manual") {
7134
+ return toFiniteNumber(rateContext.primaryRate);
7135
+ }
7136
+ if (rateContext.primaryServiceId == null) return void 0;
7137
+ const cap = getServiceCapability(serviceMap, rateContext.primaryServiceId);
7138
+ return toFiniteNumber(cap == null ? void 0 : cap.rate);
7139
+ }
7042
7140
  function evaluatePoliciesRaw(raw, serviceIds, svcMap, tagId, visibleServiceIds) {
7043
7141
  const compiled = compilePolicies(raw);
7044
7142
  const evaluated = evaluateServicePolicies(
@@ -7126,7 +7224,9 @@ function collectVisibleServiceIds(builder, tagId, selectedButtons) {
7126
7224
  const fields = (_b = props.fields) != null ? _b : [];
7127
7225
  const tag = tags.find((t) => t.id === tagId);
7128
7226
  if ((tag == null ? void 0 : tag.service_id) != null) out.add(String(tag.service_id));
7129
- const visibleFieldIds = new Set(builder.visibleFields(tagId, selectedButtons));
7227
+ const visibleFieldIds = new Set(
7228
+ builder.visibleFields(tagId, selectedButtons)
7229
+ );
7130
7230
  for (const field of fields) {
7131
7231
  if (!visibleFieldIds.has(field.id)) continue;
7132
7232
  if (field.service_id != null) {
@@ -7149,8 +7249,7 @@ function matchesRuleFilter(cap, rule, tagId) {
7149
7249
  if (!cap) return false;
7150
7250
  const f = rule.filter;
7151
7251
  if (!f) return true;
7152
- if (f.tag_id && !toStrSet(f.tag_id).has(String(tagId))) return false;
7153
- return true;
7252
+ return !(f.tag_id && !toStrSet(f.tag_id).has(String(tagId)));
7154
7253
  }
7155
7254
  function toStrSet(v) {
7156
7255
  const arr = Array.isArray(v) ? v : [v];
@@ -7158,6 +7257,107 @@ function toStrSet(v) {
7158
7257
  for (const x of arr) s.add(String(x));
7159
7258
  return s;
7160
7259
  }
7260
+ function candidatePassesRateCoherence(builder, serviceMap, tagId, selectedKeys, usedServiceIds, candidateId, ratePolicy) {
7261
+ var _a, _b, _c, _d;
7262
+ if (usedServiceIds.length === 0) return true;
7263
+ const props = builder.getProps();
7264
+ const baseFields = (_a = props.fields) != null ? _a : [];
7265
+ const candidateFieldId = syntheticServiceFieldId("candidate", candidateId, 0);
7266
+ const syntheticFields = [
7267
+ ...usedServiceIds.map((serviceId, index) => ({
7268
+ id: syntheticServiceFieldId("used", serviceId, index),
7269
+ label: `Used service ${String(serviceId)}`,
7270
+ type: "custom",
7271
+ button: true,
7272
+ service_id: serviceId,
7273
+ pricing_role: "base"
7274
+ })),
7275
+ {
7276
+ id: candidateFieldId,
7277
+ label: `Candidate ${String(candidateId)}`,
7278
+ type: "custom",
7279
+ button: true,
7280
+ service_id: candidateId,
7281
+ pricing_role: "base"
7282
+ }
7283
+ ];
7284
+ const fields = [...baseFields, ...syntheticFields];
7285
+ const visibleFieldIds = [
7286
+ ...builder.visibleFields(tagId, selectedKeys),
7287
+ ...syntheticFields.map((field) => field.id)
7288
+ ];
7289
+ const anchoredFilters = ((_b = props.filters) != null ? _b : []).map(
7290
+ (tag) => tag.id === tagId && usedServiceIds[0] != null ? { ...tag, service_id: usedServiceIds[0] } : tag
7291
+ );
7292
+ const validationProps = {
7293
+ ...props,
7294
+ filters: anchoredFilters,
7295
+ fields
7296
+ };
7297
+ const errors = [];
7298
+ const tags = (_c = validationProps.filters) != null ? _c : [];
7299
+ const fieldById = new Map(fields.map((field) => [field.id, field]));
7300
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
7301
+ const v = {
7302
+ props: validationProps,
7303
+ nodeMap: buildNodeMap(validationProps),
7304
+ options: {
7305
+ ...(_d = builder.getOptions) == null ? void 0 : _d.call(builder),
7306
+ serviceMap,
7307
+ ratePolicy
7308
+ },
7309
+ errors,
7310
+ serviceMap,
7311
+ selectedKeys: new Set(selectedKeys),
7312
+ tags,
7313
+ fields,
7314
+ invalidRateFieldIds: /* @__PURE__ */ new Set(),
7315
+ tagById,
7316
+ fieldById,
7317
+ fieldsVisibleUnder: () => [],
7318
+ simulatedVisibilityContexts: []
7319
+ };
7320
+ validateRateCoherenceForVisibleContext({
7321
+ v,
7322
+ tagId,
7323
+ selectedKeys,
7324
+ visibleFieldIds,
7325
+ effectMap: buildTriggerEffectMap(validationProps),
7326
+ seen: /* @__PURE__ */ new Set()
7327
+ });
7328
+ return !errors.some(
7329
+ (error) => rateIssueAffectsCandidate(
7330
+ error,
7331
+ candidateId,
7332
+ candidateFieldId,
7333
+ usedServiceIds[0]
7334
+ )
7335
+ );
7336
+ }
7337
+ function syntheticServiceFieldId(kind, serviceId, index) {
7338
+ return `__service_filter_${kind}__:${index}:${String(serviceId)}`;
7339
+ }
7340
+ function rateIssueAffectsCandidate(error, candidateId, candidateFieldId, primaryAnchorId) {
7341
+ var _a, _b, _c, _d;
7342
+ if (error.code !== "rate_coherence_violation") return false;
7343
+ const candidateKey = String(candidateId);
7344
+ const details = (_a = error.details) != null ? _a : {};
7345
+ const anchorKey = primaryAnchorId == null ? void 0 : String(primaryAnchorId);
7346
+ 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;
7347
+ if (primaryMatchesAnchor && ((_d = details.affectedServiceIds) == null ? void 0 : _d.some(
7348
+ (serviceId) => String(serviceId) === candidateKey
7349
+ ))) {
7350
+ return true;
7351
+ }
7352
+ if (primaryMatchesAnchor && String(error.nodeId) === candidateFieldId) {
7353
+ return true;
7354
+ }
7355
+ return [details.primary, details.candidate].some((ref) => {
7356
+ if (!ref) return false;
7357
+ if (!primaryMatchesAnchor) return false;
7358
+ return String(ref.serviceId) === candidateKey || String(ref.service_id) === candidateKey || String(ref.fieldId) === candidateFieldId || String(ref.nodeId) === candidateFieldId;
7359
+ });
7360
+ }
7161
7361
 
7162
7362
  // src/react/canvas/editor/editor-ids.ts
7163
7363
  function uniqueId(ctx, base) {
@@ -7787,7 +7987,7 @@ function setService(ctx, id, input) {
7787
7987
  );
7788
7988
  }
7789
7989
  const isOptionBased = Array.isArray(f.options) && f.options.length > 0;
7790
- const isButton2 = !!f.button;
7990
+ const isButton = !!f.button;
7791
7991
  if (nextRole) {
7792
7992
  f.pricing_role = nextRole;
7793
7993
  }
@@ -7803,7 +8003,7 @@ function setService(ctx, id, input) {
7803
8003
  if ("service_id" in f) delete f.service_id;
7804
8004
  return;
7805
8005
  }
7806
- if (!isButton2) {
8006
+ if (!isButton) {
7807
8007
  if (hasSidKey) {
7808
8008
  ctx.api.emit("error", {
7809
8009
  message: "Only button fields (without options) can have a service_id.",
@@ -8827,7 +9027,8 @@ function filterServicesForVisibleGroup2(ctx, candidates, input) {
8827
9027
  policies: input.policies,
8828
9028
  ratePolicy: input.ratePolicy,
8829
9029
  fallbackSettings: input.fallbackSettings,
8830
- fallback: input.fallback
9030
+ fallback: input.fallback,
9031
+ rateContext: input.rateContext
8831
9032
  }
8832
9033
  };
8833
9034
  const result = filterServicesForVisibleGroup(coreInput, {
@@ -10985,18 +11186,23 @@ function useErrors(opts = {}) {
10985
11186
  setValidating(true);
10986
11187
  schedule(
10987
11188
  () => {
10988
- var _a2, _b2, _c2, _d2, _e2;
11189
+ var _a2, _b2, _c2, _d2, _e2, _f, _g;
10989
11190
  if (token !== runTokenRef.current) return;
10990
11191
  try {
10991
11192
  const props = api.editor.getProps();
10992
- const res = validate(props, (_c2 = (_b2 = (_a2 = api.builder).getOptions) == null ? void 0 : _b2.call(_a2)) != null ? _c2 : {});
11193
+ const builderOptions = (_c2 = (_b2 = (_a2 = api.builder).getOptions) == null ? void 0 : _b2.call(_a2)) != null ? _c2 : {};
11194
+ const res = validate(props, {
11195
+ ...builderOptions,
11196
+ simulateVisibility: (_d2 = builderOptions.simulateVisibility) != null ? _d2 : true,
11197
+ visibilityOnlyEffectfulTriggers: (_e2 = builderOptions.visibilityOnlyEffectfulTriggers) != null ? _e2 : true
11198
+ });
10993
11199
  if (token !== runTokenRef.current) return;
10994
11200
  setValidation(toValidationRows(res != null ? res : []));
10995
11201
  } catch (err) {
10996
11202
  if (token !== runTokenRef.current) return;
10997
11203
  pushLog({
10998
- message: (_d2 = err == null ? void 0 : err.message) != null ? _d2 : "validate() threw",
10999
- code: (_e2 = err == null ? void 0 : err.code) != null ? _e2 : "validate_throw",
11204
+ message: (_f = err == null ? void 0 : err.message) != null ? _f : "validate() threw",
11205
+ code: (_g = err == null ? void 0 : err.code) != null ? _g : "validate_throw",
11000
11206
  meta: err
11001
11207
  });
11002
11208
  setValidation([]);