@timeax/digital-service-engine 0.3.4 → 0.3.6

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.
@@ -26,17 +26,25 @@ __export(core_exports, {
26
26
  createBuilder: () => createBuilder,
27
27
  createFallbackEditor: () => createFallbackEditor,
28
28
  createNodeIndex: () => createNodeIndex,
29
+ fieldOptionIdSet: () => fieldOptionIdSet,
30
+ fieldOptionIds: () => fieldOptionIds,
31
+ filterFieldOptionsById: () => filterFieldOptionsById,
29
32
  filterServicesForVisibleGroup: () => filterServicesForVisibleGroup,
33
+ findFieldOption: () => findFieldOption,
34
+ findOptionOwnerField: () => findOptionOwnerField,
30
35
  getAssignedServiceIds: () => getAssignedServiceIds,
31
36
  getEligibleFallbacks: () => getEligibleFallbacks,
32
37
  getFallbackRegistrationInfo: () => getFallbackRegistrationInfo,
33
38
  isRefExcludedBySelectedKeys: () => isRefExcludedBySelectedKeys,
34
39
  normalise: () => normalise,
35
40
  normalizeFieldValidation: () => normalizeFieldValidation,
41
+ optionOwnerMap: () => optionOwnerMap,
36
42
  resolveServiceFallback: () => resolveServiceFallback,
37
43
  validate: () => validate,
38
44
  validateAsync: () => validateAsync,
39
- validateRateCoherenceDeep: () => validateRateCoherenceDeep
45
+ validateRateCoherenceDeep: () => validateRateCoherenceDeep,
46
+ walkFieldOptions: () => walkFieldOptions,
47
+ walkOptions: () => walkOptions
40
48
  });
41
49
  module.exports = __toCommonJS(core_exports);
42
50
 
