@timeax/digital-service-engine 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,24 +5190,44 @@ 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) {
4749
- const out = {};
5205
+ function getSelectedOptionsByFieldId(selection, fieldById, mode, visibleOptionsByFieldId) {
5206
+ var _a;
5207
+ const collected = {};
4750
5208
  for (const visit of buildSelectedNodeVisitOrder(selection, fieldById)) {
4751
5209
  if (visit.kind !== "option") continue;
4752
- if (!out[visit.fieldId]) out[visit.fieldId] = [];
4753
- out[visit.fieldId].push(visit.optionId);
5210
+ if (!collected[visit.fieldId]) collected[visit.fieldId] = [];
5211
+ collected[visit.fieldId].push(visit.optionId);
5212
+ }
5213
+ const out = {};
5214
+ for (const [fieldId, optionIds] of Object.entries(collected)) {
5215
+ const field = fieldById.get(fieldId);
5216
+ if (!field) continue;
5217
+ const validOptionIds = fieldOptionIdSet(field);
5218
+ const visibleOptionIds = (visibleOptionsByFieldId == null ? void 0 : visibleOptionsByFieldId[fieldId]) ? new Set(visibleOptionsByFieldId[fieldId]) : void 0;
5219
+ const dedupedValid = [];
5220
+ const seen = /* @__PURE__ */ new Set();
5221
+ for (const optionId of optionIds) {
5222
+ if (!validOptionIds.has(optionId)) continue;
5223
+ if (visibleOptionIds && !visibleOptionIds.has(optionId)) continue;
5224
+ if (seen.has(optionId)) continue;
5225
+ seen.add(optionId);
5226
+ dedupedValid.push(optionId);
5227
+ }
5228
+ const isMulti = ((_a = field.meta) == null ? void 0 : _a.multi) === true;
5229
+ const normalized = mode === "prod" && !isMulti ? dedupedValid.length ? [dedupedValid[dedupedValid.length - 1]] : [] : dedupedValid;
5230
+ if (normalized.length) out[fieldId] = normalized;
4754
5231
  }
4755
5232
  return out;
4756
5233
  }
@@ -4765,57 +5242,49 @@ function buildSelectedNodeVisitOrder(selection, fieldById) {
4765
5242
  out.push({ kind: "field", fieldId });
4766
5243
  }
4767
5244
  function pushOption(fieldId, optionId) {
4768
- const key = `option:${fieldId}::${optionId}`;
5245
+ const key = `option:${optionId}`;
4769
5246
  if (seen.has(key)) return;
4770
5247
  seen.add(key);
4771
5248
  out.push({ kind: "option", fieldId, optionId });
4772
5249
  }
4773
- for (const item of (_a = selection.optionTraversalOrder) != null ? _a : []) {
4774
- 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);
4775
5253
  }
4776
5254
  for (const rawKey of (_b = selection.selectedKeys) != null ? _b : []) {
4777
5255
  const key = String(rawKey);
4778
- if (key.includes("::")) {
4779
- const [fieldId, optionId] = key.split("::", 2);
4780
- if (fieldId && optionId) pushOption(fieldId, optionId);
4781
- continue;
4782
- }
4783
5256
  const field = fieldById.get(key);
4784
5257
  if (field) {
4785
5258
  pushField(field.id);
4786
5259
  continue;
4787
5260
  }
4788
- const ownerField = findOptionOwnerField(key, fieldById);
5261
+ const ownerField = findOptionOwnerField(fieldById.values(), key);
4789
5262
  if (ownerField) pushOption(ownerField.id, key);
4790
5263
  }
4791
5264
  for (const [fieldId, optionIds] of Object.entries(
4792
5265
  (_c = selection.optionSelectionsByFieldId) != null ? _c : {}
4793
5266
  )) {
4794
- if (!fieldById.has(fieldId)) continue;
5267
+ const hintedField = fieldById.get(fieldId);
5268
+ if (!hintedField) continue;
4795
5269
  for (const optionId of optionIds != null ? optionIds : []) {
4796
- 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
+ }
4797
5274
  }
4798
5275
  }
4799
5276
  return out;
4800
5277
  }
4801
- function findOptionOwnerField(optionId, fieldById) {
4802
- var _a;
4803
- for (const field of fieldById.values()) {
4804
- if ((_a = field.options) == null ? void 0 : _a.some((option) => option.id === optionId)) return field;
4805
- }
4806
- return void 0;
4807
- }
4808
5278
 
4809
5279
  // src/utils/build-order-snapshot/services.ts
