@timeax/digital-service-engine 0.3.5 → 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,20 +5240,20 @@ 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, mode) {
4791
- var _a, _b;
5255
+ function getSelectedOptionsByFieldId(selection, fieldById, mode, visibleOptionsByFieldId) {
5256
+ var _a;
4792
5257
  const collected = {};
4793
5258
  for (const visit of buildSelectedNodeVisitOrder(selection, fieldById)) {
4794
5259
  if (visit.kind !== "option") continue;
@@ -4799,18 +5264,18 @@ function getSelectedOptionsByFieldId(selection, fieldById, mode) {
4799
5264
  for (const [fieldId, optionIds] of Object.entries(collected)) {
4800
5265
  const field = fieldById.get(fieldId);
4801
5266
  if (!field) continue;
4802
- const validOptionIds = new Set(
4803
- ((_a = field.options) != null ? _a : []).map((option) => option.id)
4804
- );
5267
+ const validOptionIds = fieldOptionIdSet(field);
5268
+ const visibleOptionIds = (visibleOptionsByFieldId == null ? void 0 : visibleOptionsByFieldId[fieldId]) ? new Set(visibleOptionsByFieldId[fieldId]) : void 0;
4805
5269
  const dedupedValid = [];
4806
5270
  const seen = /* @__PURE__ */ new Set();
4807
5271
  for (const optionId of optionIds) {
4808
5272
  if (!validOptionIds.has(optionId)) continue;
5273
+ if (visibleOptionIds && !visibleOptionIds.has(optionId)) continue;
4809
5274
  if (seen.has(optionId)) continue;
4810
5275
  seen.add(optionId);
4811
5276
  dedupedValid.push(optionId);
4812
5277
  }
4813
- const isMulti = ((_b = field.meta) == null ? void 0 : _b.multi) === true;
5278
+ const isMulti = ((_a = field.meta) == null ? void 0 : _a.multi) === true;
4814
5279
  const normalized = mode === "prod" && !isMulti ? dedupedValid.length ? [dedupedValid[dedupedValid.length - 1]] : [] : dedupedValid;
4815
5280
  if (normalized.length) out[fieldId] = normalized;
4816
5281
  }
@@ -4827,57 +5292,49 @@ function buildSelectedNodeVisitOrder(selection, fieldById) {
4827
5292
  out.push({ kind: "field", fieldId });
4828
5293
  }
4829
5294
  function pushOption(fieldId, optionId) {
4830
- const key = `option:${fieldId}::${optionId}`;
5295
+ const key = `option:${optionId}`;
4831
5296
  if (seen.has(key)) return;
4832
5297
  seen.add(key);
4833
5298
  out.push({ kind: "option", fieldId, optionId });
4834
5299
  }
4835
- for (const item of (_a = selection.optionTraversalOrder) != null ? _a : []) {
4836
- 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);
4837
5303
  }
4838
5304
  for (const rawKey of (_b = selection.selectedKeys) != null ? _b : []) {
4839
5305
  const key = String(rawKey);
4840
- if (key.includes("::")) {
4841
- const [fieldId, optionId] = key.split("::", 2);
4842
- if (fieldId && optionId) pushOption(fieldId, optionId);
4843
- continue;
4844
- }
4845
5306
  const field = fieldById.get(key);
4846
5307
  if (field) {
4847
5308
  pushField(field.id);
4848
5309
  continue;
4849
5310
  }
4850
- const ownerField = findOptionOwnerField(key, fieldById);
5311
+ const ownerField = findOptionOwnerField(fieldById.values(), key);
4851
5312
  if (ownerField) pushOption(ownerField.id, key);
4852
5313
  }
4853
5314
  for (const [fieldId, optionIds] of Object.entries(
4854
5315
  (_c = selection.optionSelectionsByFieldId) != null ? _c : {}
4855
5316
  )) {
4856
- if (!fieldById.has(fieldId)) continue;
5317
+ const hintedField = fieldById.get(fieldId);
5318
+ if (!hintedField) continue;
4857
5319
  for (const optionId of optionIds != null ? optionIds : []) {
4858
- 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
+ }
4859
5324
  }
4860
5325
  }
4861
5326
  return out;
4862
5327
  }
4863
- function findOptionOwnerField(optionId, fieldById) {
4864
- var _a;
4865
- for (const field of fieldById.values()) {
4866
- if ((_a = field.options) == null ? void 0 : _a.some((option) => option.id === optionId)) return field;
4867
- }
4868
- return void 0;
4869
- }
4870
5328
 
4871
5329
  // src/utils/build-order-snapshot/services.ts
4872
5330
  function isServiceBased(field) {
4873
- var _a;
4874
5331
  if (field.service_id !== void 0 && field.service_id !== null) return true;
4875
- return !!((_a = field.options) == null ? void 0 : _a.some(
4876
- (item) => item.service_id !== void 0 && item.service_id !== null
4877
- ));
5332
+ return walkFieldOptions(field).some(
5333
+ ({ option }) => option.service_id !== void 0 && option.service_id !== null
5334
+ );
4878
5335
  }
4879
5336
  function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById, services) {
4880
- var _a, _b, _c, _d;
5337
+ var _a, _b, _c;
4881
5338
  const serviceMap = {};
4882
5339
  const visible = new Set(visibleFieldIds);
4883
5340
  const selectedBaseServices = [];
@@ -4904,9 +5361,9 @@ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById,
4904
5361
  }