@@ -55,6 +63,9 @@ function normalise(input, opts = {}) {
55
63
  const excludes_for_buttons = toStringArrayMap(
56
64
  obj.excludes_for_buttons
57
65
  );
66
+ const option_effects_for_buttons = toOptionEffectMap(
67
+ obj.option_effects_for_buttons
68
+ );
58
69
  const orderKinds = toStringMap(obj.orderKinds);
59
70
  const notices = toNoticeArray(obj.notices);
60
71
  let filters = rawFilters.map((t) => coerceTag(t, constraints));
@@ -70,6 +81,9 @@ function normalise(input, opts = {}) {
70
81
  ...isNonEmpty(orderKinds) && { orderKinds },
71
82
  ...isNonEmpty(includes_for_buttons) && { includes_for_buttons },
72
83
  ...isNonEmpty(excludes_for_buttons) && { excludes_for_buttons },
84
+ ...isNonEmpty(option_effects_for_buttons) && {
85
+ option_effects_for_buttons
86
+ },
73
87
  ...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
74
88
  fallbacks
75
89
  },
@@ -224,6 +238,7 @@ function coerceOption(src, inheritRole) {
224
238
  const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
225
239
  const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
226
240
  const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
241
+ const children = Array.isArray(src.children) ? src.children.map((child) => coerceOption(child, pricing_role)) : void 0;
227
242
  const option = {
228
243
  id: "",
229
244
  label: "",
@@ -232,7 +247,8 @@ function coerceOption(src, inheritRole) {
232
247
  ...value !== void 0 && { value },
233
248
  ...service_id !== void 0 && { service_id },
234
249
  pricing_role,
235
- ...meta && { meta }
250
+ ...meta && { meta },
251
+ ...children && children.length && { children }
236
252
  };
237
253
  return option;
238
254
  }
@@ -297,6 +313,35 @@ function toStringArrayMap(src) {
297
313
  }
298
314
  return Object.keys(out).length ? out : void 0;
299
315
  }
316
+ function toOptionEffectMap(src) {
317
+ var _a, _b;
318
+ if (!src || typeof src !== "object") return void 0;
319
+ const out = {};
320
+ for (const [triggerId, rawTargets] of Object.entries(src)) {
321
+ if (!triggerId || !rawTargets || typeof rawTargets !== "object") {
322
+ continue;
323
+ }
324
+ const targets = {};
325
+ for (const [fieldId, rawEffect] of Object.entries(rawTargets)) {
326
+ if (!fieldId || !rawEffect || typeof rawEffect !== "object") {
327
+ continue;
328
+ }
329
+ const effect = rawEffect;
330
+ const include = toStringArray(effect.include);
331
+ const exclude = toStringArray(effect.exclude);
332
+ const next = {
333
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
334
+ ...include.length ? { include: dedupe(include) } : {},
335
+ ...exclude.length ? { exclude: dedupe(exclude) } : {}
336
+ };
337
+ if (next.forceVisible === true || ((_a = next.include) == null ? void 0 : _a.length) || ((_b = next.exclude) == null ? void 0 : _b.length)) {
338
+ targets[fieldId] = next;
339
+ }
340
+ }
341
+ if (Object.keys(targets).length) out[triggerId] = targets;
342
+ }
343
+ return Object.keys(out).length ? out : void 0;
344
+ }
300
345
  function toStringArray(v) {
301
346
  if (!Array.isArray(v)) return [];
302
347
  return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
@@ -371,6 +416,76 @@ function normalizeFieldValidation(input) {
371
416
  return one ? [one] : void 0;
372
417
  }
373
418
 
419
+ // src/core/options.ts
420
+ function walkFieldOptions(field) {
421
+ const out = [];
422
+ const visit = (options, depth, parentId) => {
423
+ for (const option of options != null ? options : []) {
424
+ out.push({
425
+ field,
426
+ fieldId: field.id,
427
+ option,
428
+ optionId: option.id,
429
+ depth,
430
+ parentId
431
+ });
432
+ visit(option.children, depth + 1, option.id);
433
+ }
434
+ };
435
+ visit(field.options, 0);
436
+ return out;
437
+ }
438
+ function walkOptions(props) {
439
+ var _a;
440
+ return ((_a = props.fields) != null ? _a : []).flatMap((field) => walkFieldOptions(field));
441
+ }
442
+ function fieldOptionIds(field) {
443
+ return walkFieldOptions(field).map((visit) => visit.optionId);
444
+ }
445
+ function fieldOptionIdSet(field) {
446
+ return new Set(fieldOptionIds(field));
447
+ }
448
+ function findFieldOption(field, optionId) {
449
+ var _a;
450
+ if (!field) return void 0;
451
+ return (_a = walkFieldOptions(field).find((visit) => visit.optionId === optionId)) == null ? void 0 : _a.option;
452
+ }
453
+ function findOptionOwnerField(fields, optionId) {
454
+ for (const field of fields) {
455
+ if (findFieldOption(field, optionId)) return field;
456
+ }
457
+ return void 0;
458
+ }
459
+ function optionOwnerMap(fields) {
460
+ const out = /* @__PURE__ */ new Map();
461
+ for (const field of fields) {
462
+ for (const visit of walkFieldOptions(field)) {
463
+ if (!out.has(visit.optionId)) {
464
+ out.set(visit.optionId, {
465
+ fieldId: field.id,
466
+ option: visit.option
467
+ });
468
+ }
469
+ }
470
+ }
471
+ return out;
472
+ }
473
+ function filterFieldOptionsById(options, allowed) {
474
+ if (!Array.isArray(options)) return void 0;
475
+ const out = [];
476
+ for (const option of options) {
477
+ const children = filterFieldOptionsById(option.children, allowed);
478
+ if (!allowed.has(option.id) && (!children || children.length === 0)) {
479
+ continue;
480
+ }
481
+ out.push({
482
+ ...option,
483
+ ...children ? { children } : {}
484
+ });
485
+ }
486
+ return out;
487
+ }
488
+
374
489
  // src/core/validate/shared.ts
375
490
  function isFiniteNumber(v) {
376
491
  return typeof v === "number" && Number.isFinite(v);
@@ -379,8 +494,9 @@ function isServiceIdRef(v) {
379
494
  return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
380
495
  }
381
496
  function hasAnyServiceOption(f) {
382
- var _a;
383
- return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
497
+ return walkFieldOptions(f).some(
498
+ (visit) => isServiceIdRef(visit.option.service_id)
499
+ );
384
500
  }
385
501
  function getByPath(obj, path) {
386
502
  if (!path) return void 0;
@@ -469,14 +585,14 @@ function withAffected(details, ids) {
469
585
 
470
586
  // src/core/node-map.ts
471
587
  function buildNodeMap(props) {
472
- var _a, _b, _c;
588
+ var _a, _b;
473
589
  const map = /* @__PURE__ */ new Map();
474
590
  for (const t of (_a = props.filters) != null ? _a : []) {
475
591
  if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
476
592
  }
477
593
  for (const f of (_b = props.fields) != null ? _b : []) {
478
594
  if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
479
- for (const o of (_c = f.options) != null ? _c : []) {
595
+ for (const { option: o } of walkFieldOptions(f)) {
480
596
  if (!map.has(o.id))
481
597
  map.set(o.id, {
482
598
  kind: "option",
@@ -489,12 +605,6 @@ function buildNodeMap(props) {
489
605
  return map;
490
606
  }
491
607
  function resolveTrigger(trigger, nodeMap) {
492
- const idx = trigger.indexOf("::");
493
- if (idx !== -1) {
494
- const fieldId = trigger.slice(0, idx);
495
- const optionId = trigger.slice(idx + 2);
496
- return { kind: "composite", triggerKey: trigger, fieldId, optionId };
497
- }
498
608
  const direct = nodeMap.get(trigger);
499
609
  if (!direct) return void 0;
500
610
  if (direct.kind === "option") {
@@ -546,11 +656,6 @@ function visibleFieldIdsUnder(props, tagId, opts = {}) {
546
656
  const ownerDepthForTriggerKey = (triggerKey) => {
547
657
  const t = resolveTrigger(triggerKey, nodeMap);
548
658
  if (!t) return void 0;
549
- if (t.kind === "composite") {
550
- const f = fieldById.get(t.fieldId);
551
- if (!f) return void 0;
552
- return ownerDepthForField(f);
553
- }
554
659
  if (t.kind === "field") {
555
660
  const f = fieldById.get(t.id);
556
661
  if (!f || f.button !== true) return void 0;
@@ -633,6 +738,84 @@ function visibleFieldsUnder(props, tagId, opts = {}) {
633
738
  const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
634
739
  return ids.map((id) => fieldById.get(id)).filter(Boolean);
635
740
  }
741
+ function resolveVisibility(props, tagId, selectedKeys) {
742
+ var _a, _b, _c, _d;
743
+ const selected = new Set(selectedKeys != null ? selectedKeys : []);
744
+ const baseFieldIds = visibleFieldIdsUnder(props, tagId, { selectedKeys: selected });
745
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((field) => [field.id, field]));
746
+ const visible = new Set(baseFieldIds);
747
+ const forced = /* @__PURE__ */ new Set();
748
+ const optionsByFieldId = {};
749
+ const optionIdsByFieldId = /* @__PURE__ */ new Map();
750
+ const getOptionIds = (field) => {
751
+ let ids = optionIdsByFieldId.get(field.id);
752
+ if (!ids) {
753
+ ids = fieldOptionIds(field);
754
+ optionIdsByFieldId.set(field.id, ids);
755
+ }
756
+ return ids;
757
+ };
758
+ const ensureOptions = (field) => {
759
+ const ids = getOptionIds(field);
760
+ if (!ids.length) return void 0;
761
+ if (!optionsByFieldId[field.id]) optionsByFieldId[field.id] = [...ids];
762
+ return optionsByFieldId[field.id];
763
+ };
764
+ for (const fieldId of baseFieldIds) {
765
+ const field = fieldById.get(fieldId);
766
+ if (field) ensureOptions(field);
767
+ }
768
+ const effects = (_b = props.option_effects_for_buttons) != null ? _b : {};
769
+ for (const triggerId of selected) {
770
+ const targetRules = effects[triggerId];
771
+ if (!targetRules) continue;
772
+ for (const [targetFieldId, rule] of Object.entries(targetRules)) {
773
+ const field = fieldById.get(targetFieldId);
774
+ if (!field) continue;
775
+ const isVisible = visible.has(targetFieldId);
776
+ if (!isVisible && rule.forceVisible !== true) continue;
777
+ if (!isVisible && rule.forceVisible === true) {
778
+ visible.add(targetFieldId);
779
+ forced.add(targetFieldId);
780
+ }
781
+ const orderedOptionIds = getOptionIds(field);
782
+ if (!orderedOptionIds.length) continue;
783
+ const known = new Set(orderedOptionIds);
784
+ let allowed = (_c = optionsByFieldId[targetFieldId]) != null ? _c : [...orderedOptionIds];
785
+ if (Array.isArray(rule.include) && rule.include.length) {
786
+ const include = new Set(
787
+ rule.include.filter((optionId) => known.has(optionId))
788
+ );
789
+ allowed = orderedOptionIds.filter(
790
+ (optionId) => include.has(optionId) && allowed.includes(optionId)
791
+ );
792
+ }
793
+ if (Array.isArray(rule.exclude) && rule.exclude.length) {
794
+ const exclude = new Set(
795
+ rule.exclude.filter((optionId) => known.has(optionId))
796
+ );
797
+ allowed = allowed.filter((optionId) => !exclude.has(optionId));
798
+ }
799
+ optionsByFieldId[targetFieldId] = allowed;
800
+ }
801
+ }
802
+ const visibleFieldIds = baseFieldIds.filter((fieldId) => visible.has(fieldId));
803
+ const seen = new Set(visibleFieldIds);
804
+ for (const field of (_d = props.fields) != null ? _d : []) {
805
+ if (!visible.has(field.id) || seen.has(field.id)) continue;
806
+ seen.add(field.id);
807
+ visibleFieldIds.push(field.id);
808
+ ensureOptions(field);
809
+ }
810
+ for (const fieldId of Object.keys(optionsByFieldId)) {
811
+ if (!visible.has(fieldId)) delete optionsByFieldId[fieldId];
812
+ }
813
+ return {
814
+ fieldIds: visibleFieldIds,
815
+ optionsByFieldId,
816
+ forcedFieldIds: visibleFieldIds.filter((fieldId) => forced.has(fieldId))
817
+ };
818
+ }
636
819
 
637
820
  // src/core/validate/steps/visibility.ts
638
821
  function createFieldsVisibleUnder(v) {
@@ -650,7 +833,6 @@ function resolveRootTags(tags) {
650
833
  return roots.length ? roots : tags.slice(0, 1);
651
834
  }
652
835
  function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
653
- var _a;
654
836
  const visible = visibleFieldsUnder(v.props, tagId, {
655
837
  selectedKeys
656
838
  });
@@ -660,7 +842,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
660
842
  const t = f.id;
661
843
  if (effectfulKeys.has(t)) triggers.push(t);
662
844
  }
663
- for (const o of (_a = f.options) != null ? _a : []) {
845
+ for (const { option: o } of walkFieldOptions(f)) {
664
846
  const t = o.id;
665
847
  if (effectfulKeys.has(t)) triggers.push(t);
666
848
  }
@@ -669,7 +851,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
669
851
  return triggers;
670
852
  }
671
853
  function runVisibilityRulesOnce(v) {
672
- var _a, _b, _c, _d, _e;
854
+ var _a, _b, _c, _d;
673
855
  for (const t of v.tags) {
674
856
  const visible = v.fieldsVisibleUnder(t.id);
675
857
  const seen = /* @__PURE__ */ new Map();
@@ -719,9 +901,9 @@ function runVisibilityRulesOnce(v) {
719
901
  let hasUtility = false;
720
902
  const utilityOptionIds = [];
721
903
  for (const f of visible) {
722
- for (const o of (_c = f.options) != null ? _c : []) {
904
+ for (const { option: o } of walkFieldOptions(f)) {
723
905
  if (!isServiceIdRef(o.service_id)) continue;
724
- const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
906
+ const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
725
907
  if (role === "base") hasBase = true;
726
908
  else if (role === "utility") {
727
909
  hasUtility = true;
@@ -762,7 +944,7 @@ function dedupeErrorsInPlace(v, startIndex) {
762
944
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
763
945
  }
764
946
  function validateVisibility(v, options = {}) {
765
- var _a, _b, _c, _d, _e;
947
+ var _a, _b, _c, _d, _e, _f;
766
948
  v.simulatedVisibilityContexts = [];
767
949
  const simulate = options.simulate === true;
768
950
  if (!simulate) {
@@ -787,10 +969,13 @@ function validateVisibility(v, options = {}) {
787
969
  for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
788
970
  effectfulKeys.add(key);
789
971
  }
972
+ for (const key of Object.keys((_e = v.props.option_effects_for_buttons) != null ? _e : {})) {
973
+ effectfulKeys.add(key);
974
+ }
790
975
  }
791
976
  const roots = resolveRootTags(v.tags);
792
977
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
793
- const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
978
+ const originalSelected = new Set((_f = v.selectedKeys) != null ? _f : []);
794
979
  const errorsStart = v.errors.length;
795
980
  const visited = /* @__PURE__ */ new Set();
796
981
  const seenContexts = /* @__PURE__ */ new Set();
@@ -931,7 +1116,7 @@ function validateStructure(v) {
931
1116
 
932
1117
  // src/core/validate/steps/identity.ts
933
1118
  function validateIdentity(v) {
934
- var _a, _b;
1119
+ var _a;
935
1120
  const tags = v.tags;
936
1121
  const fields = v.fields;
937
1122
  {
@@ -1031,7 +1216,7 @@ function validateIdentity(v) {
1031
1216
  }
1032
1217
  }
1033
1218
  for (const f of fields) {
1034
- for (const o of (_b = f.options) != null ? _b : []) {
1219
+ for (const { option: o } of walkFieldOptions(f)) {
1035
1220
  if (!o.label || !o.label.trim()) {
1036
1221
  v.errors.push({
1037
1222
  code: "label_missing",
@@ -1046,25 +1231,11 @@ function validateIdentity(v) {
1046
1231
  }
1047
1232
 
1048
1233
  // src/core/validate/steps/option-maps.ts
1049
- function parseFieldOptionKey(key) {
1050
- const idx = key.indexOf("::");
1051
- if (idx === -1) return null;
1052
- const fieldId = key.slice(0, idx).trim();
1053
- const optionId = key.slice(idx + 2).trim();
1054
- if (!fieldId || !optionId) return null;
1055
- return { fieldId, optionId };
1056
- }
1057
- function hasOption(v, fid, oid) {
1058
- var _a;
1059
- const f = v.fieldById.get(fid);
1060
- if (!f) return false;
1061
- return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
1062
- }
1063
1234
  function validateOptionMaps(v) {
1064
- var _a, _b;
1235
+ var _a, _b, _c;
1065
1236
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
1066
1237
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
1067
- const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known node id (option or button-field), or "fieldId::optionId" pointing to an existing option.`;
1238
+ const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known option id or button-field id.`;
1068
1239
  const validateTriggerKey = (key) => {
1069
1240
  const ref = v.nodeMap.get(key);
1070
1241
  if (ref) {
@@ -1083,19 +1254,7 @@ function validateOptionMaps(v) {
1083
1254
  }
1084
1255
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
1085
1256
  }
1086
- const p = parseFieldOptionKey(key);
1087
- if (!p) return { ok: false };
1088
- if (!hasOption(v, p.fieldId, p.optionId))
1089
- return {
1090
- ok: false,
1091
- nodeId: p.fieldId,
1092
- affected: [p.fieldId, p.optionId]
1093
- };
1094
- return {
1095
- ok: true,
1096
- nodeId: p.fieldId,
1097
- affected: [p.fieldId, p.optionId]
1098
- };
1257
+ return { ok: false };
1099
1258
  };
1100
1259
  for (const k of Object.keys(incMap)) {
1101
1260
  const r = validateTriggerKey(k);
@@ -1121,6 +1280,57 @@ function validateOptionMaps(v) {
1121
1280
  });
1122
1281
  }
1123
1282
  }
1283
+ const effectMap = (_c = v.props.option_effects_for_buttons) != null ? _c : {};
1284
+ for (const [triggerKey, targets] of Object.entries(effectMap)) {
1285
+ const trigger = validateTriggerKey(triggerKey);
1286
+ if (!trigger.ok) {
1287
+ v.errors.push({
1288
+ code: "bad_option_effect_key",
1289
+ severity: "error",
1290
+ message: badKeyMessage(triggerKey),
1291
+ nodeId: trigger.nodeId,
1292
+ details: withAffected({ key: triggerKey }, trigger.affected)
1293
+ });
1294
+ }
1295
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
1296
+ const field = v.fieldById.get(targetFieldId);
1297
+ if (!field) {
1298
+ v.errors.push({
1299
+ code: "bad_option_effect_target",
1300
+ severity: "error",
1301
+ message: `Option effect trigger "${triggerKey}" targets unknown field "${targetFieldId}".`,
1302
+ details: withAffected(
1303
+ { key: triggerKey, targetFieldId },
1304
+ trigger.affected
1305
+ )
1306
+ });
1307
+ continue;
1308
+ }
1309
+ const validOptionIds = fieldOptionIdSet(field);
1310
+ const checkTargetOptions = (kind, optionIds) => {
1311
+ for (const optionId of optionIds != null ? optionIds : []) {
1312
+ if (validOptionIds.has(optionId)) continue;
1313
+ v.errors.push({
1314
+ code: "bad_option_effect_option",
1315
+ severity: "error",
1316
+ message: `Option effect trigger "${triggerKey}" references unknown ${kind} option "${optionId}" for field "${targetFieldId}".`,
1317
+ nodeId: targetFieldId,
1318
+ details: withAffected(
1319
+ {
1320
+ key: triggerKey,
1321
+ targetFieldId,
1322
+ optionId,
1323
+ kind
1324
+ },
1325
+ [targetFieldId, optionId]
1326
+ )
1327
+ });
1328
+ }
1329
+ };
1330
+ checkTargetOptions("include", effect == null ? void 0 : effect.include);
1331
+ checkTargetOptions("exclude", effect == null ? void 0 : effect.exclude);
1332
+ }
1333
+ }
1124
1334
  for (const k of Object.keys(incMap)) {
1125
1335
  if (!(k in excMap)) continue;
1126
1336
  const r = validateTriggerKey(k);
@@ -1134,27 +1344,231 @@ function validateOptionMaps(v) {
1134
1344
  }
1135
1345
  }
1136
1346
 
1137
- // src/utils/order-kind.ts
1138
- function normalizeSelectedTriggerKey(key, nodeMap) {
1139
- if (!key) return void 0;
1140
- const compositeIdx = key.indexOf("::");
1141
- if (compositeIdx !== -1) {
1142
- const fieldId = key.slice(0, compositeIdx).trim();
1143
- const optionId = key.slice(compositeIdx + 2).trim();
1144
- if (optionId) {
1145
- const optionRef = nodeMap.get(optionId);
1146
- if ((optionRef == null ? void 0 : optionRef.kind) === "option") {
1147
- return { nodeId: optionRef.id, nodeKind: "option" };
1347
+ // src/core/validate/steps/visibility-cycles.ts
1348
+ var MAX_VISIBILITY_CYCLE_DEPTH = 20;
1349
+ function validateVisibilityCycles(v) {
1350
+ const triggerById = buildTriggerIndex(v.fields);
1351
+ if (!triggerById.size) return;
1352
+ const fieldTriggers = buildFieldTriggerIndex(v.fields);
1353
+ const revealTargetsByTrigger = buildRevealIndex(v, triggerById);
1354
+ const reported = /* @__PURE__ */ new Set();
1355
+ for (const rootTriggerId of Array.from(triggerById.keys()).sort()) {
1356
+ const required = makeRequiredState(triggerById, [rootTriggerId]);
1357
+ walkFromTrigger({
1358
+ v,
1359
+ triggerById,
1360
+ fieldTriggers,
1361
+ revealTargetsByTrigger,
1362
+ rootTriggerId,
1363
+ currentTriggerId: rootTriggerId,
1364
+ required,
1365
+ path: [rootTriggerId],
1366
+ visited: /* @__PURE__ */ new Set(),
1367
+ reported,
1368
+ depth: 0
1369
+ });
1370
+ }
1371
+ }
1372
+ function buildTriggerIndex(fields) {
1373
+ const out = /* @__PURE__ */ new Map();
1374
+ const owners = optionOwnerMap(fields);
1375
+ for (const field of fields) {
1376
+ if (field.button === true) {
1377
+ out.set(field.id, {
1378
+ kind: "field",
1379
+ id: field.id,
1380
+ ownerFieldId: field.id
1381
+ });
1382
+ }
1383
+ }
1384
+ for (const [optionId, owner] of owners) {
1385
+ out.set(optionId, {
1386
+ kind: "option",
1387
+ id: optionId,
1388
+ ownerFieldId: owner.fieldId
1389
+ });
1390
+ }
1391
+ return out;
1392
+ }
1393
+ function buildFieldTriggerIndex(fields) {
1394
+ const out = /* @__PURE__ */ new Map();
1395
+ for (const field of fields) {
1396
+ const triggers = [];
1397
+ if (field.button === true) triggers.push(field.id);
1398
+ for (const visit of walkFieldOptions(field)) {
1399
+ triggers.push(visit.optionId);
1400
+ }
1401
+ out.set(field.id, triggers);
1402
+ }
1403
+ return out;
1404
+ }
1405
+ function buildRevealIndex(v, triggerById) {
1406
+ var _a, _b;
1407
+ const out = /* @__PURE__ */ new Map();
1408
+ const addReveal = (triggerId, targetFieldId) => {
1409
+ var _a2;
1410
+ if (!triggerById.has(triggerId)) return;
1411
+ if (!v.fieldById.has(targetFieldId)) return;
1412
+ const set = (_a2 = out.get(triggerId)) != null ? _a2 : /* @__PURE__ */ new Set();
1413
+ set.add(targetFieldId);
1414
+ out.set(triggerId, set);
1415
+ };
1416
+ for (const [triggerId, targetIds] of Object.entries(
1417
+ (_a = v.props.includes_for_buttons) != null ? _a : {}
1418
+ )) {
1419
+ for (const targetId of targetIds != null ? targetIds : []) addReveal(triggerId, targetId);
1420
+ }
1421
+ for (const [triggerId, targets] of Object.entries(
1422
+ (_b = v.props.option_effects_for_buttons) != null ? _b : {}
1423
+ )) {
1424
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
1425
+ if ((effect == null ? void 0 : effect.forceVisible) === true)
1426
+ addReveal(triggerId, targetFieldId);
1427
+ }
1428
+ }
1429
+ return new Map(
1430
+ Array.from(out.entries()).map(([triggerId, fieldIds]) => [
1431
+ triggerId,
1432
+ Array.from(fieldIds).sort()
1433
+ ])
1434
+ );
1435
+ }
1436
+ function walkFromTrigger(args) {
1437
+ var _a, _b, _c;
1438
+ if (args.depth >= MAX_VISIBILITY_CYCLE_DEPTH) return;
1439
+ const visitedKey = `${args.rootTriggerId}::${args.currentTriggerId}::${args.path.join(">")}`;
1440
+ if (args.visited.has(visitedKey)) return;
1441
+ args.visited.add(visitedKey);
1442
+ const revealedFieldIds = (_a = args.revealTargetsByTrigger.get(args.currentTriggerId)) != null ? _a : [];
1443
+ for (const revealedFieldId of revealedFieldIds) {
1444
+ const reachableTriggers = (_c = (_b = args.fieldTriggers.get(revealedFieldId)) == null ? void 0 : _b.slice().sort()) != null ? _c : [];
1445
+ for (const reachableTriggerId of reachableTriggers) {
1446
+ const invalidation = invalidatesRequiredPath(
1447
+ args.v,
1448
+ args.triggerById,
1449
+ reachableTriggerId,
1450
+ args.required
1451
+ );
1452
+ if (invalidation) {
1453
+ emitCycleError({
1454
+ v: args.v,
1455
+ rootTriggerId: args.rootTriggerId,
1456
+ revealedFieldId,
1457
+ conflictingTriggerId: reachableTriggerId,
1458
+ invalidatedId: invalidation.invalidatedId,
1459
+ path: [...args.path, reachableTriggerId],
1460
+ reported: args.reported
1461
+ });
1462
+ }
1463
+ if (args.path.includes(reachableTriggerId)) continue;
1464
+ walkFromTrigger({
1465
+ ...args,
1466
+ currentTriggerId: reachableTriggerId,
1467
+ required: addRequiredTrigger(
1468
+ args.triggerById,
1469
+ args.required,
1470
+ reachableTriggerId
1471
+ ),
1472
+ path: [...args.path, reachableTriggerId],
1473
+ depth: args.depth + 1
1474
+ });
1475
+ }
1476
+ }
1477
+ }
1478
+ function makeRequiredState(triggerById, triggerIds) {
1479
+ let required = {
1480
+ triggers: /* @__PURE__ */ new Set(),
1481
+ ownerFields: /* @__PURE__ */ new Set()
1482
+ };
1483
+ for (const triggerId of triggerIds) {
1484
+ required = addRequiredTrigger(triggerById, required, triggerId);
1485
+ }
1486
+ return required;
1487
+ }
1488
+ function addRequiredTrigger(triggerById, current, triggerId) {
1489
+ const next = {
1490
+ triggers: new Set(current.triggers),
1491
+ ownerFields: new Set(current.ownerFields)
1492
+ };
1493
+ const trigger = triggerById.get(triggerId);
1494
+ if (!trigger) return next;
1495
+ next.triggers.add(triggerId);
1496
+ next.ownerFields.add(trigger.ownerFieldId);
1497
+ return next;
1498
+ }
1499
+ function invalidatesRequiredPath(v, triggerById, conflictingTriggerId, required) {
1500
+ var _a, _b, _c, _d, _e, _f;
1501
+ for (const targetId of (_b = (_a = v.props.excludes_for_buttons) == null ? void 0 : _a[conflictingTriggerId]) != null ? _b : []) {
1502
+ if (required.ownerFields.has(targetId)) {
1503
+ return { invalidatedId: targetId };
1504
+ }
1505
+ const targetTrigger = triggerById.get(targetId);
1506
+ if ((targetTrigger == null ? void 0 : targetTrigger.kind) === "option" && required.triggers.has(targetId)) {
1507
+ return { invalidatedId: targetId };
1508
+ }
1509
+ }
1510
+ const effects = (_d = (_c = v.props.option_effects_for_buttons) == null ? void 0 : _c[conflictingTriggerId]) != null ? _d : {};
1511
+ for (const [targetFieldId, effect] of Object.entries(effects)) {
1512
+ if (!v.fieldById.has(targetFieldId)) continue;
1513
+ if ((_e = effect == null ? void 0 : effect.exclude) == null ? void 0 : _e.length) {
1514
+ const excluded = new Set(effect.exclude);
1515
+ for (const requiredTriggerId of required.triggers) {
1516
+ const requiredTrigger = triggerById.get(requiredTriggerId);
1517
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
1518
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
1519
+ if (excluded.has(requiredTriggerId)) {
1520
+ return { invalidatedId: requiredTriggerId };
1521
+ }
1148
1522
  }
1149
1523
  }
1150
- if (fieldId) {
1151
- const fieldRef = nodeMap.get(fieldId);
1152
- if ((fieldRef == null ? void 0 : fieldRef.kind) === "field") {
1153
- return { nodeId: fieldRef.id, nodeKind: "field" };
1524
+ if ((_f = effect == null ? void 0 : effect.include) == null ? void 0 : _f.length) {
1525
+ const included = new Set(effect.include);
1526
+ for (const requiredTriggerId of required.triggers) {
1527
+ const requiredTrigger = triggerById.get(requiredTriggerId);
1528
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
1529
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
1530
+ if (!included.has(requiredTriggerId)) {
1531
+ return { invalidatedId: requiredTriggerId };
1532
+ }
1154
1533
  }
1155
1534
  }
1156
- return void 0;
1157
1535
  }
1536
+ return void 0;
1537
+ }
1538
+ function emitCycleError(args) {
1539
+ const key = [
1540
+ args.rootTriggerId,
1541
+ args.conflictingTriggerId,
1542
+ args.invalidatedId,
1543
+ args.path.join(">")
1544
+ ].join("::");
1545
+ if (args.reported.has(key)) return;
1546
+ args.reported.add(key);
1547
+ args.v.errors.push({
1548
+ code: "visibility_dependency_cycle",
1549
+ severity: "error",
1550
+ message: `Visibility dependency cycle: trigger "${args.rootTriggerId}" reveals "${args.revealedFieldId}", but reachable trigger "${args.conflictingTriggerId}" can hide or remove "${args.invalidatedId}".`,
1551
+ nodeId: args.conflictingTriggerId,
1552
+ details: withAffected(
1553
+ {
1554
+ rootTriggerId: args.rootTriggerId,
1555
+ conflictingTriggerId: args.conflictingTriggerId,
1556
+ invalidatedId: args.invalidatedId,
1557
+ path: args.path
1558
+ },
1559
+ [
1560
+ args.rootTriggerId,
1561
+ args.revealedFieldId,
1562
+ args.conflictingTriggerId,
1563
+ args.invalidatedId
1564
+ ]
1565
+ )
1566
+ });
1567
+ }
1568
+
1569
+ // src/utils/order-kind.ts
1570
+ function normalizeSelectedTriggerKey(key, nodeMap) {
1571
+ if (!key) return void 0;
1158
1572
  const ref = nodeMap.get(key);
1159
1573
  if (!ref) return void 0;
1160
1574
  if (ref.kind !== "field" && ref.kind !== "option") return void 0;
@@ -1313,8 +1727,7 @@ function validateUtilityMarkers(v) {
1313
1727
  "percent"
1314
1728
  ]);
1315
1729
  for (const f of v.fields) {
1316
- const optsArr = Array.isArray(f.options) ? f.options : [];
1317
- for (const o of optsArr) {
1730
+ for (const { option: o } of walkFieldOptions(f)) {
1318
1731
  const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
1319
1732
  const hasService = isServiceIdRef(o.service_id);
1320
1733
  const util = (_c = o.meta) == null ? void 0 : _c.utility;
@@ -1550,13 +1963,13 @@ function normalizeServiceRef(value) {
1550
1963
 
1551
1964
  // src/core/validate/steps/rates.ts
1552
1965
  function validateRates(v) {
1553
- var _a, _b, _c;
1966
+ var _a, _b;
1554
1967
  const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1555
1968
  for (const f of v.fields) {
1556
1969
  if (!isMultiField(f)) continue;
1557
1970
  const baseRates = [];
1558
- for (const o of (_a = f.options) != null ? _a : []) {
1559
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1971
+ for (const { option: o } of walkFieldOptions(f)) {
1972
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
1560
1973
  if (role !== "base") continue;
1561
1974
  const sid = o.service_id;
1562
1975
  if (!isServiceIdRef(sid)) continue;
@@ -1758,8 +2171,9 @@ function collectAnchors(fields) {
1758
2171
  const anchors = [];
1759
2172
  for (const field of fields) {
1760
2173
  if (!isButton(field)) continue;
1761
- if (Array.isArray(field.options) && field.options.length > 0) {
1762
- for (const option of field.options) {
2174
+ const optionVisits = walkFieldOptions(field);
2175
+ if (optionVisits.length > 0) {
2176
+ for (const { option } of optionVisits) {
1763
2177
  anchors.push({
1764
2178
  kind: "option",
1765
2179
  id: option.id,
@@ -1808,8 +2222,9 @@ function collectFieldReferences(field, services) {
1808
2222
  function collectBaseMembers(field, services) {
1809
2223
  var _a, _b, _c;
1810
2224
  const members = [];
1811
- if (Array.isArray(field.options) && field.options.length > 0) {
1812
- for (const option of field.options) {
2225
+ const optionVisits = walkFieldOptions(field);
2226
+ if (optionVisits.length > 0) {
2227
+ for (const { option } of optionVisits) {
1813
2228
  const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
1814
2229
  if (role2 !== "base") continue;
1815
2230
  if (option.service_id === void 0 || option.service_id === null) {
@@ -2224,7 +2639,7 @@ function effectiveConstraints(v, tagId) {
2224
2639
  return out;
2225
2640
  }
2226
2641
  function validateConstraints(v) {
2227
- var _a, _b;
2642
+ var _a;
2228
2643
  for (const t of v.tags) {
2229
2644
  const eff = effectiveConstraints(v, t.id);
2230
2645
  const hasAnyRequired = Object.values(eff).some(
@@ -2233,7 +2648,7 @@ function validateConstraints(v) {
2233
2648
  if (!hasAnyRequired) continue;
2234
2649
  const visible = v.fieldsVisibleUnder(t.id);
2235
2650
  for (const f of visible) {
2236
- for (const o of (_a = f.options) != null ? _a : []) {
2651
+ for (const { option: o } of walkFieldOptions(f)) {
2237
2652
  if (!isServiceIdRef(o.service_id)) continue;
2238
2653
  const svc = getServiceCapability(v.serviceMap, o.service_id);
2239
2654
  if (!svc || typeof svc !== "object") continue;
@@ -2287,7 +2702,7 @@ function validateConstraints(v) {
2287
2702
  if (!row) continue;
2288
2703
  const from = row.from === true;
2289
2704
  const to = row.to === true;
2290
- const origin = String((_b = row.origin) != null ? _b : "");
2705
+ const origin = String((_a = row.origin) != null ? _a : "");
2291
2706
  v.errors.push({
2292
2707
  code: "constraint_overridden",
2293
2708
  severity: "warning",
@@ -2321,14 +2736,14 @@ function validateCustomFields(v) {
2321
2736
 
2322
2737
  // src/core/validate/steps/global-utility-guard.ts
2323
2738
  function validateGlobalUtilityGuard(v) {
2324
- var _a, _b, _c;
2739
+ var _a, _b;
2325
2740
  if (!v.options.globalUtilityGuard) return;
2326
2741
  let hasUtility = false;
2327
2742
  let hasBase = false;
2328
2743
  for (const f of v.fields) {
2329
- for (const o of (_a = f.options) != null ? _a : []) {
2744
+ for (const { option: o } of walkFieldOptions(f)) {
2330
2745
  if (!isServiceIdRef(o.service_id)) continue;
2331
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
2746
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
2332
2747
  if (role === "base") hasBase = true;
2333
2748
  else if (role === "utility") hasUtility = true;
2334
2749
  if (hasUtility && hasBase) break;
@@ -2530,7 +2945,7 @@ function applyFilterAllowLists(tagId, fieldId, filter) {
2530
2945
  return true;
2531
2946
  }
2532
2947
  function collectServiceItems(args) {
2533
- var _a, _b, _c, _d, _e;
2948
+ var _a, _b, _c, _d;
2534
2949
  const filter = args.filter;
2535
2950
  const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
2536
2951
  const where = filter == null ? void 0 : filter.where;
@@ -2580,7 +2995,7 @@ function collectServiceItems(args) {
2580
2995
  affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
2581
2996
  });
2582
2997
  }
2583
- for (const o of (_d = f.options) != null ? _d : []) {
2998
+ for (const { option: o } of walkFieldOptions(f)) {
2584
2999
  const oSid = o.service_id;
2585
3000
  if (!isServiceIdRef2(oSid)) continue;
2586
3001
  const role = fieldRoleOf(f, o);
@@ -2665,7 +3080,7 @@ function collectServiceItems(args) {
2665
3080
  }
2666
3081
  } else if (includeGroupFallbacks) {
2667
3082
  const allowPrimaries = new Set(
2668
- ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
3083
+ ((_d = args.visiblePrimaries) != null ? _d : []).map((x) => String(x))
2669
3084
  );
2670
3085
  for (const primaryKey of allowPrimaries) {
2671
3086
  const list = globalFb[primaryKey];
@@ -2746,17 +3161,15 @@ function affectedFromItems(items) {
2746
3161
  return uniq(ids);
2747
3162
  }
2748
3163
  function visibleGroupNodeIds(tag, fields) {
2749
- var _a;
2750
3164
  const ids = [tag.id];
2751
3165
  for (const f of fields) {
2752
- for (const o of (_a = f.options) != null ? _a : []) {
3166
+ for (const { option: o } of walkFieldOptions(f)) {
2753
3167
  ids.push(o.id);
2754
3168
  }
2755
3169
  }
2756
3170
  return uniq(ids);
2757
3171
  }
2758
3172
  function visibleGroupPrimaries(tag, fields) {
2759
- var _a;
2760
3173
  const prim = [];
2761
3174
  const tagSid = tag.service_id;
2762
3175
  if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
@@ -2767,7 +3180,7 @@ function visibleGroupPrimaries(tag, fields) {
2767
3180
  if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
2768
3181
  prim.push(fsid);
2769
3182
  }
2770
- for (const o of (_a = f.options) != null ? _a : []) {
3183
+ for (const { option: o } of walkFieldOptions(f)) {
2771
3184
  const osid = o.service_id;
2772
3185
  if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
2773
3186
  prim.push(osid);
@@ -2991,6 +3404,7 @@ function validate(props, ctx = {}) {
2991
3404
  validateStructure(v);
2992
3405
  validateIdentity(v);
2993
3406
  validateOptionMaps(v);
3407
+ validateVisibilityCycles(v);
2994
3408
  validateOrderKinds(v);
2995
3409
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
2996
3410
  const visSim = readVisibilitySimOpts(options);
@@ -3135,14 +3549,14 @@ var BuilderImpl = class {
3135
3549
  const showOptions = showSet.has(f.id);
3136
3550
  if (!showOptions) continue;
3137
3551
  if (!Array.isArray(f.options)) continue;
3138
- for (const o of f.options) {
3552
+ for (const { option: o, parentId } of walkFieldOptions(f)) {
3139
3553
  nodes.push({
3140
3554
  id: o.id,
3141
3555
  kind: "option",
3142
3556
  label: o.label
3143
3557
  });
3144
3558
  const e = {
3145
- from: f.id,
3559
+ from: parentId != null ? parentId : f.id,
3146
3560
  to: o.id,
3147
3561
  kind: "option",
3148
3562
  meta: { ownerField: f.id }
@@ -3189,7 +3603,7 @@ var BuilderImpl = class {
3189
3603
  return { nodes, edges };
3190
3604
  }
3191
3605
  cleanedProps() {
3192
- var _a, _b, _c, _d, _e;
3606
+ var _a, _b, _c, _d, _e, _f;
3193
3607
  const fieldIds = new Set(this.props.fields.map((f) => f.id));
3194
3608
  const optionIds = /* @__PURE__ */ new Set();
3195
3609
  this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
@@ -3201,6 +3615,7 @@ var BuilderImpl = class {
3201
3615
  }
3202
3616
  const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
3203
3617
  const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
3618
+ const effectMap = (_e = this.props.option_effects_for_buttons) != null ? _e : {};
3204
3619
  const includedByButtons = /* @__PURE__ */ new Set();
3205
3620
  const referencedKeys = /* @__PURE__ */ new Set();
3206
3621
  const referencedOwnerFields = /* @__PURE__ */ new Set();
@@ -3220,6 +3635,14 @@ var BuilderImpl = class {
3220
3635
  void fid;
3221
3636
  }
3222
3637
  }
3638
+ for (const [key, targets] of Object.entries(effectMap)) {
3639
+ referencedKeys.add(key);
3640
+ const owner = this.optionOwnerById.get(key);
3641
+ if (owner) referencedOwnerFields.add(owner.fieldId);
3642
+ for (const [fid, effect] of Object.entries(targets != null ? targets : {})) {
3643
+ if ((effect == null ? void 0 : effect.forceVisible) === true) includedByButtons.add(fid);
3644
+ }
3645
+ }
3223
3646
  const boundIds = /* @__PURE__ */ new Set();
3224
3647
  for (const f of this.props.fields) {
3225
3648
  const b = f.bind_id;
@@ -3237,6 +3660,7 @@ var BuilderImpl = class {
3237
3660
  return bound || included || referenced || !excluded;
3238
3661
  });
3239
3662
  const allowedTargets = new Set(fields.map((f) => f.id));
3663
+ const allowedFieldById = new Map(fields.map((f) => [f.id, f]));
3240
3664
  const pruneButtons = (src) => {
3241
3665
  if (!src) return void 0;
3242
3666
  const out2 = {};
@@ -3256,13 +3680,52 @@ var BuilderImpl = class {
3256
3680
  const excludes_for_buttons = pruneButtons(
3257
3681
  this.props.excludes_for_buttons
3258
3682
  );
3683
+ const pruneOptionEffects = (src) => {
3684
+ var _a2, _b2, _c2, _d2;
3685
+ if (!src) return void 0;
3686
+ const out2 = {};
3687
+ for (const [key, targets] of Object.entries(src)) {
3688
+ const keyIsValid = optionIds.has(key) || fieldIds.has(key);
3689
+ if (!keyIsValid) continue;
3690
+ const cleanedTargets = {};
3691
+ for (const [targetFieldId, effect] of Object.entries(
3692
+ targets != null ? targets : {}
3693
+ )) {
3694
+ const field = allowedFieldById.get(targetFieldId);
3695
+ if (!field || !effect) continue;
3696
+ const validOptionIds = fieldOptionIdSet(field);
3697
+ const include = Array.from(
3698
+ new Set((_a2 = effect.include) != null ? _a2 : [])
3699
+ ).filter((optionId) => validOptionIds.has(optionId));
3700
+ const exclude = Array.from(
3701
+ new Set((_b2 = effect.exclude) != null ? _b2 : [])
3702
+ ).filter((optionId) => validOptionIds.has(optionId));
3703
+ const next = {
3704
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
3705
+ ...include.length ? { include } : {},
3706
+ ...exclude.length ? { exclude } : {}
3707
+ };
3708
+ if (next.forceVisible === true || ((_c2 = next.include) == null ? void 0 : _c2.length) || ((_d2 = next.exclude) == null ? void 0 : _d2.length)) {
3709
+ cleanedTargets[targetFieldId] = next;
3710
+ }
3711
+ }
3712
+ if (Object.keys(cleanedTargets).length) {
3713
+ out2[key] = cleanedTargets;
3714
+ }
3715
+ }
3716
+ return Object.keys(out2).length ? out2 : void 0;
3717
+ };
3718
+ const option_effects_for_buttons = pruneOptionEffects(
3719
+ this.props.option_effects_for_buttons
3720
+ );
3259
3721
  const out = {
3260
3722
  filters: this.props.filters.slice(),
3261
3723
  fields,
3262
3724
  ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
3263
3725
  ...includes_for_buttons && { includes_for_buttons },
3264
3726
  ...excludes_for_buttons && { excludes_for_buttons },
3265
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
3727
+ ...option_effects_for_buttons && { option_effects_for_buttons },
3728
+ schema_version: (_f = this.props.schema_version) != null ? _f : "1.0",
3266
3729
  // keep fallbacks & other maps as-is
3267
3730
  ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
3268
3731
  };
@@ -3275,12 +3738,15 @@ var BuilderImpl = class {
3275
3738
  return (0, import_lodash_es2.cloneDeep)(this.options);
3276
3739
  }
3277
3740
  visibleFields(tagId, selectedKeys) {
3741
+ return this.resolveVisibility(tagId, selectedKeys).fieldIds;
3742
+ }
3743
+ resolveVisibility(tagId, selectedKeys) {
3278
3744
  var _a;
3279
- return visibleFieldIdsUnder(this.props, tagId, {
3280
- selectedKeys: new Set(
3281
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
3282
- )
3283
- });
3745
+ return resolveVisibility(
3746
+ this.props,
3747
+ tagId,
3748
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
3749
+ );
3284
3750
  }
3285
3751
  getNodeMap() {
3286
3752
  if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
@@ -3295,9 +3761,8 @@ var BuilderImpl = class {
3295
3761
  for (const t of this.props.filters) this.tagById.set(t.id, t);
3296
3762
  for (const f of this.props.fields) {
3297
3763
  this.fieldById.set(f.id, f);
3298
- if (Array.isArray(f.options)) {
3299
- for (const o of f.options)
3300
- this.optionOwnerById.set(o.id, { fieldId: f.id });
3764
+ for (const [optionId, owner] of optionOwnerMap([f])) {
3765
+ this.optionOwnerById.set(optionId, { fieldId: owner.fieldId });
3301
3766
  }
3302
3767
  }
3303
3768
  }
@@ -4775,24 +5240,44 @@ function isFiniteNumber2(v) {
4775
5240
 
4776
5241
  // src/utils/build-order-snapshot/selection.ts
4777
5242
  function isOptionBased(f) {
4778
- const hasOptions = Array.isArray(f.options) && f.options.length > 0;
5243
+ const hasOptions = fieldOptionIdSet(f).size > 0;
4779
5244
  return hasOptions || isMultiField(f);
4780
5245
  }
4781
5246
  function toSelectedOptionKeys(byField) {
4782
5247
  const keys = [];
4783
- for (const [fieldId, optionIds] of Object.entries(byField != null ? byField : {})) {
5248
+ for (const optionIds of Object.values(byField != null ? byField : {})) {
4784
5249
  for (const optionId of optionIds != null ? optionIds : []) {
4785
- keys.push(`${fieldId}::${optionId}`);
5250
+ keys.push(optionId);
4786
5251
  }
4787
5252
  }
4788
5253
  return keys;
4789
5254
  }
4790
- function getSelectedOptionsByFieldId(selection, fieldById) {
4791
- const out = {};
5255
+ function getSelectedOptionsByFieldId(selection, fieldById, mode, visibleOptionsByFieldId) {
5256
+ var _a;
5257
+ const collected = {};
4792
5258
  for (const visit of buildSelectedNodeVisitOrder(selection, fieldById)) {
4793
5259
  if (visit.kind !== "option") continue;
4794
- if (!out[visit.fieldId]) out[visit.fieldId] = [];
4795
- out[visit.fieldId].push(visit.optionId);
5260
+ if (!collected[visit.fieldId]) collected[visit.fieldId] = [];
5261
+ collected[visit.fieldId].push(visit.optionId);
5262
+ }
5263
+ const out = {};
5264
+ for (const [fieldId, optionIds] of Object.entries(collected)) {
5265
+ const field = fieldById.get(fieldId);
5266
+ if (!field) continue;
5267
+ const validOptionIds = fieldOptionIdSet(field);
5268
+ const visibleOptionIds = (visibleOptionsByFieldId == null ? void 0 : visibleOptionsByFieldId[fieldId]) ? new Set(visibleOptionsByFieldId[fieldId]) : void 0;
5269
+ const dedupedValid = [];
5270
+ const seen = /* @__PURE__ */ new Set();
5271
+ for (const optionId of optionIds) {
5272
+ if (!validOptionIds.has(optionId)) continue;
5273
+ if (visibleOptionIds && !visibleOptionIds.has(optionId)) continue;
5274
+ if (seen.has(optionId)) continue;
5275
+ seen.add(optionId);
5276
+ dedupedValid.push(optionId);
5277
+ }
5278
+ const isMulti = ((_a = field.meta) == null ? void 0 : _a.multi) === true;
5279
+ const normalized = mode === "prod" && !isMulti ? dedupedValid.length ? [dedupedValid[dedupedValid.length - 1]] : [] : dedupedValid;
5280
+ if (normalized.length) out[fieldId] = normalized;
4796
5281
  }
4797
5282
  return out;
4798
5283
  }
@@ -4807,57 +5292,49 @@ function buildSelectedNodeVisitOrder(selection, fieldById) {
4807
5292
  out.push({ kind: "field", fieldId });
4808
5293
  }
4809
5294
  function pushOption(fieldId, optionId) {
4810
- const key = `option:${fieldId}::${optionId}`;
5295
+ const key = `option:${optionId}`;
4811
5296
  if (seen.has(key)) return;
4812
5297
  seen.add(key);
4813
5298
  out.push({ kind: "option", fieldId, optionId });
4814
5299
  }
4815
- for (const item of (_a = selection.optionTraversalOrder) != null ? _a : []) {
4816
- pushOption(item.fieldId, item.optionId);
5300
+ for (const optionId of (_a = selection.optionTraversalOrder) != null ? _a : []) {
5301
+ const ownerField = findOptionOwnerField(fieldById.values(), optionId);
5302
+ if (ownerField) pushOption(ownerField.id, optionId);
4817
5303
  }
4818
5304
  for (const rawKey of (_b = selection.selectedKeys) != null ? _b : []) {
4819
5305
  const key = String(rawKey);
4820
- if (key.includes("::")) {
4821
- const [fieldId, optionId] = key.split("::", 2);
4822
- if (fieldId && optionId) pushOption(fieldId, optionId);
4823
- continue;
4824
- }
4825
5306
  const field = fieldById.get(key);
4826
5307
  if (field) {
4827
5308
  pushField(field.id);
4828
5309
  continue;
4829
5310
  }
4830
- const ownerField = findOptionOwnerField(key, fieldById);
5311
+ const ownerField = findOptionOwnerField(fieldById.values(), key);
4831
5312
  if (ownerField) pushOption(ownerField.id, key);
4832
5313
  }
4833
5314
  for (const [fieldId, optionIds] of Object.entries(
4834
5315
  (_c = selection.optionSelectionsByFieldId) != null ? _c : {}
4835
5316
  )) {
4836
- if (!fieldById.has(fieldId)) continue;
5317
+ const hintedField = fieldById.get(fieldId);
5318
+ if (!hintedField) continue;
4837
5319
  for (const optionId of optionIds != null ? optionIds : []) {
4838
- pushOption(fieldId, optionId);
5320
+ const ownerField = findOptionOwnerField(fieldById.values(), optionId);
5321
+ if ((ownerField == null ? void 0 : ownerField.id) === hintedField.id) {
5322
+ pushOption(ownerField.id, optionId);
5323
+ }
4839
5324
  }
4840
5325
  }
4841
5326
  return out;
4842
5327
  }
4843
- function findOptionOwnerField(optionId, fieldById) {
4844
- var _a;
4845
- for (const field of fieldById.values()) {
4846
- if ((_a = field.options) == null ? void 0 : _a.some((option) => option.id === optionId)) return field;
4847
- }
4848
- return void 0;
4849
- }
4850
5328
 
4851
5329
  // src/utils/build-order-snapshot/services.ts
4852
5330
  function isServiceBased(field) {
4853
- var _a;
4854
5331
  if (field.service_id !== void 0 && field.service_id !== null) return true;
4855
- return !!((_a = field.options) == null ? void 0 : _a.some(
4856
- (item) => item.service_id !== void 0 && item.service_id !== null
4857
- ));
5332
+ return walkFieldOptions(field).some(
5333
+ ({ option }) => option.service_id !== void 0 && option.service_id !== null
5334
+ );
4858
5335
  }
4859
5336
  function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById, services) {
4860
- var _a, _b, _c, _d;
5337
+ var _a, _b, _c;
4861
5338
  const serviceMap = {};
4862
5339
  const visible = new Set(visibleFieldIds);
4863
5340
  const selectedBaseServices = [];
@@ -4884,9 +5361,9 @@ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById,
4884
5361
  }
4885
5362
  continue;
4886
5363
  }
4887
- const option = (_b = field.options) == null ? void 0 : _b.find((item) => item.id === visit.optionId);
5364
+ const option = findFieldOption(field, visit.optionId);
4888
5365
  if (!option) continue;
4889
- const role = (_d = (_c = option.pricing_role) != null ? _c : field.pricing_role) != null ? _d : "base";
5366
+ const role = (_c = (_b = option.pricing_role) != null ? _b : field.pricing_role) != null ? _c : "base";
4890
5367
  if (role === "utility") continue;
4891
5368
  if (option.service_id !== void 0 && option.service_id !== null) {
4892
5369
  addSelectedBaseService(option.id, option.service_id);
@@ -5017,16 +5494,15 @@ function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId,
5017
5494
  return { quantity: hostDefault, source: { kind: "default", defaultedFromHost: true } };
5018
5495
  }
5019
5496
  function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
5020
- var _a, _b, _c;
5497
+ var _a, _b;
5021
5498
  const visible = new Set(visibleFieldIds);
5022
5499
  const visits = buildSelectedNodeVisitOrder(selection, fieldById);
5023
5500
  for (const visit of visits) {
5024
5501
  if (visit.kind !== "option") continue;
5025
5502
  if (!visible.has(visit.fieldId)) continue;
5026
5503
  const field = fieldById.get(visit.fieldId);
5027
- if (!((_a = field == null ? void 0 : field.options) == null ? void 0 : _a.length)) continue;
5028
- const option = field.options.find((item) => item.id === visit.optionId);
5029
- const quantity = readPositiveFiniteNumber((_b = option == null ? void 0 : option.meta) == null ? void 0 : _b.quantityDefault);
5504
+ const option = findFieldOption(field, visit.optionId);
5505
+ const quantity = readPositiveFiniteNumber((_a = option == null ? void 0 : option.meta) == null ? void 0 : _a.quantityDefault);
5030
5506
  if (quantity !== void 0) {
5031
5507
  return { quantity, source: { kind: "option", id: option.id } };
5032
5508
  }
@@ -5041,7 +5517,7 @@ function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selecti
5041
5517
  }
5042
5518
  }
5043
5519
  const tag = tagById.get(tagId);
5044
- const tagQuantity = readPositiveFiniteNumber((_c = tag == null ? void 0 : tag.meta) == null ? void 0 : _c.quantityDefault);
5520
+ const tagQuantity = readPositiveFiniteNumber((_b = tag == null ? void 0 : tag.meta) == null ? void 0 : _b.quantityDefault);
5045
5521
  if (tagQuantity !== void 0) {
5046
5522
  return { quantity: tagQuantity, source: { kind: "tag", id: tagId } };
5047
5523
  }
@@ -5144,12 +5620,10 @@ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, selected
5144
5620
  const item = buildUtilityItemFromMarker(field.id, marker, quantity, value);
5145
5621
  if (item) items.push(item);
5146
5622
  }
5147
- if (Array.isArray(field.options) && field.options.length) {
5148
- const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5149
- if (!selectedOptionIds.length) continue;
5150
- const optById = new Map(field.options.map((o) => [o.id, o]));
5623
+ const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5624
+ if (selectedOptionIds.length) {
5151
5625
  for (const oid of selectedOptionIds) {
5152
- const option = optById.get(oid);
5626
+ const option = findFieldOption(field, oid);
5153
5627
  if (!option) continue;
5154
5628
  if (((_d = option.pricing_role) != null ? _d : "base") !== "utility") continue;
5155
5629
  const optionMarker = readUtilityMarker((_e = option.meta) == null ? void 0 : _e.utility);
@@ -5265,7 +5739,7 @@ function buildDevWarnings(props, svcMap, originalFallbacks, fieldById, visibleFi
5265
5739
 
5266
5740
  // src/utils/build-order-snapshot/index.ts
5267
5741
  function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
5268
- var _a, _b, _c, _d, _e, _f, _g, _h;
5742
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
5269
5743
  const mode = (_a = settings.mode) != null ? _a : "prod";
5270
5744
  const hostDefaultQty = Number.isFinite((_b = settings.hostDefaultQuantity) != null ? _b : 1) ? settings.hostDefaultQuantity : 1;
5271
5745
  const fbSettings = {
@@ -5278,11 +5752,32 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5278
5752
  const builtAt = (/* @__PURE__ */ new Date()).toISOString();
5279
5753
  const tagId = selection.activeTagId;
5280
5754
  const selectedButtonKeys = (_d = selection.selectedKeys) != null ? _d : toSelectedOptionKeys(selection.optionSelectionsByFieldId);
5281
- const visibleFieldIds = builder.visibleFields(tagId, selectedButtonKeys);
5282
5755
  const tagById = new Map(((_e = props.filters) != null ? _e : []).map((t) => [t.id, t]));
5283
5756
  const fieldById = new Map(((_f = props.fields) != null ? _f : []).map((f) => [f.id, f]));
5284
- const tagConstraints = (_h = (_g = tagById.get(tagId)) == null ? void 0 : _g.constraints) != null ? _h : void 0;
5285
- const selectedOptionsByFieldId = getSelectedOptionsByFieldId(selection, fieldById);
5757
+ const resolve = typeof builder.resolveVisibility === "function" ? builder.resolveVisibility.bind(builder) : void 0;
5758
+ let resolvedVisibility = resolve == null ? void 0 : resolve(tagId, selectedButtonKeys);
5759
+ let visibleFieldIds = (_g = resolvedVisibility == null ? void 0 : resolvedVisibility.fieldIds) != null ? _g : builder.visibleFields(tagId, selectedButtonKeys);
5760
+ const filteredSelectedButtonKeys = filterSelectedKeysByVisibility(
5761
+ selectedButtonKeys,
5762
+ visibleFieldIds,
5763
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId,
5764
+ fieldById
5765
+ );
5766
+ if (resolve && filteredSelectedButtonKeys.join("\0") !== selectedButtonKeys.join("\0")) {
5767
+ resolvedVisibility = resolve(tagId, filteredSelectedButtonKeys);
5768
+ visibleFieldIds = resolvedVisibility.fieldIds;
5769
+ }
5770
+ const effectiveSelection = {
5771
+ ...selection,
5772
+ selectedKeys: filteredSelectedButtonKeys
5773
+ };
5774
+ const tagConstraints = (_i = (_h = tagById.get(tagId)) == null ? void 0 : _h.constraints) != null ? _i : void 0;
5775
+ const selectedOptionsByFieldId = getSelectedOptionsByFieldId(
5776
+ effectiveSelection,
5777
+ fieldById,
5778
+ mode,
5779
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId
5780
+ );
5286
5781
  const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
5287
5782
  var _a2;
5288
5783
  const optionIds = isOptionBased(f) ? (_a2 = selectedOptionsByFieldId[f.id]) != null ? _a2 : [] : void 0;
@@ -5295,21 +5790,21 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5295
5790
  const { formValues, selections } = buildInputs(
5296
5791
  visibleFieldIds,
5297
5792
  fieldById,
5298
- selection,
5793
+ effectiveSelection,
5299
5794
  selectedOptionsByFieldId
5300
5795
  );
5301
5796
  const qtyRes = resolveQuantity(
5302
5797
  visibleFieldIds,
5303
5798
  fieldById,
5304
5799
  tagById,
5305
- selection,
5800
+ effectiveSelection,
5306
5801
  tagId,
5307
5802
  hostDefaultQty
5308
5803
  );
5309
5804
  const { serviceMap, servicesList } = resolveServices(
5310
5805
  tagId,
5311
5806
  visibleFieldIds,
5312
- selection,
5807
+ effectiveSelection,
5313
5808
  tagById,
5314
5809
  fieldById,
5315
5810
  services
@@ -5331,7 +5826,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5331
5826
  const utilities = collectUtilityLineItems(
5332
5827
  visibleFieldIds,
5333
5828
  fieldById,
5334
- selection,
5829
+ effectiveSelection,
5335
5830
  selectedOptionsByFieldId,
5336
5831
  qtyRes.quantity
5337
5832
  );
@@ -5341,7 +5836,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5341
5836
  prunedFallbacks.original,
5342
5837
  fieldById,
5343
5838
  visibleFieldIds,
5344
- selection
5839
+ effectiveSelection
5345
5840
  ) : void 0;
5346
5841
  const meta = {
5347
5842
  schema_version: props.schema_version,
@@ -5354,7 +5849,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5354
5849
  tagId,
5355
5850
  visibleFieldIds,
5356
5851
  fieldById,
5357
- selection,
5852
+ effectiveSelection,
5358
5853
  selectedOptionsByFieldId
5359
5854
  ),
5360
5855
  policy: toSnapshotPolicy(fbSettings)
@@ -5366,7 +5861,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5366
5861
  builtAt,
5367
5862
  selection: {
5368
5863
  tag: tagId,
5369
- buttons: selectedButtonKeys,
5864
+ buttons: filteredSelectedButtonKeys,
5370
5865
  fields: selectionFields
5371
5866
  },
5372
5867
  inputs: { form: formValues, selections },
@@ -5384,6 +5879,24 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5384
5879
  meta
5385
5880
  };
5386
5881
  }
5882
+ function filterSelectedKeysByVisibility(selectedKeys, visibleFieldIds, optionsByFieldId, fieldById) {
5883
+ if (!optionsByFieldId) return selectedKeys;
5884
+ const visibleFields = new Set(visibleFieldIds);
5885
+ const out = [];
5886
+ for (const rawKey of selectedKeys) {
5887
+ const key = String(rawKey);
5888
+ if (fieldById.has(key)) {
5889
+ if (visibleFields.has(key)) out.push(key);
5890
+ continue;
5891
+ }
5892
+ const owner = findOptionOwnerField(fieldById.values(), key);
5893
+ if (!owner || !visibleFields.has(owner.id)) continue;
5894
+ const allowed = optionsByFieldId[owner.id];
5895
+ if (allowed && !allowed.includes(key)) continue;
5896
+ out.push(key);
5897
+ }
5898
+ return out;
5899
+ }
5387
5900
 
5388
5901
  // src/core/fallback-editor.ts
5389
5902
  function createFallbackEditor(options = {}) {
@@ -5765,16 +6278,24 @@ function mapDiagReason(reason) {
5765
6278
  createBuilder,
5766
6279
  createFallbackEditor,
5767
6280
  createNodeIndex,
6281
+ fieldOptionIdSet,
6282
+ fieldOptionIds,
6283
+ filterFieldOptionsById,
5768
6284
  filterServicesForVisibleGroup,
6285
+ findFieldOption,
6286
+ findOptionOwnerField,
5769
6287
  getAssignedServiceIds,
5770
6288
  getEligibleFallbacks,
5771
6289
  getFallbackRegistrationInfo,
5772
6290
  isRefExcludedBySelectedKeys,
5773
6291
  normalise,
5774
6292
  normalizeFieldValidation,
6293
+ optionOwnerMap,
5775
6294
  resolveServiceFallback,
5776
6295
  validate,
5777
6296
  validateAsync,
5778
- validateRateCoherenceDeep
6297
+ validateRateCoherenceDeep,
6298
+ walkFieldOptions,
6299
+ walkOptions
5779
6300
  });
5780
6301
  //# sourceMappingURL=index.cjs.map