4810
5280
  function isServiceBased(field) {
4811
- var _a;
4812
5281
  if (field.service_id !== void 0 && field.service_id !== null) return true;
4813
- return !!((_a = field.options) == null ? void 0 : _a.some(
4814
- (item) => item.service_id !== void 0 && item.service_id !== null
4815
- ));
5282
+ return walkFieldOptions(field).some(
5283
+ ({ option }) => option.service_id !== void 0 && option.service_id !== null
5284
+ );
4816
5285
  }
4817
5286
  function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById, services) {
4818
- var _a, _b, _c, _d;
5287
+ var _a, _b, _c;
4819
5288
  const serviceMap = {};
4820
5289
  const visible = new Set(visibleFieldIds);
4821
5290
  const selectedBaseServices = [];
@@ -4842,9 +5311,9 @@ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById,
4842
5311
  }
4843
5312
  continue;
4844
5313
  }
4845
- const option = (_b = field.options) == null ? void 0 : _b.find((item) => item.id === visit.optionId);
5314
+ const option = findFieldOption(field, visit.optionId);
4846
5315
  if (!option) continue;
4847
- 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";
4848
5317
  if (role === "utility") continue;
4849
5318
  if (option.service_id !== void 0 && option.service_id !== null) {
4850
5319
  addSelectedBaseService(option.id, option.service_id);
@@ -4975,16 +5444,15 @@ function resolveQuantity(visibleFieldIds, fieldById, tagById, selection, tagId,
4975
5444
  return { quantity: hostDefault, source: { kind: "default", defaultedFromHost: true } };
4976
5445
  }
4977
5446
  function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selection, tagId) {
4978
- var _a, _b, _c;
5447
+ var _a, _b;
4979
5448
  const visible = new Set(visibleFieldIds);
4980
5449
  const visits = buildSelectedNodeVisitOrder(selection, fieldById);
4981
5450
  for (const visit of visits) {
4982
5451
  if (visit.kind !== "option") continue;
4983
5452
  if (!visible.has(visit.fieldId)) continue;
4984
5453
  const field = fieldById.get(visit.fieldId);
4985
- if (!((_a = field == null ? void 0 : field.options) == null ? void 0 : _a.length)) continue;
4986
- const option = field.options.find((item) => item.id === visit.optionId);
4987
- 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);
4988
5456
  if (quantity !== void 0) {
4989
5457
  return { quantity, source: { kind: "option", id: option.id } };
4990
5458
  }
@@ -4999,7 +5467,7 @@ function resolveNodeDefaultQuantity(visibleFieldIds, fieldById, tagById, selecti
4999
5467
  }
5000
5468
  }
5001
5469
  const tag = tagById.get(tagId);
5002
- 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);
5003
5471
  if (tagQuantity !== void 0) {
5004
5472
  return { quantity: tagQuantity, source: { kind: "tag", id: tagId } };
5005
5473
  }
@@ -5102,12 +5570,10 @@ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, selected
5102
5570
  const item = buildUtilityItemFromMarker(field.id, marker, quantity, value);
5103
5571
  if (item) items.push(item);
5104
5572
  }