4905
5362
  continue;
4906
5363
  }
4907
- const option = (_b = field.options) == null ? void 0 : _b.find((item) => item.id === visit.optionId);
5364
+ const option = findFieldOption(field, visit.optionId);
4908
5365
  if (!option) continue;
4909
- 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";
4910
5367
  if (role === "utility") continue;
4911
5368
  if (option.service_id !== void 0 && option.service_id !== null) {
4912
5369
  addSelectedBaseService(option.id, option.service_id);
@@ -5037,16 +5494,15 @@ function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId,
5037
5494
  return { quantity: hostDefault, source: { kind: "default", defaultedFromHost: true } };
5038
5495
  }
5039
5496
  function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
5040
- var _a, _b, _c;
5497
+ var _a, _b;
5041
5498
  const visible = new Set(visibleFieldIds);
5042
5499
  const visits = buildSelectedNodeVisitOrder(selection, fieldById);
5043
5500
  for (const visit of visits) {
5044
5501
  if (visit.kind !== "option") continue;
5045
5502
  if (!visible.has(visit.fieldId)) continue;
5046
5503
  const field = fieldById.get(visit.fieldId);
5047
- if (!((_a = field == null ? void 0 : field.options) == null ? void 0 : _a.length)) continue;
5048
- const option = field.options.find((item) => item.id === visit.optionId);
5049
- 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);
5050
5506
  if (quantity !== void 0) {
5051
5507
  return { quantity, source: { kind: "option", id: option.id } };
5052
5508
  }
@@ -5061,7 +5517,7 @@ function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selecti
5061
5517
  }
5062
5518
  }
5063
5519
  const tag = tagById.get(tagId);
5064
- 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);
5065
5521
  if (tagQuantity !== void 0) {
5066
5522
  return { quantity: tagQuantity, source: { kind: "tag", id: tagId } };
5067
5523
  }
@@ -5164,12 +5620,10 @@ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, selected
5164
5620
  const item = buildUtilityItemFromMarker(field.id, marker, quantity, value);
5165
5621
  if (item) items.push(item);
5166
5622
  }
