@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.
@@ -13,6 +13,9 @@ function normalise(input, opts = {}) {
13
13
  const excludes_for_buttons = toStringArrayMap(
14
14
  obj.excludes_for_buttons
15
15
  );
16
+ const option_effects_for_buttons = toOptionEffectMap(
17
+ obj.option_effects_for_buttons
18
+ );
16
19
  const orderKinds = toStringMap(obj.orderKinds);
17
20
  const notices = toNoticeArray(obj.notices);
18
21
  let filters = rawFilters.map((t) => coerceTag(t, constraints));
@@ -28,6 +31,9 @@ function normalise(input, opts = {}) {
28
31
  ...isNonEmpty(orderKinds) && { orderKinds },
29
32
  ...isNonEmpty(includes_for_buttons) && { includes_for_buttons },
30
33
  ...isNonEmpty(excludes_for_buttons) && { excludes_for_buttons },
34
+ ...isNonEmpty(option_effects_for_buttons) && {
35
+ option_effects_for_buttons
36
+ },
31
37
  ...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
32
38
  fallbacks
33
39
  },
@@ -182,6 +188,7 @@ function coerceOption(src, inheritRole) {
182
188
  const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
183
189
  const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
184
190
  const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
191
+ const children = Array.isArray(src.children) ? src.children.map((child) => coerceOption(child, pricing_role)) : void 0;
185
192
  const option = {
186
193
  id: "",
187
194
  label: "",
@@ -190,7 +197,8 @@ function coerceOption(src, inheritRole) {
190
197
  ...value !== void 0 && { value },
191
198
  ...service_id !== void 0 && { service_id },
192
199
  pricing_role,
193
- ...meta && { meta }
200
+ ...meta && { meta },
201
+ ...children && children.length && { children }
194
202
  };
195
203
  return option;
196
204
  }
@@ -255,6 +263,35 @@ function toStringArrayMap(src) {
255
263
  }
256
264
  return Object.keys(out).length ? out : void 0;
257
265
  }
266
+ function toOptionEffectMap(src) {
267
+ var _a, _b;
268
+ if (!src || typeof src !== "object") return void 0;
269
+ const out = {};
270
+ for (const [triggerId, rawTargets] of Object.entries(src)) {
271
+ if (!triggerId || !rawTargets || typeof rawTargets !== "object") {
272
+ continue;
273
+ }
274
+ const targets = {};
275
+ for (const [fieldId, rawEffect] of Object.entries(rawTargets)) {
276
+ if (!fieldId || !rawEffect || typeof rawEffect !== "object") {
277
+ continue;
278
+ }
279
+ const effect = rawEffect;
280
+ const include = toStringArray(effect.include);
281
+ const exclude = toStringArray(effect.exclude);
282
+ const next = {
283
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
284
+ ...include.length ? { include: dedupe(include) } : {},
285
+ ...exclude.length ? { exclude: dedupe(exclude) } : {}
286
+ };
287
+ if (next.forceVisible === true || ((_a = next.include) == null ? void 0 : _a.length) || ((_b = next.exclude) == null ? void 0 : _b.length)) {
288
+ targets[fieldId] = next;
289
+ }
290
+ }
291
+ if (Object.keys(targets).length) out[triggerId] = targets;
292
+ }
293
+ return Object.keys(out).length ? out : void 0;
294
+ }
258
295
  function toStringArray(v) {
259
296
  if (!Array.isArray(v)) return [];
260
297
  return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
@@ -329,6 +366,76 @@ function normalizeFieldValidation(input) {
329
366
  return one ? [one] : void 0;
330
367
  }
331
368
 
369
+ // src/core/options.ts
370
+ function walkFieldOptions(field) {
371
+ const out = [];
372
+ const visit = (options, depth, parentId) => {
373
+ for (const option of options != null ? options : []) {
374
+ out.push({
375
+ field,
376
+ fieldId: field.id,
377
+ option,
378
+ optionId: option.id,
379
+ depth,
380
+ parentId
381
+ });
382
+ visit(option.children, depth + 1, option.id);
383
+ }
384
+ };
385
+ visit(field.options, 0);
386
+ return out;
387
+ }
388
+ function walkOptions(props) {
389
+ var _a;
390
+ return ((_a = props.fields) != null ? _a : []).flatMap((field) => walkFieldOptions(field));
391
+ }
392
+ function fieldOptionIds(field) {
393
+ return walkFieldOptions(field).map((visit) => visit.optionId);
394
+ }
395
+ function fieldOptionIdSet(field) {
396
+ return new Set(fieldOptionIds(field));
397
+ }
398
+ function findFieldOption(field, optionId) {
399
+ var _a;
400
+ if (!field) return void 0;
401
+ return (_a = walkFieldOptions(field).find((visit) => visit.optionId === optionId)) == null ? void 0 : _a.option;
402
+ }
403
+ function findOptionOwnerField(fields, optionId) {
404
+ for (const field of fields) {
405
+ if (findFieldOption(field, optionId)) return field;
406
+ }
407
+ return void 0;
408
+ }
409
+ function optionOwnerMap(fields) {
410
+ const out = /* @__PURE__ */ new Map();
411
+ for (const field of fields) {
412
+ for (const visit of walkFieldOptions(field)) {
413
+ if (!out.has(visit.optionId)) {
414
+ out.set(visit.optionId, {
415
+ fieldId: field.id,
416
+ option: visit.option
417
+ });
418
+ }
419
+ }
420
+ }
421
+ return out;
422
+ }
423
+ function filterFieldOptionsById(options, allowed) {
424
+ if (!Array.isArray(options)) return void 0;
425
+ const out = [];
426
+ for (const option of options) {
427
+ const children = filterFieldOptionsById(option.children, allowed);
428
+ if (!allowed.has(option.id) && (!children || children.length === 0)) {
429
+ continue;
430
+ }
431
+ out.push({
432
+ ...option,
433
+ ...children ? { children } : {}
434
+ });
435
+ }
436
+ return out;
437
+ }
438
+
332
439
  // src/core/validate/shared.ts
333
440
  function isFiniteNumber(v) {
334
441
  return typeof v === "number" && Number.isFinite(v);
@@ -337,8 +444,9 @@ function isServiceIdRef(v) {
337
444
  return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
338
445
  }
339
446
  function hasAnyServiceOption(f) {
340
- var _a;
341
- return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
447
+ return walkFieldOptions(f).some(
448
+ (visit) => isServiceIdRef(visit.option.service_id)
449
+ );
342
450
  }
343
451
  function getByPath(obj, path) {
344
452
  if (!path) return void 0;
@@ -427,14 +535,14 @@ function withAffected(details, ids) {
427
535
 
428
536
  // src/core/node-map.ts
429
537
  function buildNodeMap(props) {
430
- var _a, _b, _c;
538
+ var _a, _b;
431
539
  const map = /* @__PURE__ */ new Map();
432
540
  for (const t of (_a = props.filters) != null ? _a : []) {
433
541
  if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
434
542
  }
435
543
  for (const f of (_b = props.fields) != null ? _b : []) {
436
544
  if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
437
- for (const o of (_c = f.options) != null ? _c : []) {
545
+ for (const { option: o } of walkFieldOptions(f)) {
438
546
  if (!map.has(o.id))
439
547
  map.set(o.id, {
440
548
  kind: "option",
@@ -447,12 +555,6 @@ function buildNodeMap(props) {
447
555
  return map;
448
556
  }
449
557
  function resolveTrigger(trigger, nodeMap) {
450
- const idx = trigger.indexOf("::");
451
- if (idx !== -1) {
452
- const fieldId = trigger.slice(0, idx);
453
- const optionId = trigger.slice(idx + 2);
454
- return { kind: "composite", triggerKey: trigger, fieldId, optionId };
455
- }
456
558
  const direct = nodeMap.get(trigger);
457
559
  if (!direct) return void 0;
458
560
  if (direct.kind === "option") {
@@ -504,11 +606,6 @@ function visibleFieldIdsUnder(props, tagId, opts = {}) {
504
606
  const ownerDepthForTriggerKey = (triggerKey) => {
505
607
  const t = resolveTrigger(triggerKey, nodeMap);
506
608
  if (!t) return void 0;
507
- if (t.kind === "composite") {
508
- const f = fieldById.get(t.fieldId);
509
- if (!f) return void 0;
510
- return ownerDepthForField(f);
511
- }
512
609
  if (t.kind === "field") {
513
610
  const f = fieldById.get(t.id);
514
611
  if (!f || f.button !== true) return void 0;
@@ -591,6 +688,84 @@ function visibleFieldsUnder(props, tagId, opts = {}) {
591
688
  const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
592
689
  return ids.map((id) => fieldById.get(id)).filter(Boolean);
593
690
  }
691
+ function resolveVisibility(props, tagId, selectedKeys) {
692
+ var _a, _b, _c, _d;
693
+ const selected = new Set(selectedKeys != null ? selectedKeys : []);
694
+ const baseFieldIds = visibleFieldIdsUnder(props, tagId, { selectedKeys: selected });
695
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((field) => [field.id, field]));
696
+ const visible = new Set(baseFieldIds);
697
+ const forced = /* @__PURE__ */ new Set();
698
+ const optionsByFieldId = {};
699
+ const optionIdsByFieldId = /* @__PURE__ */ new Map();
700
+ const getOptionIds = (field) => {
701
+ let ids = optionIdsByFieldId.get(field.id);
702
+ if (!ids) {
703
+ ids = fieldOptionIds(field);
704
+ optionIdsByFieldId.set(field.id, ids);
705
+ }
706
+ return ids;
707
+ };
708
+ const ensureOptions = (field) => {
709
+ const ids = getOptionIds(field);
710
+ if (!ids.length) return void 0;
711
+ if (!optionsByFieldId[field.id]) optionsByFieldId[field.id] = [...ids];
712
+ return optionsByFieldId[field.id];
713
+ };
714
+ for (const fieldId of baseFieldIds) {
715
+ const field = fieldById.get(fieldId);
716
+ if (field) ensureOptions(field);
717
+ }
718
+ const effects = (_b = props.option_effects_for_buttons) != null ? _b : {};
719
+ for (const triggerId of selected) {
720
+ const targetRules = effects[triggerId];
721
+ if (!targetRules) continue;
722
+ for (const [targetFieldId, rule] of Object.entries(targetRules)) {
723
+ const field = fieldById.get(targetFieldId);
724
+ if (!field) continue;
725
+ const isVisible = visible.has(targetFieldId);
726
+ if (!isVisible && rule.forceVisible !== true) continue;
727
+ if (!isVisible && rule.forceVisible === true) {
728
+ visible.add(targetFieldId);
729
+ forced.add(targetFieldId);
730
+ }
731
+ const orderedOptionIds = getOptionIds(field);
732
+ if (!orderedOptionIds.length) continue;
733
+ const known = new Set(orderedOptionIds);
734
+ let allowed = (_c = optionsByFieldId[targetFieldId]) != null ? _c : [...orderedOptionIds];
735
+ if (Array.isArray(rule.include) && rule.include.length) {
736
+ const include = new Set(
737
+ rule.include.filter((optionId) => known.has(optionId))
738
+ );
739
+ allowed = orderedOptionIds.filter(
740
+ (optionId) => include.has(optionId) && allowed.includes(optionId)
741
+ );
742
+ }
743
+ if (Array.isArray(rule.exclude) && rule.exclude.length) {
744
+ const exclude = new Set(
745
+ rule.exclude.filter((optionId) => known.has(optionId))
746
+ );
747
+ allowed = allowed.filter((optionId) => !exclude.has(optionId));
748
+ }
749
+ optionsByFieldId[targetFieldId] = allowed;
750
+ }
751
+ }
752
+ const visibleFieldIds = baseFieldIds.filter((fieldId) => visible.has(fieldId));
753
+ const seen = new Set(visibleFieldIds);
754
+ for (const field of (_d = props.fields) != null ? _d : []) {
755
+ if (!visible.has(field.id) || seen.has(field.id)) continue;
756
+ seen.add(field.id);
757
+ visibleFieldIds.push(field.id);
758
+ ensureOptions(field);
759
+ }
760
+ for (const fieldId of Object.keys(optionsByFieldId)) {
761
+ if (!visible.has(fieldId)) delete optionsByFieldId[fieldId];
762
+ }
763
+ return {
764
+ fieldIds: visibleFieldIds,
765
+ optionsByFieldId,
766
+ forcedFieldIds: visibleFieldIds.filter((fieldId) => forced.has(fieldId))
767
+ };
768
+ }
594
769
 
595
770
  // src/core/validate/steps/visibility.ts
596
771
  function createFieldsVisibleUnder(v) {
@@ -608,7 +783,6 @@ function resolveRootTags(tags) {
608
783
  return roots.length ? roots : tags.slice(0, 1);
609
784
  }
610
785
  function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
611
- var _a;
612
786
  const visible = visibleFieldsUnder(v.props, tagId, {
613
787
  selectedKeys
614
788
  });
@@ -618,7 +792,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
618
792
  const t = f.id;
619
793
  if (effectfulKeys.has(t)) triggers.push(t);
620
794
  }
621
- for (const o of (_a = f.options) != null ? _a : []) {
795
+ for (const { option: o } of walkFieldOptions(f)) {
622
796
  const t = o.id;
623
797
  if (effectfulKeys.has(t)) triggers.push(t);
624
798
  }
@@ -627,7 +801,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
627
801
  return triggers;
628
802
  }
629
803
  function runVisibilityRulesOnce(v) {
630
- var _a, _b, _c, _d, _e;
804
+ var _a, _b, _c, _d;
631
805
  for (const t of v.tags) {
632
806
  const visible = v.fieldsVisibleUnder(t.id);
633
807
  const seen = /* @__PURE__ */ new Map();
@@ -677,9 +851,9 @@ function runVisibilityRulesOnce(v) {
677
851
  let hasUtility = false;
678
852
  const utilityOptionIds = [];
679
853
  for (const f of visible) {
680
- for (const o of (_c = f.options) != null ? _c : []) {
854
+ for (const { option: o } of walkFieldOptions(f)) {
681
855
  if (!isServiceIdRef(o.service_id)) continue;
682
- const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
856
+ const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
683
857
  if (role === "base") hasBase = true;
684
858
  else if (role === "utility") {
685
859
  hasUtility = true;
@@ -720,7 +894,7 @@ function dedupeErrorsInPlace(v, startIndex) {
720
894
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
721
895
  }
722
896
  function validateVisibility(v, options = {}) {
723
- var _a, _b, _c, _d, _e;
897
+ var _a, _b, _c, _d, _e, _f;
724
898
  v.simulatedVisibilityContexts = [];
725
899
  const simulate = options.simulate === true;
726
900
  if (!simulate) {
@@ -745,10 +919,13 @@ function validateVisibility(v, options = {}) {
745
919
  for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
746
920
  effectfulKeys.add(key);
747
921
  }
922
+ for (const key of Object.keys((_e = v.props.option_effects_for_buttons) != null ? _e : {})) {
923
+ effectfulKeys.add(key);
924
+ }
748
925
  }
749
926
  const roots = resolveRootTags(v.tags);
750
927
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
751
- const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
928
+ const originalSelected = new Set((_f = v.selectedKeys) != null ? _f : []);
752
929
  const errorsStart = v.errors.length;
753
930
  const visited = /* @__PURE__ */ new Set();
754
931
  const seenContexts = /* @__PURE__ */ new Set();
@@ -889,7 +1066,7 @@ function validateStructure(v) {
889
1066
 
890
1067
  // src/core/validate/steps/identity.ts
891
1068
  function validateIdentity(v) {
892
- var _a, _b;
1069
+ var _a;
893
1070
  const tags = v.tags;
894
1071
  const fields = v.fields;
895
1072
  {
@@ -989,7 +1166,7 @@ function validateIdentity(v) {
989
1166
  }
990
1167
  }
991
1168
  for (const f of fields) {
992
- for (const o of (_b = f.options) != null ? _b : []) {
1169
+ for (const { option: o } of walkFieldOptions(f)) {
993
1170
  if (!o.label || !o.label.trim()) {
994
1171
  v.errors.push({
995
1172
  code: "label_missing",
@@ -1004,25 +1181,11 @@ function validateIdentity(v) {
1004
1181
  }
1005
1182
 
1006
1183
  // src/core/validate/steps/option-maps.ts
1007
- function parseFieldOptionKey(key) {
1008
- const idx = key.indexOf("::");
1009
- if (idx === -1) return null;
1010
- const fieldId = key.slice(0, idx).trim();
1011
- const optionId = key.slice(idx + 2).trim();
1012
- if (!fieldId || !optionId) return null;
1013
- return { fieldId, optionId };
1014
- }
1015
- function hasOption(v, fid, oid) {
1016
- var _a;
1017
- const f = v.fieldById.get(fid);
1018
- if (!f) return false;
1019
- return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
1020
- }
1021
1184
  function validateOptionMaps(v) {
1022
- var _a, _b;
1185
+ var _a, _b, _c;
1023
1186
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
1024
1187
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
1025
- 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.`;
1188
+ const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known option id or button-field id.`;
1026
1189
  const validateTriggerKey = (key) => {
1027
1190
  const ref = v.nodeMap.get(key);
1028
1191
  if (ref) {
@@ -1041,19 +1204,7 @@ function validateOptionMaps(v) {
1041
1204
  }
1042
1205
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
1043
1206
  }
1044
- const p = parseFieldOptionKey(key);
1045
- if (!p) return { ok: false };
1046
- if (!hasOption(v, p.fieldId, p.optionId))
1047
- return {
1048
- ok: false,
1049
- nodeId: p.fieldId,
1050
- affected: [p.fieldId, p.optionId]
1051
- };
1052
- return {
1053
- ok: true,
1054
- nodeId: p.fieldId,
1055
- affected: [p.fieldId, p.optionId]
1056
- };
1207
+ return { ok: false };
1057
1208
  };
1058
1209
  for (const k of Object.keys(incMap)) {
1059
1210
  const r = validateTriggerKey(k);
@@ -1079,6 +1230,57 @@ function validateOptionMaps(v) {
1079
1230
  });
1080
1231
  }
1081
1232
  }
1233
+ const effectMap = (_c = v.props.option_effects_for_buttons) != null ? _c : {};
1234
+ for (const [triggerKey, targets] of Object.entries(effectMap)) {
1235
+ const trigger = validateTriggerKey(triggerKey);
1236
+ if (!trigger.ok) {
1237
+ v.errors.push({
1238
+ code: "bad_option_effect_key",
1239
+ severity: "error",
1240
+ message: badKeyMessage(triggerKey),
1241
+ nodeId: trigger.nodeId,
1242
+ details: withAffected({ key: triggerKey }, trigger.affected)
1243
+ });
1244
+ }
1245
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
1246
+ const field = v.fieldById.get(targetFieldId);
1247
+ if (!field) {
1248
+ v.errors.push({
1249
+ code: "bad_option_effect_target",
1250
+ severity: "error",
1251
+ message: `Option effect trigger "${triggerKey}" targets unknown field "${targetFieldId}".`,
1252
+ details: withAffected(
1253
+ { key: triggerKey, targetFieldId },
1254
+ trigger.affected
1255
+ )
1256
+ });
1257
+ continue;
1258
+ }
1259
+ const validOptionIds = fieldOptionIdSet(field);
1260
+ const checkTargetOptions = (kind, optionIds) => {
1261
+ for (const optionId of optionIds != null ? optionIds : []) {
1262
+ if (validOptionIds.has(optionId)) continue;
1263
+ v.errors.push({
1264
+ code: "bad_option_effect_option",
1265
+ severity: "error",
1266
+ message: `Option effect trigger "${triggerKey}" references unknown ${kind} option "${optionId}" for field "${targetFieldId}".`,
1267
+ nodeId: targetFieldId,
1268
+ details: withAffected(
1269
+ {
1270
+ key: triggerKey,
1271
+ targetFieldId,
1272
+ optionId,
1273
+ kind
1274
+ },
1275
+ [targetFieldId, optionId]
1276
+ )
1277
+ });
1278
+ }
1279
+ };
1280
+ checkTargetOptions("include", effect == null ? void 0 : effect.include);
1281
+ checkTargetOptions("exclude", effect == null ? void 0 : effect.exclude);
1282
+ }
1283
+ }
1082
1284
  for (const k of Object.keys(incMap)) {
1083
1285
  if (!(k in excMap)) continue;
1084
1286
  const r = validateTriggerKey(k);
@@ -1092,27 +1294,231 @@ function validateOptionMaps(v) {
1092
1294
  }
1093
1295
  }
1094
1296
 
1095
- // src/utils/order-kind.ts
1096
- function normalizeSelectedTriggerKey(key, nodeMap) {
1097
- if (!key) return void 0;
1098
- const compositeIdx = key.indexOf("::");
1099
- if (compositeIdx !== -1) {
1100
- const fieldId = key.slice(0, compositeIdx).trim();
1101
- const optionId = key.slice(compositeIdx + 2).trim();
1102
- if (optionId) {
1103
- const optionRef = nodeMap.get(optionId);
1104
- if ((optionRef == null ? void 0 : optionRef.kind) === "option") {
1105
- return { nodeId: optionRef.id, nodeKind: "option" };
1297
+ // src/core/validate/steps/visibility-cycles.ts
1298
+ var MAX_VISIBILITY_CYCLE_DEPTH = 20;
1299
+ function validateVisibilityCycles(v) {
1300
+ const triggerById = buildTriggerIndex(v.fields);
1301
+ if (!triggerById.size) return;
1302
+ const fieldTriggers = buildFieldTriggerIndex(v.fields);
1303
+ const revealTargetsByTrigger = buildRevealIndex(v, triggerById);
1304
+ const reported = /* @__PURE__ */ new Set();
1305
+ for (const rootTriggerId of Array.from(triggerById.keys()).sort()) {
1306
+ const required = makeRequiredState(triggerById, [rootTriggerId]);
1307
+ walkFromTrigger({
1308
+ v,
1309
+ triggerById,
1310
+ fieldTriggers,
1311
+ revealTargetsByTrigger,
1312
+ rootTriggerId,
1313
+ currentTriggerId: rootTriggerId,
1314
+ required,
1315
+ path: [rootTriggerId],
1316
+ visited: /* @__PURE__ */ new Set(),
1317
+ reported,
1318
+ depth: 0
1319
+ });
1320
+ }
1321
+ }
1322
+ function buildTriggerIndex(fields) {
1323
+ const out = /* @__PURE__ */ new Map();
1324
+ const owners = optionOwnerMap(fields);
1325
+ for (const field of fields) {
1326
+ if (field.button === true) {
1327
+ out.set(field.id, {
1328
+ kind: "field",
1329
+ id: field.id,
1330
+ ownerFieldId: field.id
1331
+ });
1332
+ }
1333
+ }
1334
+ for (const [optionId, owner] of owners) {
1335
+ out.set(optionId, {
1336
+ kind: "option",
1337
+ id: optionId,
1338
+ ownerFieldId: owner.fieldId
1339
+ });
1340
+ }
1341
+ return out;
1342
+ }
1343
+ function buildFieldTriggerIndex(fields) {
1344
+ const out = /* @__PURE__ */ new Map();
1345
+ for (const field of fields) {
1346
+ const triggers = [];
1347
+ if (field.button === true) triggers.push(field.id);
1348
+ for (const visit of walkFieldOptions(field)) {
1349
+ triggers.push(visit.optionId);
1350
+ }
1351
+ out.set(field.id, triggers);
1352
+ }
1353
+ return out;
1354
+ }
1355
+ function buildRevealIndex(v, triggerById) {
1356
+ var _a, _b;
1357
+ const out = /* @__PURE__ */ new Map();
1358
+ const addReveal = (triggerId, targetFieldId) => {
1359
+ var _a2;
1360
+ if (!triggerById.has(triggerId)) return;
1361
+ if (!v.fieldById.has(targetFieldId)) return;
1362
+ const set = (_a2 = out.get(triggerId)) != null ? _a2 : /* @__PURE__ */ new Set();
1363
+ set.add(targetFieldId);
1364
+ out.set(triggerId, set);
1365
+ };
1366
+ for (const [triggerId, targetIds] of Object.entries(
1367
+ (_a = v.props.includes_for_buttons) != null ? _a : {}
1368
+ )) {
1369
+ for (const targetId of targetIds != null ? targetIds : []) addReveal(triggerId, targetId);
1370
+ }
1371
+ for (const [triggerId, targets] of Object.entries(
1372
+ (_b = v.props.option_effects_for_buttons) != null ? _b : {}
1373
+ )) {
1374
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
1375
+ if ((effect == null ? void 0 : effect.forceVisible) === true)
1376
+ addReveal(triggerId, targetFieldId);
1377
+ }
1378
+ }
1379
+ return new Map(
1380
+ Array.from(out.entries()).map(([triggerId, fieldIds]) => [
1381
+ triggerId,
1382
+ Array.from(fieldIds).sort()
1383
+ ])
1384
+ );
1385
+ }
1386
+ function walkFromTrigger(args) {
1387
+ var _a, _b, _c;
1388
+ if (args.depth >= MAX_VISIBILITY_CYCLE_DEPTH) return;
1389
+ const visitedKey = `${args.rootTriggerId}::${args.currentTriggerId}::${args.path.join(">")}`;
1390
+ if (args.visited.has(visitedKey)) return;
1391
+ args.visited.add(visitedKey);
1392
+ const revealedFieldIds = (_a = args.revealTargetsByTrigger.get(args.currentTriggerId)) != null ? _a : [];
1393
+ for (const revealedFieldId of revealedFieldIds) {
1394
+ const reachableTriggers = (_c = (_b = args.fieldTriggers.get(revealedFieldId)) == null ? void 0 : _b.slice().sort()) != null ? _c : [];
1395
+ for (const reachableTriggerId of reachableTriggers) {
1396
+ const invalidation = invalidatesRequiredPath(
1397
+ args.v,
1398
+ args.triggerById,
1399
+ reachableTriggerId,
1400
+ args.required
1401
+ );
1402
+ if (invalidation) {
1403
+ emitCycleError({
1404
+ v: args.v,
1405
+ rootTriggerId: args.rootTriggerId,
1406
+ revealedFieldId,
1407
+ conflictingTriggerId: reachableTriggerId,
1408
+ invalidatedId: invalidation.invalidatedId,
1409
+ path: [...args.path, reachableTriggerId],
1410
+ reported: args.reported
1411
+ });
1412
+ }
1413
+ if (args.path.includes(reachableTriggerId)) continue;
1414
+ walkFromTrigger({
1415
+ ...args,
1416
+ currentTriggerId: reachableTriggerId,
1417
+ required: addRequiredTrigger(
1418
+ args.triggerById,
1419
+ args.required,
1420
+ reachableTriggerId
1421
+ ),
1422
+ path: [...args.path, reachableTriggerId],
1423
+ depth: args.depth + 1
1424
+ });
1425
+ }
1426
+ }
1427
+ }
1428
+ function makeRequiredState(triggerById, triggerIds) {
1429
+ let required = {
1430
+ triggers: /* @__PURE__ */ new Set(),
1431
+ ownerFields: /* @__PURE__ */ new Set()
1432
+ };
1433
+ for (const triggerId of triggerIds) {
1434
+ required = addRequiredTrigger(triggerById, required, triggerId);
1435
+ }
1436
+ return required;
1437
+ }
1438
+ function addRequiredTrigger(triggerById, current, triggerId) {
1439
+ const next = {
1440
+ triggers: new Set(current.triggers),
1441
+ ownerFields: new Set(current.ownerFields)
1442
+ };
1443
+ const trigger = triggerById.get(triggerId);
1444
+ if (!trigger) return next;
1445
+ next.triggers.add(triggerId);
1446
+ next.ownerFields.add(trigger.ownerFieldId);
1447
+ return next;
1448
+ }
1449
+ function invalidatesRequiredPath(v, triggerById, conflictingTriggerId, required) {
1450
+ var _a, _b, _c, _d, _e, _f;
1451
+ for (const targetId of (_b = (_a = v.props.excludes_for_buttons) == null ? void 0 : _a[conflictingTriggerId]) != null ? _b : []) {
1452
+ if (required.ownerFields.has(targetId)) {
1453
+ return { invalidatedId: targetId };
1454
+ }
1455
+ const targetTrigger = triggerById.get(targetId);
1456
+ if ((targetTrigger == null ? void 0 : targetTrigger.kind) === "option" && required.triggers.has(targetId)) {
1457
+ return { invalidatedId: targetId };
1458
+ }
1459
+ }
1460
+ const effects = (_d = (_c = v.props.option_effects_for_buttons) == null ? void 0 : _c[conflictingTriggerId]) != null ? _d : {};
1461
+ for (const [targetFieldId, effect] of Object.entries(effects)) {
1462
+ if (!v.fieldById.has(targetFieldId)) continue;
1463
+ if ((_e = effect == null ? void 0 : effect.exclude) == null ? void 0 : _e.length) {
1464
+ const excluded = new Set(effect.exclude);
1465
+ for (const requiredTriggerId of required.triggers) {
1466
+ const requiredTrigger = triggerById.get(requiredTriggerId);
1467
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
1468
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
1469
+ if (excluded.has(requiredTriggerId)) {
1470
+ return { invalidatedId: requiredTriggerId };
1471
+ }
1106
1472
  }
1107
1473
  }
1108
- if (fieldId) {
1109
- const fieldRef = nodeMap.get(fieldId);
1110
- if ((fieldRef == null ? void 0 : fieldRef.kind) === "field") {
1111
- return { nodeId: fieldRef.id, nodeKind: "field" };
1474
+ if ((_f = effect == null ? void 0 : effect.include) == null ? void 0 : _f.length) {
1475
+ const included = new Set(effect.include);
1476
+ for (const requiredTriggerId of required.triggers) {
1477
+ const requiredTrigger = triggerById.get(requiredTriggerId);
1478
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
1479
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
1480
+ if (!included.has(requiredTriggerId)) {
1481
+ return { invalidatedId: requiredTriggerId };
1482
+ }
1112
1483
  }
1113
1484
  }
1114
- return void 0;
1115
1485
  }
1486
+ return void 0;
1487
+ }
1488
+ function emitCycleError(args) {
1489
+ const key = [
1490
+ args.rootTriggerId,
1491
+ args.conflictingTriggerId,
1492
+ args.invalidatedId,
1493
+ args.path.join(">")
1494
+ ].join("::");
1495
+ if (args.reported.has(key)) return;
1496
+ args.reported.add(key);
1497
+ args.v.errors.push({
1498
+ code: "visibility_dependency_cycle",
1499
+ severity: "error",
1500
+ message: `Visibility dependency cycle: trigger "${args.rootTriggerId}" reveals "${args.revealedFieldId}", but reachable trigger "${args.conflictingTriggerId}" can hide or remove "${args.invalidatedId}".`,
1501
+ nodeId: args.conflictingTriggerId,
1502
+ details: withAffected(
1503
+ {
1504
+ rootTriggerId: args.rootTriggerId,
1505
+ conflictingTriggerId: args.conflictingTriggerId,
1506
+ invalidatedId: args.invalidatedId,
1507
+ path: args.path
1508
+ },
1509
+ [
1510
+ args.rootTriggerId,
1511
+ args.revealedFieldId,
1512
+ args.conflictingTriggerId,
1513
+ args.invalidatedId
1514
+ ]
1515
+ )
1516
+ });
1517
+ }
1518
+
1519
+ // src/utils/order-kind.ts
1520
+ function normalizeSelectedTriggerKey(key, nodeMap) {
1521
+ if (!key) return void 0;
1116
1522
  const ref = nodeMap.get(key);
1117
1523
  if (!ref) return void 0;
1118
1524
  if (ref.kind !== "field" && ref.kind !== "option") return void 0;
@@ -1271,8 +1677,7 @@ function validateUtilityMarkers(v) {
1271
1677
  "percent"
1272
1678
  ]);
1273
1679
  for (const f of v.fields) {
1274
- const optsArr = Array.isArray(f.options) ? f.options : [];
1275
- for (const o of optsArr) {
1680
+ for (const { option: o } of walkFieldOptions(f)) {
1276
1681
  const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
1277
1682
  const hasService = isServiceIdRef(o.service_id);
1278
1683
  const util = (_c = o.meta) == null ? void 0 : _c.utility;
@@ -1508,13 +1913,13 @@ function normalizeServiceRef(value) {
1508
1913
 
1509
1914
  // src/core/validate/steps/rates.ts
1510
1915
  function validateRates(v) {
1511
- var _a, _b, _c;
1916
+ var _a, _b;
1512
1917
  const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1513
1918
  for (const f of v.fields) {
1514
1919
  if (!isMultiField(f)) continue;
1515
1920
  const baseRates = [];
1516
- for (const o of (_a = f.options) != null ? _a : []) {
1517
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1921
+ for (const { option: o } of walkFieldOptions(f)) {
1922
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
1518
1923
  if (role !== "base") continue;
1519
1924
  const sid = o.service_id;
1520
1925
  if (!isServiceIdRef(sid)) continue;
@@ -1716,8 +2121,9 @@ function collectAnchors(fields) {
1716
2121
  const anchors = [];
1717
2122
  for (const field of fields) {
1718
2123
  if (!isButton(field)) continue;
1719
- if (Array.isArray(field.options) && field.options.length > 0) {
1720
- for (const option of field.options) {
2124
+ const optionVisits = walkFieldOptions(field);
2125
+ if (optionVisits.length > 0) {
2126
+ for (const { option } of optionVisits) {
1721
2127
  anchors.push({
1722
2128
  kind: "option",
1723
2129
  id: option.id,
@@ -1766,8 +2172,9 @@ function collectFieldReferences(field, services) {
1766
2172
  function collectBaseMembers(field, services) {
1767
2173
  var _a, _b, _c;
1768
2174
  const members = [];
1769
- if (Array.isArray(field.options) && field.options.length > 0) {
1770
- for (const option of field.options) {
2175
+ const optionVisits = walkFieldOptions(field);
2176
+ if (optionVisits.length > 0) {
2177
+ for (const { option } of optionVisits) {
1771
2178
  const role2 = normalizeRole((_a = option.pricing_role) != null ? _a : field.pricing_role, "base");
1772
2179
  if (role2 !== "base") continue;
1773
2180
  if (option.service_id === void 0 || option.service_id === null) {
@@ -2182,7 +2589,7 @@ function effectiveConstraints(v, tagId) {
2182
2589
  return out;
2183
2590
  }
2184
2591
  function validateConstraints(v) {
2185
- var _a, _b;
2592
+ var _a;
2186
2593
  for (const t of v.tags) {
2187
2594
  const eff = effectiveConstraints(v, t.id);
2188
2595
  const hasAnyRequired = Object.values(eff).some(
@@ -2191,7 +2598,7 @@ function validateConstraints(v) {
2191
2598
  if (!hasAnyRequired) continue;
2192
2599
  const visible = v.fieldsVisibleUnder(t.id);
2193
2600
  for (const f of visible) {
2194
- for (const o of (_a = f.options) != null ? _a : []) {
2601
+ for (const { option: o } of walkFieldOptions(f)) {
2195
2602
  if (!isServiceIdRef(o.service_id)) continue;
2196
2603
  const svc = getServiceCapability(v.serviceMap, o.service_id);
2197
2604
  if (!svc || typeof svc !== "object") continue;
@@ -2245,7 +2652,7 @@ function validateConstraints(v) {
2245
2652
  if (!row) continue;
2246
2653
  const from = row.from === true;
2247
2654
  const to = row.to === true;
2248
- const origin = String((_b = row.origin) != null ? _b : "");
2655
+ const origin = String((_a = row.origin) != null ? _a : "");
2249
2656
  v.errors.push({
2250
2657
  code: "constraint_overridden",
2251
2658
  severity: "warning",
@@ -2279,14 +2686,14 @@ function validateCustomFields(v) {
2279
2686
 
2280
2687
  // src/core/validate/steps/global-utility-guard.ts
2281
2688
  function validateGlobalUtilityGuard(v) {
2282
- var _a, _b, _c;
2689
+ var _a, _b;
2283
2690
  if (!v.options.globalUtilityGuard) return;
2284
2691
  let hasUtility = false;
2285
2692
  let hasBase = false;
2286
2693
  for (const f of v.fields) {
2287
- for (const o of (_a = f.options) != null ? _a : []) {
2694
+ for (const { option: o } of walkFieldOptions(f)) {
2288
2695
  if (!isServiceIdRef(o.service_id)) continue;
2289
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
2696
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
2290
2697
  if (role === "base") hasBase = true;
2291
2698
  else if (role === "utility") hasUtility = true;
2292
2699
  if (hasUtility && hasBase) break;
@@ -2488,7 +2895,7 @@ function applyFilterAllowLists(tagId, fieldId, filter) {
2488
2895
  return true;
2489
2896
  }
2490
2897
  function collectServiceItems(args) {
2491
- var _a, _b, _c, _d, _e;
2898
+ var _a, _b, _c, _d;
2492
2899
  const filter = args.filter;
2493
2900
  const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
2494
2901
  const where = filter == null ? void 0 : filter.where;
@@ -2538,7 +2945,7 @@ function collectServiceItems(args) {
2538
2945
  affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
2539
2946
  });
2540
2947
  }
2541
- for (const o of (_d = f.options) != null ? _d : []) {
2948
+ for (const { option: o } of walkFieldOptions(f)) {
2542
2949
  const oSid = o.service_id;
2543
2950
  if (!isServiceIdRef2(oSid)) continue;
2544
2951
  const role = fieldRoleOf(f, o);
@@ -2623,7 +3030,7 @@ function collectServiceItems(args) {
2623
3030
  }
2624
3031
  } else if (includeGroupFallbacks) {
2625
3032
  const allowPrimaries = new Set(
2626
- ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
3033
+ ((_d = args.visiblePrimaries) != null ? _d : []).map((x) => String(x))
2627
3034
  );
2628
3035
  for (const primaryKey of allowPrimaries) {
2629
3036
  const list = globalFb[primaryKey];
@@ -2704,17 +3111,15 @@ function affectedFromItems(items) {
2704
3111
  return uniq(ids);
2705
3112
  }
2706
3113
  function visibleGroupNodeIds(tag, fields) {
2707
- var _a;
2708
3114
  const ids = [tag.id];
2709
3115
  for (const f of fields) {
2710
- for (const o of (_a = f.options) != null ? _a : []) {
3116
+ for (const { option: o } of walkFieldOptions(f)) {
2711
3117
  ids.push(o.id);
2712
3118
  }
2713
3119
  }
2714
3120
  return uniq(ids);
2715
3121
  }
2716
3122
  function visibleGroupPrimaries(tag, fields) {
2717
- var _a;
2718
3123
  const prim = [];
2719
3124
  const tagSid = tag.service_id;
2720
3125
  if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
@@ -2725,7 +3130,7 @@ function visibleGroupPrimaries(tag, fields) {
2725
3130
  if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
2726
3131
  prim.push(fsid);
2727
3132
  }
2728
- for (const o of (_a = f.options) != null ? _a : []) {
3133
+ for (const { option: o } of walkFieldOptions(f)) {
2729
3134
  const osid = o.service_id;
2730
3135
  if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
2731
3136
  prim.push(osid);
@@ -2949,6 +3354,7 @@ function validate(props, ctx = {}) {
2949
3354
  validateStructure(v);
2950
3355
  validateIdentity(v);
2951
3356
  validateOptionMaps(v);
3357
+ validateVisibilityCycles(v);
2952
3358
  validateOrderKinds(v);
2953
3359
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
2954
3360
  const visSim = readVisibilitySimOpts(options);
@@ -3093,14 +3499,14 @@ var BuilderImpl = class {
3093
3499
  const showOptions = showSet.has(f.id);
3094
3500
  if (!showOptions) continue;
3095
3501
  if (!Array.isArray(f.options)) continue;
3096
- for (const o of f.options) {
3502
+ for (const { option: o, parentId } of walkFieldOptions(f)) {
3097
3503
  nodes.push({
3098
3504
  id: o.id,
3099
3505
  kind: "option",
3100
3506
  label: o.label
3101
3507
  });
3102
3508
  const e = {
3103
- from: f.id,
3509
+ from: parentId != null ? parentId : f.id,
3104
3510
  to: o.id,
3105
3511
  kind: "option",
3106
3512
  meta: { ownerField: f.id }
@@ -3147,7 +3553,7 @@ var BuilderImpl = class {
3147
3553
  return { nodes, edges };
3148
3554
  }
3149
3555
  cleanedProps() {
3150
- var _a, _b, _c, _d, _e;
3556
+ var _a, _b, _c, _d, _e, _f;
3151
3557
  const fieldIds = new Set(this.props.fields.map((f) => f.id));
3152
3558
  const optionIds = /* @__PURE__ */ new Set();
3153
3559
  this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
@@ -3159,6 +3565,7 @@ var BuilderImpl = class {
3159
3565
  }
3160
3566
  const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
3161
3567
  const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
3568
+ const effectMap = (_e = this.props.option_effects_for_buttons) != null ? _e : {};
3162
3569
  const includedByButtons = /* @__PURE__ */ new Set();
3163
3570
  const referencedKeys = /* @__PURE__ */ new Set();
3164
3571
  const referencedOwnerFields = /* @__PURE__ */ new Set();
@@ -3178,6 +3585,14 @@ var BuilderImpl = class {
3178
3585
  void fid;
3179
3586
  }
3180
3587
  }
3588
+ for (const [key, targets] of Object.entries(effectMap)) {
3589
+ referencedKeys.add(key);
3590
+ const owner = this.optionOwnerById.get(key);
3591
+ if (owner) referencedOwnerFields.add(owner.fieldId);
3592
+ for (const [fid, effect] of Object.entries(targets != null ? targets : {})) {
3593
+ if ((effect == null ? void 0 : effect.forceVisible) === true) includedByButtons.add(fid);
3594
+ }
3595
+ }
3181
3596
  const boundIds = /* @__PURE__ */ new Set();
3182
3597
  for (const f of this.props.fields) {
3183
3598
  const b = f.bind_id;
@@ -3195,6 +3610,7 @@ var BuilderImpl = class {
3195
3610
  return bound || included || referenced || !excluded;
3196
3611
  });
3197
3612
  const allowedTargets = new Set(fields.map((f) => f.id));
3613
+ const allowedFieldById = new Map(fields.map((f) => [f.id, f]));
3198
3614
  const pruneButtons = (src) => {
3199
3615
  if (!src) return void 0;
3200
3616
  const out2 = {};
@@ -3214,13 +3630,52 @@ var BuilderImpl = class {
3214
3630
  const excludes_for_buttons = pruneButtons(
3215
3631
  this.props.excludes_for_buttons
3216
3632
  );
3633
+ const pruneOptionEffects = (src) => {
3634
+ var _a2, _b2, _c2, _d2;
3635
+ if (!src) return void 0;
3636
+ const out2 = {};
3637
+ for (const [key, targets] of Object.entries(src)) {
3638
+ const keyIsValid = optionIds.has(key) || fieldIds.has(key);
3639
+ if (!keyIsValid) continue;
3640
+ const cleanedTargets = {};
3641
+ for (const [targetFieldId, effect] of Object.entries(
3642
+ targets != null ? targets : {}
3643
+ )) {
3644
+ const field = allowedFieldById.get(targetFieldId);
3645
+ if (!field || !effect) continue;
3646
+ const validOptionIds = fieldOptionIdSet(field);
3647
+ const include = Array.from(
3648
+ new Set((_a2 = effect.include) != null ? _a2 : [])
3649
+ ).filter((optionId) => validOptionIds.has(optionId));
3650
+ const exclude = Array.from(
3651
+ new Set((_b2 = effect.exclude) != null ? _b2 : [])
3652
+ ).filter((optionId) => validOptionIds.has(optionId));
3653
+ const next = {
3654
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
3655
+ ...include.length ? { include } : {},
3656
+ ...exclude.length ? { exclude } : {}
3657
+ };
3658
+ if (next.forceVisible === true || ((_c2 = next.include) == null ? void 0 : _c2.length) || ((_d2 = next.exclude) == null ? void 0 : _d2.length)) {
3659
+ cleanedTargets[targetFieldId] = next;
3660
+ }
3661
+ }
3662
+ if (Object.keys(cleanedTargets).length) {
3663
+ out2[key] = cleanedTargets;
3664
+ }
3665
+ }
3666
+ return Object.keys(out2).length ? out2 : void 0;
3667
+ };
3668
+ const option_effects_for_buttons = pruneOptionEffects(
3669
+ this.props.option_effects_for_buttons
3670
+ );
3217
3671
  const out = {
3218
3672
  filters: this.props.filters.slice(),
3219
3673
  fields,
3220
3674
  ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
3221
3675
  ...includes_for_buttons && { includes_for_buttons },
3222
3676
  ...excludes_for_buttons && { excludes_for_buttons },
3223
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
3677
+ ...option_effects_for_buttons && { option_effects_for_buttons },
3678
+ schema_version: (_f = this.props.schema_version) != null ? _f : "1.0",
3224
3679
  // keep fallbacks & other maps as-is
3225
3680
  ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
3226
3681
  };
@@ -3233,12 +3688,15 @@ var BuilderImpl = class {
3233
3688
  return cloneDeep2(this.options);
3234
3689
  }
3235
3690
  visibleFields(tagId, selectedKeys) {
3691
+ return this.resolveVisibility(tagId, selectedKeys).fieldIds;
3692
+ }
3693
+ resolveVisibility(tagId, selectedKeys) {
3236
3694
  var _a;
3237
- return visibleFieldIdsUnder(this.props, tagId, {
3238
- selectedKeys: new Set(
3239
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
3240
- )
3241
- });
3695
+ return resolveVisibility(
3696
+ this.props,
3697
+ tagId,
3698
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
3699
+ );
3242
3700
  }
3243
3701
  getNodeMap() {
3244
3702
  if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
@@ -3253,9 +3711,8 @@ var BuilderImpl = class {
3253
3711
  for (const t of this.props.filters) this.tagById.set(t.id, t);
3254
3712
  for (const f of this.props.fields) {
3255
3713
  this.fieldById.set(f.id, f);
3256
- if (Array.isArray(f.options)) {
3257
- for (const o of f.options)
3258
- this.optionOwnerById.set(o.id, { fieldId: f.id });
3714
+ for (const [optionId, owner] of optionOwnerMap([f])) {
3715
+ this.optionOwnerById.set(optionId, { fieldId: owner.fieldId });
3259
3716
  }
3260
3717
  }
3261
3718
  }
@@ -4733,20 +5190,20 @@ function isFiniteNumber2(v) {
4733
5190
 
4734
5191
  // src/utils/build-order-snapshot/selection.ts
4735
5192
  function isOptionBased(f) {
4736
- const hasOptions = Array.isArray(f.options) && f.options.length > 0;
5193
+ const hasOptions = fieldOptionIdSet(f).size > 0;
4737
5194
  return hasOptions || isMultiField(f);
4738
5195
  }
4739
5196
  function toSelectedOptionKeys(byField) {
4740
5197
  const keys = [];
4741
- for (const [fieldId, optionIds] of Object.entries(byField != null ? byField : {})) {
5198
+ for (const optionIds of Object.values(byField != null ? byField : {})) {
4742
5199
  for (const optionId of optionIds != null ? optionIds : []) {
4743
- keys.push(`${fieldId}::${optionId}`);
5200
+ keys.push(optionId);
4744
5201
  }
4745
5202
  }
4746
5203
  return keys;
4747
5204
  }
4748
- function getSelectedOptionsByFieldId(selection, fieldById, mode) {
4749
- var _a, _b;
5205
+ function getSelectedOptionsByFieldId(selection, fieldById, mode, visibleOptionsByFieldId) {
5206
+ var _a;
4750
5207
  const collected = {};
4751
5208
  for (const visit of buildSelectedNodeVisitOrder(selection, fieldById)) {
4752
5209
  if (visit.kind !== "option") continue;
@@ -4757,18 +5214,18 @@ function getSelectedOptionsByFieldId(selection, fieldById, mode) {
4757
5214
  for (const [fieldId, optionIds] of Object.entries(collected)) {
4758
5215
  const field = fieldById.get(fieldId);
4759
5216
  if (!field) continue;
4760
- const validOptionIds = new Set(
4761
- ((_a = field.options) != null ? _a : []).map((option) => option.id)
4762
- );
5217
+ const validOptionIds = fieldOptionIdSet(field);
5218
+ const visibleOptionIds = (visibleOptionsByFieldId == null ? void 0 : visibleOptionsByFieldId[fieldId]) ? new Set(visibleOptionsByFieldId[fieldId]) : void 0;
4763
5219
  const dedupedValid = [];
4764
5220
  const seen = /* @__PURE__ */ new Set();
4765
5221
  for (const optionId of optionIds) {
4766
5222
  if (!validOptionIds.has(optionId)) continue;
5223
+ if (visibleOptionIds && !visibleOptionIds.has(optionId)) continue;
4767
5224
  if (seen.has(optionId)) continue;
4768
5225
  seen.add(optionId);
4769
5226
  dedupedValid.push(optionId);
4770
5227
  }
4771
- const isMulti = ((_b = field.meta) == null ? void 0 : _b.multi) === true;
5228
+ const isMulti = ((_a = field.meta) == null ? void 0 : _a.multi) === true;
4772
5229
  const normalized = mode === "prod" && !isMulti ? dedupedValid.length ? [dedupedValid[dedupedValid.length - 1]] : [] : dedupedValid;
4773
5230
  if (normalized.length) out[fieldId] = normalized;
4774
5231
  }
@@ -4785,57 +5242,49 @@ function buildSelectedNodeVisitOrder(selection, fieldById) {
4785
5242
  out.push({ kind: "field", fieldId });
4786
5243
  }
4787
5244
  function pushOption(fieldId, optionId) {
4788
- const key = `option:${fieldId}::${optionId}`;
5245
+ const key = `option:${optionId}`;
4789
5246
  if (seen.has(key)) return;
4790
5247
  seen.add(key);
4791
5248
  out.push({ kind: "option", fieldId, optionId });
4792
5249
  }
4793
- for (const item of (_a = selection.optionTraversalOrder) != null ? _a : []) {
4794
- pushOption(item.fieldId, item.optionId);
5250
+ for (const optionId of (_a = selection.optionTraversalOrder) != null ? _a : []) {
5251
+ const ownerField = findOptionOwnerField(fieldById.values(), optionId);
5252
+ if (ownerField) pushOption(ownerField.id, optionId);
4795
5253
  }
4796
5254
  for (const rawKey of (_b = selection.selectedKeys) != null ? _b : []) {
4797
5255
  const key = String(rawKey);
4798
- if (key.includes("::")) {
4799
- const [fieldId, optionId] = key.split("::", 2);
4800
- if (fieldId && optionId) pushOption(fieldId, optionId);
4801
- continue;
4802
- }
4803
5256
  const field = fieldById.get(key);
4804
5257
  if (field) {
4805
5258
  pushField(field.id);
4806
5259
  continue;
4807
5260
  }
4808
- const ownerField = findOptionOwnerField(key, fieldById);
5261
+ const ownerField = findOptionOwnerField(fieldById.values(), key);
4809
5262
  if (ownerField) pushOption(ownerField.id, key);
4810
5263
  }
4811
5264
  for (const [fieldId, optionIds] of Object.entries(
4812
5265
  (_c = selection.optionSelectionsByFieldId) != null ? _c : {}
4813
5266
  )) {
4814
- if (!fieldById.has(fieldId)) continue;
5267
+ const hintedField = fieldById.get(fieldId);
5268
+ if (!hintedField) continue;
4815
5269
  for (const optionId of optionIds != null ? optionIds : []) {
4816
- pushOption(fieldId, optionId);
5270
+ const ownerField = findOptionOwnerField(fieldById.values(), optionId);
5271
+ if ((ownerField == null ? void 0 : ownerField.id) === hintedField.id) {
5272
+ pushOption(ownerField.id, optionId);
5273
+ }
4817
5274
  }
4818
5275
  }
4819
5276
  return out;
4820
5277
  }
4821
- function findOptionOwnerField(optionId, fieldById) {
4822
- var _a;
4823
- for (const field of fieldById.values()) {
4824
- if ((_a = field.options) == null ? void 0 : _a.some((option) => option.id === optionId)) return field;
4825
- }
4826
- return void 0;
4827
- }
4828
5278
 
4829
5279
  // src/utils/build-order-snapshot/services.ts
4830
5280
  function isServiceBased(field) {
4831
- var _a;
4832
5281
  if (field.service_id !== void 0 && field.service_id !== null) return true;
4833
- return !!((_a = field.options) == null ? void 0 : _a.some(
4834
- (item) => item.service_id !== void 0 && item.service_id !== null
4835
- ));
5282
+ return walkFieldOptions(field).some(
5283
+ ({ option }) => option.service_id !== void 0 && option.service_id !== null
5284
+ );
4836
5285
  }
4837
5286
  function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById, services) {
4838
- var _a, _b, _c, _d;
5287
+ var _a, _b, _c;
4839
5288
  const serviceMap = {};
4840
5289
  const visible = new Set(visibleFieldIds);
4841
5290
  const selectedBaseServices = [];
@@ -4862,9 +5311,9 @@ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById,
4862
5311
  }
4863
5312
  continue;
4864
5313
  }
4865
- const option = (_b = field.options) == null ? void 0 : _b.find((item) => item.id === visit.optionId);
5314
+ const option = findFieldOption(field, visit.optionId);
4866
5315
  if (!option) continue;
4867
- const role = (_d = (_c = option.pricing_role) != null ? _c : field.pricing_role) != null ? _d : "base";
5316
+ const role = (_c = (_b = option.pricing_role) != null ? _b : field.pricing_role) != null ? _c : "base";
4868
5317
  if (role === "utility") continue;
4869
5318
  if (option.service_id !== void 0 && option.service_id !== null) {
4870
5319
  addSelectedBaseService(option.id, option.service_id);
@@ -4995,16 +5444,15 @@ function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId,
4995
5444
  return { quantity: hostDefault, source: { kind: "default", defaultedFromHost: true } };
4996
5445
  }
4997
5446
  function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
4998
- var _a, _b, _c;
5447
+ var _a, _b;
4999
5448
  const visible = new Set(visibleFieldIds);
5000
5449
  const visits = buildSelectedNodeVisitOrder(selection, fieldById);
5001
5450
  for (const visit of visits) {
5002
5451
  if (visit.kind !== "option") continue;
5003
5452
  if (!visible.has(visit.fieldId)) continue;
5004
5453
  const field = fieldById.get(visit.fieldId);
5005
- if (!((_a = field == null ? void 0 : field.options) == null ? void 0 : _a.length)) continue;
5006
- const option = field.options.find((item) => item.id === visit.optionId);
5007
- const quantity = readPositiveFiniteNumber((_b = option == null ? void 0 : option.meta) == null ? void 0 : _b.quantityDefault);
5454
+ const option = findFieldOption(field, visit.optionId);
5455
+ const quantity = readPositiveFiniteNumber((_a = option == null ? void 0 : option.meta) == null ? void 0 : _a.quantityDefault);
5008
5456
  if (quantity !== void 0) {
5009
5457
  return { quantity, source: { kind: "option", id: option.id } };
5010
5458
  }
@@ -5019,7 +5467,7 @@ function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selecti
5019
5467
  }
5020
5468
  }
5021
5469
  const tag = tagById.get(tagId);
5022
- const tagQuantity = readPositiveFiniteNumber((_c = tag == null ? void 0 : tag.meta) == null ? void 0 : _c.quantityDefault);
5470
+ const tagQuantity = readPositiveFiniteNumber((_b = tag == null ? void 0 : tag.meta) == null ? void 0 : _b.quantityDefault);
5023
5471
  if (tagQuantity !== void 0) {
5024
5472
  return { quantity: tagQuantity, source: { kind: "tag", id: tagId } };
5025
5473
  }
@@ -5122,12 +5570,10 @@ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, selected
5122
5570
  const item = buildUtilityItemFromMarker(field.id, marker, quantity, value);
5123
5571
  if (item) items.push(item);
5124
5572
  }
5125
- if (Array.isArray(field.options) && field.options.length) {
5126
- const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5127
- if (!selectedOptionIds.length) continue;
5128
- const optById = new Map(field.options.map((o) => [o.id, o]));
5573
+ const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5574
+ if (selectedOptionIds.length) {
5129
5575
  for (const oid of selectedOptionIds) {
5130
- const option = optById.get(oid);
5576
+ const option = findFieldOption(field, oid);
5131
5577
  if (!option) continue;
5132
5578
  if (((_d = option.pricing_role) != null ? _d : "base") !== "utility") continue;
5133
5579
  const optionMarker = readUtilityMarker((_e = option.meta) == null ? void 0 : _e.utility);
@@ -5243,7 +5689,7 @@ function buildDevWarnings(props, svcMap, originalFallbacks, fieldById, visibleFi
5243
5689
 
5244
5690
  // src/utils/build-order-snapshot/index.ts
5245
5691
  function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
5246
- var _a, _b, _c, _d, _e, _f, _g, _h;
5692
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
5247
5693
  const mode = (_a = settings.mode) != null ? _a : "prod";
5248
5694
  const hostDefaultQty = Number.isFinite((_b = settings.hostDefaultQuantity) != null ? _b : 1) ? settings.hostDefaultQuantity : 1;
5249
5695
  const fbSettings = {
@@ -5256,14 +5702,31 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5256
5702
  const builtAt = (/* @__PURE__ */ new Date()).toISOString();
5257
5703
  const tagId = selection.activeTagId;
5258
5704
  const selectedButtonKeys = (_d = selection.selectedKeys) != null ? _d : toSelectedOptionKeys(selection.optionSelectionsByFieldId);
5259
- const visibleFieldIds = builder.visibleFields(tagId, selectedButtonKeys);
5260
5705
  const tagById = new Map(((_e = props.filters) != null ? _e : []).map((t) => [t.id, t]));
5261
5706
  const fieldById = new Map(((_f = props.fields) != null ? _f : []).map((f) => [f.id, f]));
5262
- const tagConstraints = (_h = (_g = tagById.get(tagId)) == null ? void 0 : _g.constraints) != null ? _h : void 0;
5707
+ const resolve = typeof builder.resolveVisibility === "function" ? builder.resolveVisibility.bind(builder) : void 0;
5708
+ let resolvedVisibility = resolve == null ? void 0 : resolve(tagId, selectedButtonKeys);
5709
+ let visibleFieldIds = (_g = resolvedVisibility == null ? void 0 : resolvedVisibility.fieldIds) != null ? _g : builder.visibleFields(tagId, selectedButtonKeys);
5710
+ const filteredSelectedButtonKeys = filterSelectedKeysByVisibility(
5711
+ selectedButtonKeys,
5712
+ visibleFieldIds,
5713
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId,
5714
+ fieldById
5715
+ );
5716
+ if (resolve && filteredSelectedButtonKeys.join("\0") !== selectedButtonKeys.join("\0")) {
5717
+ resolvedVisibility = resolve(tagId, filteredSelectedButtonKeys);
5718
+ visibleFieldIds = resolvedVisibility.fieldIds;
5719
+ }
5720
+ const effectiveSelection = {
5721
+ ...selection,
5722
+ selectedKeys: filteredSelectedButtonKeys
5723
+ };
5724
+ const tagConstraints = (_i = (_h = tagById.get(tagId)) == null ? void 0 : _h.constraints) != null ? _i : void 0;
5263
5725
  const selectedOptionsByFieldId = getSelectedOptionsByFieldId(
5264
- selection,
5726
+ effectiveSelection,
5265
5727
  fieldById,
5266
- mode
5728
+ mode,
5729
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId
5267
5730
  );
5268
5731
  const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
5269
5732
  var _a2;
@@ -5277,21 +5740,21 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5277
5740
  const { formValues, selections } = buildInputs(
5278
5741
  visibleFieldIds,
5279
5742
  fieldById,
5280
- selection,
5743
+ effectiveSelection,
5281
5744
  selectedOptionsByFieldId
5282
5745
  );
5283
5746
  const qtyRes = resolveQuantity(
5284
5747
  visibleFieldIds,
5285
5748
  fieldById,
5286
5749
  tagById,
5287
- selection,
5750
+ effectiveSelection,
5288
5751
  tagId,
5289
5752
  hostDefaultQty
5290
5753
  );
5291
5754
  const { serviceMap, servicesList } = resolveServices(
5292
5755
  tagId,
5293
5756
  visibleFieldIds,
5294
- selection,
5757
+ effectiveSelection,
5295
5758
  tagById,
5296
5759
  fieldById,
5297
5760
  services
@@ -5313,7 +5776,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5313
5776
  const utilities = collectUtilityLineItems(
5314
5777
  visibleFieldIds,
5315
5778
  fieldById,
5316
- selection,
5779
+ effectiveSelection,
5317
5780
  selectedOptionsByFieldId,
5318
5781
  qtyRes.quantity
5319
5782
  );
@@ -5323,7 +5786,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5323
5786
  prunedFallbacks.original,
5324
5787
  fieldById,
5325
5788
  visibleFieldIds,
5326
- selection
5789
+ effectiveSelection
5327
5790
  ) : void 0;
5328
5791
  const meta = {
5329
5792
  schema_version: props.schema_version,
@@ -5336,7 +5799,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5336
5799
  tagId,
5337
5800
  visibleFieldIds,
5338
5801
  fieldById,
5339
- selection,
5802
+ effectiveSelection,
5340
5803
  selectedOptionsByFieldId
5341
5804
  ),
5342
5805
  policy: toSnapshotPolicy(fbSettings)
@@ -5348,7 +5811,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5348
5811
  builtAt,
5349
5812
  selection: {
5350
5813
  tag: tagId,
5351
- buttons: selectedButtonKeys,
5814
+ buttons: filteredSelectedButtonKeys,
5352
5815
  fields: selectionFields
5353
5816
  },
5354
5817
  inputs: { form: formValues, selections },
@@ -5366,6 +5829,24 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5366
5829
  meta
5367
5830
  };
5368
5831
  }
5832
+ function filterSelectedKeysByVisibility(selectedKeys, visibleFieldIds, optionsByFieldId, fieldById) {
5833
+ if (!optionsByFieldId) return selectedKeys;
5834
+ const visibleFields = new Set(visibleFieldIds);
5835
+ const out = [];
5836
+ for (const rawKey of selectedKeys) {
5837
+ const key = String(rawKey);
5838
+ if (fieldById.has(key)) {
5839
+ if (visibleFields.has(key)) out.push(key);
5840
+ continue;
5841
+ }
5842
+ const owner = findOptionOwnerField(fieldById.values(), key);
5843
+ if (!owner || !visibleFields.has(owner.id)) continue;
5844
+ const allowed = optionsByFieldId[owner.id];
5845
+ if (allowed && !allowed.includes(key)) continue;
5846
+ out.push(key);
5847
+ }
5848
+ return out;
5849
+ }
5369
5850
 
5370
5851
  // src/core/fallback-editor.ts
5371
5852
  function createFallbackEditor(options = {}) {
@@ -5746,16 +6227,24 @@ export {
5746
6227
  createBuilder,
5747
6228
  createFallbackEditor,
5748
6229
  createNodeIndex,
6230
+ fieldOptionIdSet,
6231
+ fieldOptionIds,
6232
+ filterFieldOptionsById,
5749
6233
  filterServicesForVisibleGroup,
6234
+ findFieldOption,
6235
+ findOptionOwnerField,
5750
6236
  getAssignedServiceIds,
5751
6237
  getEligibleFallbacks,
5752
6238
  getFallbackRegistrationInfo,
5753
6239
  isRefExcludedBySelectedKeys,
5754
6240
  normalise,
5755
6241
  normalizeFieldValidation,
6242
+ optionOwnerMap,
5756
6243
  resolveServiceFallback,
5757
6244
  validate,
5758
6245
  validateAsync,
5759
- validateRateCoherenceDeep
6246
+ validateRateCoherenceDeep,
6247
+ walkFieldOptions,
6248
+ walkOptions
5760
6249
  };
5761
6250
  //# sourceMappingURL=index.js.map