5105
- if (Array.isArray(field.options) && field.options.length) {
5106
- const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5107
- if (!selectedOptionIds.length) continue;
5108
- const optById = new Map(field.options.map((o) => [o.id, o]));
5573
+ const selectedOptionIds = (_c = selectedOptionsByFieldId[field.id]) != null ? _c : [];
5574
+ if (selectedOptionIds.length) {
5109
5575
  for (const oid of selectedOptionIds) {
5110
- const option = optById.get(oid);
5576
+ const option = findFieldOption(field, oid);
5111
5577
  if (!option) continue;
5112
5578
  if (((_d = option.pricing_role) != null ? _d : "base") !== "utility") continue;
5113
5579
  const optionMarker = readUtilityMarker((_e = option.meta) == null ? void 0 : _e.utility);
@@ -5223,7 +5689,7 @@ function buildDevWarnings(props, svcMap, originalFallbacks, fieldById, visibleFi
5223
5689
 
5224
5690
  // src/utils/build-order-snapshot/index.ts
5225
5691
  function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
5226
- var _a, _b, _c, _d, _e, _f, _g, _h;
5692
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
5227
5693
  const mode = (_a = settings.mode) != null ? _a : "prod";
5228
5694
  const hostDefaultQty = Number.isFinite((_b = settings.hostDefaultQuantity) != null ? _b : 1) ? settings.hostDefaultQuantity : 1;
5229
5695
  const fbSettings = {
@@ -5236,11 +5702,32 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5236
5702
  const builtAt = (/* @__PURE__ */ new Date()).toISOString();
5237
5703
  const tagId = selection.activeTagId;
5238
5704
  const selectedButtonKeys = (_d = selection.selectedKeys) != null ? _d : toSelectedOptionKeys(selection.optionSelectionsByFieldId);
5239
- const visibleFieldIds = builder.visibleFields(tagId, selectedButtonKeys);
5240
5705
  const tagById = new Map(((_e = props.filters) != null ? _e : []).map((t) => [t.id, t]));
5241
5706
  const fieldById = new Map(((_f = props.fields) != null ? _f : []).map((f) => [f.id, f]));
5242
- const tagConstraints = (_h = (_g = tagById.get(tagId)) == null ? void 0 : _g.constraints) != null ? _h : void 0;
5243
- const selectedOptionsByFieldId = getSelectedOptionsByFieldId(selection, fieldById);
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;
5725
+ const selectedOptionsByFieldId = getSelectedOptionsByFieldId(
5726
+ effectiveSelection,
5727
+ fieldById,
5728
+ mode,
5729
+ resolvedVisibility == null ? void 0 : resolvedVisibility.optionsByFieldId
5730
+ );
5244
5731
  const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
5245
5732
  var _a2;
5246
5733
  const optionIds = isOptionBased(f) ? (_a2 = selectedOptionsByFieldId[f.id]) != null ? _a2 : [] : void 0;
@@ -5253,21 +5740,21 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5253
5740
  const { formValues, selections } = buildInputs(
5254
5741
  visibleFieldIds,
5255
5742
  fieldById,
5256
- selection,
5743
+ effectiveSelection,
5257
5744
  selectedOptionsByFieldId
5258
5745
  );
5259
5746
  const qtyRes = resolveQuantity(
5260
5747
  visibleFieldIds,
5261
5748
  fieldById,
5262
5749
  tagById,
5263
- selection,
5750
+ effectiveSelection,
5264
5751
  tagId,
5265
5752
  hostDefaultQty
5266
5753
  );
5267
5754
  const { serviceMap, servicesList } = resolveServices(
5268
5755
  tagId,
5269
5756
  visibleFieldIds,
5270
- selection,
5757
+ effectiveSelection,
5271
5758
  tagById,
5272
5759
  fieldById,
5273
5760
  services
@@ -5289,7 +5776,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5289
5776
  const utilities = collectUtilityLineItems(
5290
5777
  visibleFieldIds,
5291
5778
  fieldById,
5292
- selection,
5779
+ effectiveSelection,
5293
5780
  selectedOptionsByFieldId,
5294
5781
  qtyRes.quantity
5295
5782
  );
@@ -5299,7 +5786,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5299
5786
  prunedFallbacks.original,
5300
5787
  fieldById,
5301
5788
  visibleFieldIds,
5302
- selection
5789
+ effectiveSelection
5303
5790
  ) : void 0;
5304
5791
  const meta = {
5305
5792
  schema_version: props.schema_version,
@@ -5312,7 +5799,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5312
5799
  tagId,
5313
5800
  visibleFieldIds,
5314
5801
  fieldById,
5315
- selection,
5802
+ effectiveSelection,
5316
5803
  selectedOptionsByFieldId
5317
5804
  ),
5318
5805
  policy: toSnapshotPolicy(fbSettings)
@@ -5324,7 +5811,7 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5324
5811
  builtAt,
5325
5812
  selection: {
5326
5813
  tag: tagId,
5327
- buttons: selectedButtonKeys,
5814
+ buttons: filteredSelectedButtonKeys,
5328
5815
  fields: selectionFields
5329
5816
  },
5330
5817
  inputs: { form: formValues, selections },
@@ -5342,6 +5829,24 @@ function buildOrderSnapshot(props, builder, selection, services, settings = {})
5342
5829
  meta
5343
5830
  };
5344
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
+ }
5345
5850
 
5346
5851
  // src/core/fallback-editor.ts
5347
5852
  function createFallbackEditor(options = {}) {
@@ -5722,16 +6227,24 @@ export {
5722
6227
  createBuilder,
5723
6228
  createFallbackEditor,
5724
6229
  createNodeIndex,
6230
+ fieldOptionIdSet,
6231
+ fieldOptionIds,
6232
+ filterFieldOptionsById,
5725
6233
  filterServicesForVisibleGroup,
6234
+ findFieldOption,
6235
+ findOptionOwnerField,
5726
6236
  getAssignedServiceIds,
5727
6237
  getEligibleFallbacks,
5728
6238
  getFallbackRegistrationInfo,
5729
6239
  isRefExcludedBySelectedKeys,
5730
6240
  normalise,
5731
6241
  normalizeFieldValidation,
6242
+ optionOwnerMap,
5732
6243
  resolveServiceFallback,
5733
6244
  validate,
5734
6245
  validateAsync,
5735
- validateRateCoherenceDeep
6246
+ validateRateCoherenceDeep,
6247
+ walkFieldOptions,
6248
+ walkOptions
5736
6249
  };
5737
6250
  //# sourceMappingURL=index.js.map