5167
- if (Array.isArray(field.options) && field.options.length) {
5168
- const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5169
- if (!selectedOptionIds.length) continue;
5170
- const optById = new Map(field.options.map((o) => [o.id, o]));
5623
+ const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5624
+ if (selectedOptionIds.length) {
5171
5625
  for (const oid of selectedOptionIds) {
5172
- const option = optById.get(oid);
5626
+ const option = findFieldOption(field, oid);
5173
5627
  if (!option) continue;
5174
5628
  if (((_d = option.pricing_role) != null ? _d : "base") !== "utility") continue;
5175
5629
  const optionMarker = readUtilityMarker((_e = option.meta) == null ? void 0 : _e.utility);
@@ -5285,7 +5739,7 @@ function buildDevWarnings(props, svcMap, originalFallbacks, fieldById, visibleFi
5285
5739
 
5286
5740
  // src/utils/build-order-snapshot/index.ts
5287
5741
  function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
5288
- var _a, _b, _c, _d, _e, _f, _g, _h;
5742
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
5289
5743
  const mode = (_a = settings.mode) != null ? _a : "prod";
5290
5744
  const hostDefaultQty = Number.isFinite((_b = settings.hostDefaultQuantity) != null ? _b : 1) ? settings.hostDefaultQuantity : 1;
5291
5745
  const fbSettings = {
@@ -5298,14 +5752,31 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5298
5752
  const builtAt = (/* @__PURE__ */ new Date()).toISOString();
5299
5753
  const tagId = selection.activeTagId;
5300
5754
  const selectedButtonKeys = (_d = selection.selectedKeys) != null ? _d : toSelectedOptionKeys(selection.optionSelectionsByFieldId);
5301
- const visibleFieldIds = builder.visibleFields(tagId, selectedButtonKeys);
5302
5755
  const tagById = new Map(((_e = props.filters) != null ? _e : []).map((t) => [t.id, t]));
5303
5756
  const fieldById = new Map(((_f = props.fields) != null ? _f : []).map((f) => [f.id, f]));
5304
- const tagConstraints = (_h = (_g = tagById.get(tagId)) == null ? void 0 : _g.constraints) != null ? _h : void 0;
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;
5305
5775
  const selectedOptionsByFieldId = getSelectedOptionsByFieldId(
5306
- selection,
5776
+ effectiveSelection,
5307
5777
  fieldById,
5308
- mode
5778
+ mode,
5779
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId
5309
5780
  );
5310
5781
  const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
5311
5782
  var _a2;
@@ -5319,21 +5790,21 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5319
5790
  const { formValues, selections } = buildInputs(
5320
5791
  visibleFieldIds,
5321
5792
  fieldById,
5322
- selection,
5793
+ effectiveSelection,
5323
5794
  selectedOptionsByFieldId
5324
5795
  );
5325
5796
  const qtyRes = resolveQuantity(
5326
5797
  visibleFieldIds,
5327
5798
  fieldById,
5328
5799
  tagById,
5329
- selection,
5800
+ effectiveSelection,
5330
5801
  tagId,
5331
5802
  hostDefaultQty
5332
5803
  );
5333
5804
  const { serviceMap, servicesList } = resolveServices(
5334
5805
  tagId,
5335
5806
  visibleFieldIds,
5336
- selection,
5807
+ effectiveSelection,
5337
5808
  tagById,
5338
5809
  fieldById,
5339
5810
  services
@@ -5355,7 +5826,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5355
5826
  const utilities = collectUtilityLineItems(
5356
5827
  visibleFieldIds,
5357
5828
  fieldById,
5358
- selection,
5829
+ effectiveSelection,
5359
5830
  selectedOptionsByFieldId,
5360
5831
  qtyRes.quantity
5361
5832
  );
@@ -5365,7 +5836,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5365
5836
  prunedFallbacks.original,
5366
5837
  fieldById,
5367
5838
  visibleFieldIds,
5368
- selection
5839
+ effectiveSelection
5369
5840
  ) : void 0;
5370
5841
  const meta = {
5371
5842
  schema_version: props.schema_version,
@@ -5378,7 +5849,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5378
5849
  tagId,
5379
5850
  visibleFieldIds,
5380
5851
  fieldById,
5381
- selection,
5852
+ effectiveSelection,
5382
5853
  selectedOptionsByFieldId
5383
5854
  ),
5384
5855
  policy: toSnapshotPolicy(fbSettings)
@@ -5390,7 +5861,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5390
5861
  builtAt,
5391
5862
  selection: {
5392
5863
  tag: tagId,
5393
- buttons: selectedButtonKeys,
5864
+ buttons: filteredSelectedButtonKeys,
5394
5865
  fields: selectionFields
5395
5866
  },
5396
5867
  inputs: { form: formValues, selections },
@@ -5408,6 +5879,24 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5408
5879
  meta
5409
5880
  };
5410
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
+ }
5411
5900
 
5412
5901
  // src/core/fallback-editor.ts
5413
5902
  function createFallbackEditor(options = {}) {
@@ -5789,16 +6278,24 @@ function mapDiagReason(reason) {
5789
6278
  createBuilder,
5790
6279
  createFallbackEditor,
5791
6280
  createNodeIndex,
6281
+ fieldOptionIdSet,
6282
+ fieldOptionIds,
6283
+ filterFieldOptionsById,
5792
6284
  filterServicesForVisibleGroup,
6285
+ findFieldOption,
6286
+ findOptionOwnerField,
5793
6287
  getAssignedServiceIds,
5794
6288
  getEligibleFallbacks,
5795
6289
  getFallbackRegistrationInfo,
5796
6290
  isRefExcludedBySelectedKeys,
5797
6291
  normalise,
5798
6292
  normalizeFieldValidation,
6293
+ optionOwnerMap,
5799
6294
  resolveServiceFallback,
5800
6295
  validate,
5801
6296
  validateAsync,
5802
- validateRateCoherenceDeep
6297
+ validateRateCoherenceDeep,
6298
+ walkFieldOptions,
6299
+ walkOptions
5803
6300
  });
5804
6301
  //# sourceMappingURL=index.cjs.map