@timeax/digital-service-engine 0.0.1 → 0.0.2

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.
@@ -280,11 +280,6 @@ function hasAnyServiceOption(f) {
280
280
  var _a;
281
281
  return ((_a = f.options) != null ? _a : []).some((o) => isFiniteNumber(o.service_id));
282
282
  }
283
- function isBoundTo(f, tagId) {
284
- const b = f.bind_id;
285
- if (!b) return false;
286
- return Array.isArray(b) ? b.includes(tagId) : b === tagId;
287
- }
288
283
  function getByPath(obj, path) {
289
284
  if (!path) return void 0;
290
285
  const parts = path.split(".");
@@ -370,33 +365,217 @@ function withAffected(details, ids) {
370
365
  return { ...details != null ? details : {}, affectedIds: ids };
371
366
  }
372
367
 
368
+ // src/core/node-map.ts
369
+ function buildNodeMap(props) {
370
+ var _a, _b, _c;
371
+ const map = /* @__PURE__ */ new Map();
372
+ for (const t of (_a = props.filters) != null ? _a : []) {
373
+ if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
374
+ }
375
+ for (const f of (_b = props.fields) != null ? _b : []) {
376
+ if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
377
+ for (const o of (_c = f.options) != null ? _c : []) {
378
+ if (!map.has(o.id))
379
+ map.set(o.id, {
380
+ kind: "option",
381
+ id: o.id,
382
+ node: o,
383
+ fieldId: f.id
384
+ });
385
+ }
386
+ }
387
+ return map;
388
+ }
389
+ function resolveTrigger(trigger, nodeMap) {
390
+ const idx = trigger.indexOf("::");
391
+ if (idx !== -1) {
392
+ const fieldId = trigger.slice(0, idx);
393
+ const optionId = trigger.slice(idx + 2);
394
+ return { kind: "composite", triggerKey: trigger, fieldId, optionId };
395
+ }
396
+ const direct = nodeMap.get(trigger);
397
+ if (!direct) return void 0;
398
+ if (direct.kind === "option") {
399
+ return {
400
+ kind: "option",
401
+ triggerKey: trigger,
402
+ id: direct.id,
403
+ fieldId: direct.fieldId
404
+ };
405
+ }
406
+ return { kind: direct.kind, triggerKey: trigger, id: direct.id };
407
+ }
408
+
409
+ // src/core/visibility.ts
410
+ function visibleFieldIdsUnder(props, tagId, opts = {}) {
411
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
412
+ const tags = (_a = props.filters) != null ? _a : [];
413
+ const fields = (_b = props.fields) != null ? _b : [];
414
+ const tagById = new Map(tags.map((t) => [t.id, t]));
415
+ const tag = tagById.get(tagId);
416
+ if (!tag) return [];
417
+ const nodeMap = buildNodeMap(props);
418
+ const lineageDepth = /* @__PURE__ */ new Map();
419
+ {
420
+ const guard = /* @__PURE__ */ new Set();
421
+ let cur = tag;
422
+ let d = 0;
423
+ while (cur && !guard.has(cur.id)) {
424
+ lineageDepth.set(cur.id, d++);
425
+ guard.add(cur.id);
426
+ const parentId = cur.bind_id;
427
+ cur = parentId ? tagById.get(parentId) : void 0;
428
+ }
429
+ }
430
+ const isTagInLineage = (id) => lineageDepth.has(id);
431
+ const fieldById = new Map(fields.map((f) => [f.id, f]));
432
+ const ownerDepthForField = (f) => {
433
+ const b = f.bind_id;
434
+ if (!b) return void 0;
435
+ if (typeof b === "string") return lineageDepth.get(b);
436
+ let best = void 0;
437
+ for (const id of b) {
438
+ const d = lineageDepth.get(id);
439
+ if (d == null) continue;
440
+ if (best == null || d < best) best = d;
441
+ }
442
+ return best;
443
+ };
444
+ const ownerDepthForTriggerKey = (triggerKey) => {
445
+ const t = resolveTrigger(triggerKey, nodeMap);
446
+ if (!t) return void 0;
447
+ if (t.kind === "composite") {
448
+ const f = fieldById.get(t.fieldId);
449
+ if (!f) return void 0;
450
+ return ownerDepthForField(f);
451
+ }
452
+ if (t.kind === "field") {
453
+ const f = fieldById.get(t.id);
454
+ if (!f || f.button !== true) return void 0;
455
+ return ownerDepthForField(f);
456
+ }
457
+ if (t.kind === "option") {
458
+ const f = t.fieldId ? fieldById.get(t.fieldId) : void 0;
459
+ if (!f) return void 0;
460
+ return ownerDepthForField(f);
461
+ }
462
+ return void 0;
463
+ };
464
+ const tagInclude = new Set((_c = tag.includes) != null ? _c : []);
465
+ const tagExclude = new Set((_d = tag.excludes) != null ? _d : []);
466
+ const selected = (_e = opts.selectedKeys) != null ? _e : /* @__PURE__ */ new Set();
467
+ const incMap = (_f = props.includes_for_buttons) != null ? _f : {};
468
+ const excMap = (_g = props.excludes_for_buttons) != null ? _g : {};
469
+ const relevantTriggersInOrder = [];
470
+ for (const key of selected) {
471
+ const d = ownerDepthForTriggerKey(key);
472
+ if (d == null) continue;
473
+ relevantTriggersInOrder.push(key);
474
+ }
475
+ const visible = /* @__PURE__ */ new Set();
476
+ const isBoundToLineage = (f) => {
477
+ const b = f.bind_id;
478
+ if (!b) return false;
479
+ if (typeof b === "string") return isTagInLineage(b);
480
+ for (const id of b) if (isTagInLineage(id)) return true;
481
+ return false;
482
+ };
483
+ for (const f of fields) {
484
+ if (isBoundToLineage(f)) visible.add(f.id);
485
+ if (tagInclude.has(f.id)) visible.add(f.id);
486
+ }
487
+ for (const id of tagExclude) visible.delete(id);
488
+ const decide = /* @__PURE__ */ new Map();
489
+ const applyDecision = (fieldId, next) => {
490
+ const prev = decide.get(fieldId);
491
+ if (!prev) return void decide.set(fieldId, next);
492
+ if (next.depth < prev.depth) return void decide.set(fieldId, next);
493
+ if (next.depth > prev.depth) return;
494
+ if (prev.kind === "include" && next.kind === "exclude") {
495
+ decide.set(fieldId, next);
496
+ }
497
+ };
498
+ const revealedOrder = [];
499
+ const revealedSeen = /* @__PURE__ */ new Set();
500
+ for (const triggerKey of relevantTriggersInOrder) {
501
+ const depth = ownerDepthForTriggerKey(triggerKey);
502
+ if (depth == null) continue;
503
+ for (const id of (_h = incMap[triggerKey]) != null ? _h : []) {
504
+ applyDecision(id, { depth, kind: "include" });
505
+ if (!revealedSeen.has(id)) {
506
+ revealedSeen.add(id);
507
+ revealedOrder.push(id);
508
+ }
509
+ }
510
+ for (const id of (_i = excMap[triggerKey]) != null ? _i : []) {
511
+ applyDecision(id, { depth, kind: "exclude" });
512
+ }
513
+ }
514
+ for (const [fid, d] of decide) {
515
+ if (d.kind === "include") visible.add(fid);
516
+ else visible.delete(fid);
517
+ }
518
+ const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
519
+ const order = (_j = props.order_for_tags) == null ? void 0 : _j[tagId];
520
+ if (order && order.length) {
521
+ const ordered = order.filter((fid) => visible.has(fid));
522
+ const orderedSet = new Set(ordered);
523
+ const rest2 = base.filter((fid) => !orderedSet.has(fid));
524
+ return [...ordered, ...rest2];
525
+ }
526
+ const promoted = revealedOrder.filter((fid) => visible.has(fid));
527
+ const promotedSet = new Set(promoted);
528
+ const rest = base.filter((fid) => !promotedSet.has(fid));
529
+ return [...promoted, ...rest];
530
+ }
531
+ function visibleFieldsUnder(props, tagId, opts = {}) {
532
+ var _a;
533
+ const ids = visibleFieldIdsUnder(props, tagId, opts);
534
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
535
+ return ids.map((id) => fieldById.get(id)).filter(Boolean);
536
+ }
537
+
373
538
  // src/core/validate/steps/visibility.ts
374
539
  function createFieldsVisibleUnder(v) {
375
540
  return (tagId) => {
376
- var _a, _b, _c, _d, _e, _f;
377
- const tag = v.tagById.get(tagId);
378
- const includesTag = new Set((_a = tag == null ? void 0 : tag.includes) != null ? _a : []);
379
- const excludesTag = new Set((_b = tag == null ? void 0 : tag.excludes) != null ? _b : []);
380
- const incForOpt = (_c = v.props.includes_for_buttons) != null ? _c : {};
381
- const excForOpt = (_d = v.props.excludes_for_buttons) != null ? _d : {};
382
- const includesOpt = /* @__PURE__ */ new Set();
383
- const excludesOpt = /* @__PURE__ */ new Set();
384
- for (const key of v.selectedKeys) {
385
- for (const id of (_e = incForOpt[key]) != null ? _e : []) includesOpt.add(id);
386
- for (const id of (_f = excForOpt[key]) != null ? _f : []) excludesOpt.add(id);
387
- }
388
- const merged = /* @__PURE__ */ new Map();
389
- for (const f of v.fields) {
390
- if (isBoundTo(f, tagId)) merged.set(f.id, f);
391
- if (includesTag.has(f.id)) merged.set(f.id, f);
392
- if (includesOpt.has(f.id)) merged.set(f.id, f);
393
- }
394
- for (const id of excludesTag) merged.delete(id);
395
- for (const id of excludesOpt) merged.delete(id);
396
- return Array.from(merged.values());
541
+ return visibleFieldsUnder(v.props, tagId, {
542
+ selectedKeys: v.selectedKeys
543
+ });
397
544
  };
398
545
  }
399
- function validateVisibility(v) {
546
+ function stableKeyOfSelection(keys) {
547
+ return Array.from(keys).sort().join("|");
548
+ }
549
+ function resolveRootTags(tags) {
550
+ const roots = tags.filter((t) => !t.bind_id);
551
+ return roots.length ? roots : tags.slice(0, 1);
552
+ }
553
+ function isEffectfulTrigger(v, trigger) {
554
+ var _a, _b, _c, _d, _e, _f;
555
+ const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
556
+ const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
557
+ return ((_d = (_c = inc[trigger]) == null ? void 0 : _c.length) != null ? _d : 0) > 0 || ((_f = (_e = exc[trigger]) == null ? void 0 : _e.length) != null ? _f : 0) > 0;
558
+ }
559
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
560
+ var _a;
561
+ const visible = visibleFieldsUnder(v.props, tagId, {
562
+ selectedKeys
563
+ });
564
+ const triggers = [];
565
+ for (const f of visible) {
566
+ if (f.button === true) {
567
+ const t = f.id;
568
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
569
+ }
570
+ for (const o of (_a = f.options) != null ? _a : []) {
571
+ const t = `${f.id}::${o.id}`;
572
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
573
+ }
574
+ }
575
+ triggers.sort();
576
+ return triggers;
577
+ }
578
+ function runVisibilityRulesOnce(v) {
400
579
  var _a, _b, _c, _d, _e;
401
580
  for (const t of v.tags) {
402
581
  const visible = v.fieldsVisibleUnder(t.id);
@@ -471,12 +650,85 @@ function validateVisibility(v) {
471
650
  }
472
651
  }
473
652
  }
653
+ function dedupeErrorsInPlace(v, startIndex) {
654
+ const seen = /* @__PURE__ */ new Set();
655
+ const keyOfErr = (e) => {
656
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
657
+ const tagId = (_e = (_d = (_a = e == null ? void 0 : e.details) == null ? void 0 : _a.tagId) != null ? _d : (_c = (_b = e == null ? void 0 : e.details) == null ? void 0 : _b.affected) == null ? void 0 : _c.tagId) != null ? _e : "";
658
+ const other = (_g = (_f = e == null ? void 0 : e.details) == null ? void 0 : _f.other) != null ? _g : "";
659
+ return `${(_h = e == null ? void 0 : e.code) != null ? _h : ""}::${(_i = e == null ? void 0 : e.nodeId) != null ? _i : ""}::${tagId}::${other}::${(_j = e == null ? void 0 : e.message) != null ? _j : ""}`;
660
+ };
661
+ const kept = [];
662
+ for (let i = startIndex; i < v.errors.length; i++) {
663
+ const e = v.errors[i];
664
+ const k = keyOfErr(e);
665
+ if (seen.has(k)) continue;
666
+ seen.add(k);
667
+ kept.push(e);
668
+ }
669
+ v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
670
+ }
671
+ function validateVisibility(v, options = {}) {
672
+ var _a, _b, _c;
673
+ const simulate = options.simulate === true;
674
+ if (!simulate) {
675
+ runVisibilityRulesOnce(v);
676
+ return;
677
+ }
678
+ const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
679
+ const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
680
+ const onlyEffectful = options.onlyEffectfulTriggers !== false;
681
+ const roots = resolveRootTags(v.tags);
682
+ const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
683
+ const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
684
+ const errorsStart = v.errors.length;
685
+ const visited = /* @__PURE__ */ new Set();
686
+ const stack = [];
687
+ for (const rt of rootTags) {
688
+ stack.push({
689
+ rootTagId: rt.id,
690
+ selected: new Set(originalSelected),
691
+ depth: 0
692
+ });
693
+ }
694
+ let validatedStates = 0;
695
+ while (stack.length) {
696
+ if (validatedStates >= maxStates) break;
697
+ const state = stack.pop();
698
+ const sig = stableKeyOfSelection(state.selected);
699
+ if (visited.has(sig)) continue;
700
+ visited.add(sig);
701
+ v.selectedKeys = state.selected;
702
+ validatedStates++;
703
+ runVisibilityRulesOnce(v);
704
+ if (state.depth >= maxDepth) continue;
705
+ const triggers = collectSelectableTriggersInContext(
706
+ v,
707
+ state.rootTagId,
708
+ state.selected,
709
+ onlyEffectful
710
+ );
711
+ for (let i = triggers.length - 1; i >= 0; i--) {
712
+ const trig = triggers[i];
713
+ if (state.selected.has(trig)) continue;
714
+ const next = new Set(state.selected);
715
+ next.add(trig);
716
+ stack.push({
717
+ rootTagId: state.rootTagId,
718
+ selected: next,
719
+ depth: state.depth + 1
720
+ });
721
+ }
722
+ }
723
+ v.selectedKeys = originalSelected;
724
+ dedupeErrorsInPlace(v, errorsStart);
725
+ }
474
726
 
475
727
  // src/core/validate/steps/structure.ts
476
728
  function validateStructure(v) {
477
729
  const tags = v.tags;
478
730
  const fields = v.fields;
479
- if (!tags.some((t) => t.id === "root")) {
731
+ if (!tags.some((t) => t.id === "t:root")) {
480
732
  v.errors.push({
481
733
  code: "root_missing",
482
734
  severity: "error",
@@ -666,58 +918,91 @@ function validateIdentity(v) {
666
918
  }
667
919
 
668
920
  // src/core/validate/steps/option-maps.ts
921
+ function parseFieldOptionKey(key) {
922
+ const idx = key.indexOf("::");
923
+ if (idx === -1) return null;
924
+ const fieldId = key.slice(0, idx).trim();
925
+ const optionId = key.slice(idx + 2).trim();
926
+ if (!fieldId || !optionId) return null;
927
+ return { fieldId, optionId };
928
+ }
929
+ function hasOption(v, fid, oid) {
930
+ var _a;
931
+ const f = v.fieldById.get(fid);
932
+ if (!f) return false;
933
+ return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
934
+ }
669
935
  function validateOptionMaps(v) {
670
936
  var _a, _b;
671
937
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
672
938
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
673
- const parseKey = (key) => {
674
- const parts = key.split("::");
675
- const fid = parts[0];
676
- const oid = parts[1];
677
- if (!fid || !oid) return null;
678
- return { fieldId: fid, optionId: oid };
679
- };
680
- const hasOption = (fid, oid) => {
681
- var _a2;
682
- const f = v.fieldById.get(fid);
683
- if (!f) return false;
684
- return !!((_a2 = f.options) != null ? _a2 : []).find((o) => o.id === oid);
939
+ 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.`;
940
+ const validateTriggerKey = (key) => {
941
+ const ref = v.nodeMap.get(key);
942
+ if (ref) {
943
+ if (ref.kind === "option") {
944
+ return {
945
+ ok: true,
946
+ nodeId: ref.fieldId,
947
+ affected: [ref.fieldId, ref.id]
948
+ };
949
+ }
950
+ if (ref.kind === "field") {
951
+ const isButton2 = ref.node.button === true;
952
+ if (!isButton2)
953
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
954
+ return { ok: true, nodeId: ref.id, affected: [ref.id] };
955
+ }
956
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
957
+ }
958
+ const p = parseFieldOptionKey(key);
959
+ if (!p) return { ok: false };
960
+ if (!hasOption(v, p.fieldId, p.optionId))
961
+ return {
962
+ ok: false,
963
+ nodeId: p.fieldId,
964
+ affected: [p.fieldId, p.optionId]
965
+ };
966
+ return {
967
+ ok: true,
968
+ nodeId: p.fieldId,
969
+ affected: [p.fieldId, p.optionId]
970
+ };
685
971
  };
686
- const badKeyMessage = (key) => `Invalid option-map key "${key}". Expected "fieldId::optionId" pointing to an existing option.`;
687
972
  for (const k of Object.keys(incMap)) {
688
- const p = parseKey(k);
689
- if (!p || !hasOption(p.fieldId, p.optionId)) {
973
+ const r = validateTriggerKey(k);
974
+ if (!r.ok) {
690
975
  v.errors.push({
691
976
  code: "bad_option_key",
692
977
  severity: "error",
693
978
  message: badKeyMessage(k),
694
- details: { key: k }
979
+ nodeId: r.nodeId,
980
+ details: withAffected({ key: k }, r.affected)
695
981
  });
696
982
  }
697
983
  }
698
984
  for (const k of Object.keys(excMap)) {
699
- const p = parseKey(k);
700
- if (!p || !hasOption(p.fieldId, p.optionId)) {
985
+ const r = validateTriggerKey(k);
986
+ if (!r.ok) {
701
987
  v.errors.push({
702
988
  code: "bad_option_key",
703
989
  severity: "error",
704
990
  message: badKeyMessage(k),
705
- details: { key: k }
991
+ nodeId: r.nodeId,
992
+ details: withAffected({ key: k }, r.affected)
706
993
  });
707
994
  }
708
995
  }
709
996
  for (const k of Object.keys(incMap)) {
710
- if (k in excMap) {
711
- const p = parseKey(k);
712
- const affected = p ? [p.fieldId, p.optionId] : void 0;
713
- v.errors.push({
714
- code: "option_include_exclude_conflict",
715
- severity: "error",
716
- message: `Option-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
717
- nodeId: p == null ? void 0 : p.fieldId,
718
- details: withAffected({ key: k }, affected)
719
- });
720
- }
997
+ if (!(k in excMap)) continue;
998
+ const r = validateTriggerKey(k);
999
+ v.errors.push({
1000
+ code: "option_include_exclude_conflict",
1001
+ severity: "error",
1002
+ message: `Trigger-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
1003
+ nodeId: r.nodeId,
1004
+ details: withAffected({ key: k }, r.affected)
1005
+ });
721
1006
  }
722
1007
  }
723
1008
 
@@ -1622,8 +1907,23 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
1622
1907
  }
1623
1908
 
1624
1909
  // src/core/validate/index.ts
1910
+ function readVisibilitySimOpts(ctx) {
1911
+ const c = ctx;
1912
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
1913
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
1914
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
1915
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
1916
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
1917
+ return {
1918
+ simulate,
1919
+ maxStates,
1920
+ maxDepth,
1921
+ simulateAllRoots,
1922
+ onlyEffectfulTriggers
1923
+ };
1924
+ }
1625
1925
  function validate(props, ctx = {}) {
1626
- var _a, _b;
1926
+ var _a, _b, _c;
1627
1927
  const errors = [];
1628
1928
  const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
1629
1929
  const selectedKeys = new Set(
@@ -1637,6 +1937,7 @@ function validate(props, ctx = {}) {
1637
1937
  for (const f of fields) fieldById.set(f.id, f);
1638
1938
  const v = {
1639
1939
  props,
1940
+ nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
1640
1941
  options: ctx,
1641
1942
  errors,
1642
1943
  serviceMap,
@@ -1651,7 +1952,8 @@ function validate(props, ctx = {}) {
1651
1952
  validateIdentity(v);
1652
1953
  validateOptionMaps(v);
1653
1954
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
1654
- validateVisibility(v);
1955
+ const visSim = readVisibilitySimOpts(ctx);
1956
+ validateVisibility(v, visSim);
1655
1957
  applyPolicies(
1656
1958
  v.errors,
1657
1959
  v.props,
@@ -1670,6 +1972,17 @@ function validate(props, ctx = {}) {
1670
1972
  validateFallbacks(v);
1671
1973
  return v.errors;
1672
1974
  }
1975
+ async function validateAsync(props, ctx = {}) {
1976
+ await Promise.resolve();
1977
+ if (typeof requestAnimationFrame === "function") {
1978
+ await new Promise(
1979
+ (resolve) => requestAnimationFrame(() => resolve())
1980
+ );
1981
+ } else {
1982
+ await new Promise((resolve) => setTimeout(resolve, 0));
1983
+ }
1984
+ return validate(props, ctx);
1985
+ }
1673
1986
 
1674
1987
  // src/core/builder.ts
1675
1988
  function createBuilder(opts = {}) {
@@ -1687,11 +2000,21 @@ var BuilderImpl = class {
1687
2000
  this.optionOwnerById = /* @__PURE__ */ new Map();
1688
2001
  this.history = [];
1689
2002
  this.future = [];
2003
+ this._nodemap = null;
1690
2004
  var _a;
1691
2005
  this.options = { ...opts };
1692
2006
  this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
1693
2007
  }
1694
2008
  /* ───── lifecycle ─────────────────────────────────────────────────────── */
2009
+ isTagId(id) {
2010
+ return this.tagById.has(id);
2011
+ }
2012
+ isFieldId(id) {
2013
+ return this.fieldById.has(id);
2014
+ }
2015
+ isOptionId(id) {
2016
+ return this.optionOwnerById.has(id);
2017
+ }
1695
2018
  load(raw) {
1696
2019
  const next = normalise(raw, {
1697
2020
  defaultPricingRole: "base",
@@ -1911,129 +2234,16 @@ var BuilderImpl = class {
1911
2234
  return validate(this.props, this.options);
1912
2235
  }
1913
2236
  visibleFields(tagId, selectedKeys) {
1914
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
1915
- const props = this.props;
1916
- const tags = (_a = props.filters) != null ? _a : [];
1917
- const fields = (_b = props.fields) != null ? _b : [];
1918
- const tagById = new Map(tags.map((t) => [t.id, t]));
1919
- const tag = tagById.get(tagId);
1920
- if (!tag) return [];
1921
- const lineageDepth = /* @__PURE__ */ new Map();
1922
- {
1923
- const guard = /* @__PURE__ */ new Set();
1924
- let cur = tag;
1925
- let d = 0;
1926
- while (cur && !guard.has(cur.id)) {
1927
- lineageDepth.set(cur.id, d++);
1928
- guard.add(cur.id);
1929
- const parentId = cur.bind_id;
1930
- cur = parentId ? tagById.get(parentId) : void 0;
1931
- }
1932
- }
1933
- const isTagInLineage = (id) => lineageDepth.has(id);
1934
- const fieldById = new Map(fields.map((f) => [f.id, f]));
1935
- const optionOwnerFieldId = /* @__PURE__ */ new Map();
1936
- for (const f of fields) {
1937
- for (const o of (_c = f.options) != null ? _c : []) optionOwnerFieldId.set(o.id, f.id);
1938
- }
1939
- const ownerDepthForField = (f) => {
1940
- const b = f.bind_id;
1941
- if (!b) return void 0;
1942
- if (typeof b === "string") return lineageDepth.get(b);
1943
- let best = void 0;
1944
- for (const id of b) {
1945
- const d = lineageDepth.get(id);
1946
- if (d == null) continue;
1947
- if (best == null || d < best) best = d;
1948
- }
1949
- return best;
1950
- };
1951
- const ownerDepthForTrigger = (triggerId) => {
1952
- if (triggerId.startsWith("o:")) {
1953
- const fid = optionOwnerFieldId.get(triggerId);
1954
- if (!fid) return void 0;
1955
- const f2 = fieldById.get(fid);
1956
- if (!f2) return void 0;
1957
- return ownerDepthForField(f2);
1958
- }
1959
- const f = fieldById.get(triggerId);
1960
- if (!f || f.button !== true) return void 0;
1961
- return ownerDepthForField(f);
1962
- };
1963
- const tagInclude = new Set((_d = tag.includes) != null ? _d : []);
1964
- const tagExclude = new Set((_e = tag.excludes) != null ? _e : []);
1965
- const selected = new Set(
1966
- (_f = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _f : []
1967
- );
1968
- const incMap = (_g = props.includes_for_buttons) != null ? _g : {};
1969
- const excMap = (_h = props.excludes_for_buttons) != null ? _h : {};
1970
- const relevantTriggersInOrder = [];
1971
- for (const key of selected) {
1972
- const d = ownerDepthForTrigger(key);
1973
- if (d == null) continue;
1974
- relevantTriggersInOrder.push(key);
1975
- }
1976
- const visible = /* @__PURE__ */ new Set();
1977
- const isBoundToLineage = (f) => {
1978
- const b = f.bind_id;
1979
- if (!b) return false;
1980
- if (typeof b === "string") return isTagInLineage(b);
1981
- for (const id of b) if (isTagInLineage(id)) return true;
1982
- return false;
1983
- };
1984
- for (const f of fields) {
1985
- if (isBoundToLineage(f)) visible.add(f.id);
1986
- if (tagInclude.has(f.id)) visible.add(f.id);
1987
- }
1988
- for (const id of tagExclude) visible.delete(id);
1989
- const decide = /* @__PURE__ */ new Map();
1990
- const applyDecision = (fieldId, next) => {
1991
- const prev = decide.get(fieldId);
1992
- if (!prev) {
1993
- decide.set(fieldId, next);
1994
- return;
1995
- }
1996
- if (next.depth < prev.depth) {
1997
- decide.set(fieldId, next);
1998
- return;
1999
- }
2000
- if (next.depth > prev.depth) return;
2001
- if (prev.kind === "include" && next.kind === "exclude") {
2002
- decide.set(fieldId, next);
2003
- }
2004
- };
2005
- const revealedOrder = [];
2006
- const revealedSeen = /* @__PURE__ */ new Set();
2007
- for (const triggerId of relevantTriggersInOrder) {
2008
- const depth = ownerDepthForTrigger(triggerId);
2009
- if (depth == null) continue;
2010
- for (const id of (_i = incMap[triggerId]) != null ? _i : []) {
2011
- applyDecision(id, { depth, kind: "include" });
2012
- if (!revealedSeen.has(id)) {
2013
- revealedSeen.add(id);
2014
- revealedOrder.push(id);
2015
- }
2016
- }
2017
- for (const id of (_j = excMap[triggerId]) != null ? _j : []) {
2018
- applyDecision(id, { depth, kind: "exclude" });
2019
- }
2020
- }
2021
- for (const [fid, d] of decide) {
2022
- if (d.kind === "include") visible.add(fid);
2023
- else visible.delete(fid);
2024
- }
2025
- const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
2026
- const order = (_k = props.order_for_tags) == null ? void 0 : _k[tagId];
2027
- if (order && order.length) {
2028
- const ordered = order.filter((fid) => visible.has(fid));
2029
- const orderedSet = new Set(ordered);
2030
- const rest2 = base.filter((fid) => !orderedSet.has(fid));
2031
- return [...ordered, ...rest2];
2032
- }
2033
- const promoted = revealedOrder.filter((fid) => visible.has(fid));
2034
- const promotedSet = new Set(promoted);
2035
- const rest = base.filter((fid) => !promotedSet.has(fid));
2036
- return [...promoted, ...rest];
2237
+ var _a;
2238
+ return visibleFieldIdsUnder(this.props, tagId, {
2239
+ selectedKeys: new Set(
2240
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
2241
+ )
2242
+ });
2243
+ }
2244
+ getNodeMap() {
2245
+ if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
2246
+ return this._nodemap;
2037
2247
  }
2038
2248
  /* ───── history ─────────────────────────────────────────────────────── */
2039
2249
  undo() {
@@ -2057,6 +2267,7 @@ var BuilderImpl = class {
2057
2267
  this.tagById.clear();
2058
2268
  this.fieldById.clear();
2059
2269
  this.optionOwnerById.clear();
2270
+ this._nodemap = null;
2060
2271
  for (const t of this.props.filters) this.tagById.set(t.id, t);
2061
2272
  for (const f of this.props.fields) {
2062
2273
  this.fieldById.set(f.id, f);
@@ -2538,6 +2749,7 @@ var toBindList = (b) => {
2538
2749
  function createNodeIndex(builder) {
2539
2750
  var _a, _b, _c, _d;
2540
2751
  const props = builder.getProps();
2752
+ const nodeMap = builder.getNodeMap();
2541
2753
  const tags = (_a = props.filters) != null ? _a : [];
2542
2754
  const fields = (_b = props.fields) != null ? _b : [];
2543
2755
  const tagById = new Map(tags.map((t) => [t.id, t]));
@@ -2741,7 +2953,9 @@ function createNodeIndex(builder) {
2741
2953
  return parentById.get(id) === tid;
2742
2954
  },
2743
2955
  getDescendant(descendantId) {
2744
- return this.getDescendants().find((item) => item.id == descendantId);
2956
+ return this.getDescendants().find(
2957
+ (item) => item.id == descendantId
2958
+ );
2745
2959
  },
2746
2960
  getDescendants() {
2747
2961
  const results = [];
@@ -2750,7 +2964,9 @@ function createNodeIndex(builder) {
2750
2964
  const node2 = getField(fieldId);
2751
2965
  if (!node2) continue;
2752
2966
  const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, id);
2753
- results.push(explicit ? node2 : { ...node2, isInherited: true });
2967
+ results.push(
2968
+ explicit ? node2 : { ...node2, isInherited: true }
2969
+ );
2754
2970
  }
2755
2971
  return Object.freeze(results);
2756
2972
  }
@@ -2799,7 +3015,9 @@ function createNodeIndex(builder) {
2799
3015
  return false;
2800
3016
  },
2801
3017
  getDescendant(descendantId, context) {
2802
- return this.getDescendants(context).find((item) => item.id == descendantId);
3018
+ return this.getDescendants(context).find(
3019
+ (item) => item.id == descendantId
3020
+ );
2803
3021
  },
2804
3022
  getDescendants(tagId) {
2805
3023
  return resolveDescendants(id, includes, tagId, !isButton2);
@@ -2841,7 +3059,9 @@ function createNodeIndex(builder) {
2841
3059
  return owner.isBoundTo(tagId);
2842
3060
  },
2843
3061
  getDescendant(descendantId, context) {
2844
- return this.getDescendants(context).find((item) => item.id == descendantId);
3062
+ return this.getDescendants(context).find(
3063
+ (item) => item.id == descendantId
3064
+ );
2845
3065
  },
2846
3066
  getDescendants(tagId) {
2847
3067
  return resolveDescendants(id, includes, tagId);
@@ -2852,7 +3072,7 @@ function createNodeIndex(builder) {
2852
3072
  return node;
2853
3073
  };
2854
3074
  const getNode = (input) => {
2855
- var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
3075
+ var _a2, _b2, _c2, _d2, _e;
2856
3076
  if (typeof input !== "string") {
2857
3077
  if ("bind_id" in input && !("type" in input))
2858
3078
  return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
@@ -2862,11 +3082,7 @@ function createNodeIndex(builder) {
2862
3082
  }
2863
3083
  const cached = nodeCache.get(input);
2864
3084
  if (cached) return cached;
2865
- const id = input;
2866
- if (id.startsWith("t:")) return (_d2 = getTag(id)) != null ? _d2 : mkUnknown(id);
2867
- if (id.startsWith("f:")) return (_e = getField(id)) != null ? _e : mkUnknown(id);
2868
- if (id.startsWith("o:")) return (_f = getOption(id)) != null ? _f : mkUnknown(id);
2869
- return (_i = (_h = (_g = getTag(id)) != null ? _g : getField(id)) != null ? _h : getOption(id)) != null ? _i : mkUnknown(id);
3085
+ return (_e = (_d2 = nodeMap.get(input)) == null ? void 0 : _d2.node) != null ? _e : mkUnknown(input);
2870
3086
  };
2871
3087
  const mkUnknown = (id) => {
2872
3088
  const u = {
@@ -2886,7 +3102,692 @@ function createNodeIndex(builder) {
2886
3102
  getOption
2887
3103
  };
2888
3104
  }
3105
+
3106
+ // src/utils/prune-fallbacks.ts
3107
+ function pruneInvalidNodeFallbacks(props, services, settings) {
3108
+ var _a, _b;
3109
+ const fb = props.fallbacks;
3110
+ if (!(fb == null ? void 0 : fb.nodes) || Object.keys(fb.nodes).length === 0) {
3111
+ return { props, removed: [] };
3112
+ }
3113
+ const nodeContexts = /* @__PURE__ */ new Map();
3114
+ const nodePrimary = /* @__PURE__ */ new Map();
3115
+ for (const nodeId of Object.keys(fb.nodes)) {
3116
+ const tag = props.filters.find((t) => t.id === nodeId);
3117
+ if (tag) {
3118
+ nodeContexts.set(nodeId, [tag.id]);
3119
+ nodePrimary.set(nodeId, tag.service_id);
3120
+ continue;
3121
+ }
3122
+ const field = props.fields.find((f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId));
3123
+ if (field) {
3124
+ const contexts = toBindArray(field.bind_id);
3125
+ nodeContexts.set(nodeId, contexts);
3126
+ const opt = field.options.find((o) => o.id === nodeId);
3127
+ nodePrimary.set(nodeId, opt.service_id);
3128
+ continue;
3129
+ }
3130
+ nodeContexts.set(nodeId, []);
3131
+ nodePrimary.set(nodeId, void 0);
3132
+ }
3133
+ const diags = collectFailedFallbacks(props, services, { ...settings, mode: "dev" });
3134
+ const failuresByPair = /* @__PURE__ */ new Map();
3135
+ const totalContextsByNode = /* @__PURE__ */ new Map();
3136
+ for (const [nodeId, ctxs] of nodeContexts.entries()) {
3137
+ totalContextsByNode.set(nodeId, Math.max(1, ctxs.length));
3138
+ }
3139
+ for (const d of diags) {
3140
+ if (d.scope !== "node") continue;
3141
+ const key = `${d.nodeId}::${String(d.candidate)}`;
3142
+ let rec = failuresByPair.get(key);
3143
+ if (!rec) {
3144
+ rec = { reasons: /* @__PURE__ */ new Set(), contexts: /* @__PURE__ */ new Set() };
3145
+ failuresByPair.set(key, rec);
3146
+ }
3147
+ rec.reasons.add(d.reason);
3148
+ if (d.tagContext) rec.contexts.add(d.tagContext);
3149
+ }
3150
+ const prunedNodes = {};
3151
+ const removed = [];
3152
+ for (const [nodeId, list] of Object.entries(fb.nodes)) {
3153
+ const contexts = (_a = nodeContexts.get(nodeId)) != null ? _a : [];
3154
+ const totalContexts = Math.max(1, contexts.length);
3155
+ const keep = [];
3156
+ for (const cand of list) {
3157
+ const key = `${nodeId}::${String(cand)}`;
3158
+ const rec = failuresByPair.get(key);
3159
+ if (!rec) {
3160
+ keep.push(cand);
3161
+ continue;
3162
+ }
3163
+ const failedContextsCount = rec.contexts.size > 0 ? rec.contexts.size : totalContexts;
3164
+ const failsAll = failedContextsCount >= totalContexts;
3165
+ if (failsAll) {
3166
+ removed.push({
3167
+ nodeId,
3168
+ candidate: cand,
3169
+ reasons: Array.from(rec.reasons),
3170
+ contexts: contexts.length ? contexts.slice() : void 0
3171
+ });
3172
+ } else {
3173
+ keep.push(cand);
3174
+ }
3175
+ }
3176
+ if (keep.length) prunedNodes[nodeId] = keep;
3177
+ }
3178
+ const outProps = {
3179
+ ...props,
3180
+ fallbacks: {
3181
+ ...((_b = props.fallbacks) == null ? void 0 : _b.global) ? { global: props.fallbacks.global } : {},
3182
+ ...Object.keys(prunedNodes).length ? { nodes: prunedNodes } : {}
3183
+ }
3184
+ };
3185
+ return { props: outProps, removed };
3186
+ }
3187
+ function toBindArray(bind) {
3188
+ if (!bind) return [];
3189
+ return Array.isArray(bind) ? bind.slice() : [bind];
3190
+ }
3191
+
3192
+ // src/utils/util.ts
3193
+ function toFiniteNumber(v) {
3194
+ const n = Number(v);
3195
+ return Number.isFinite(n) ? n : NaN;
3196
+ }
3197
+ function constraintFitOk(svcMap, candidate, constraints) {
3198
+ const cap = svcMap[Number(candidate)];
3199
+ if (!cap) return false;
3200
+ if (constraints.dripfeed === true && !cap.dripfeed) return false;
3201
+ if (constraints.refill === true && !cap.refill) return false;
3202
+ return !(constraints.cancel === true && !cap.cancel);
3203
+ }
3204
+ function rateOk(svcMap, candidate, primary, policy) {
3205
+ var _a, _b, _c;
3206
+ const cand = svcMap[Number(candidate)];
3207
+ const prim = svcMap[Number(primary)];
3208
+ if (!cand || !prim) return false;
3209
+ const cRate = toFiniteNumber(cand.rate);
3210
+ const pRate = toFiniteNumber(prim.rate);
3211
+ if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
3212
+ const rp = (_a = policy.ratePolicy) != null ? _a : { kind: "lte_primary" };
3213
+ switch (rp.kind) {
3214
+ case "lte_primary":
3215
+ return cRate <= pRate;
3216
+ case "within_pct": {
3217
+ const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
3218
+ return cRate <= pRate * (1 + pct / 100);
3219
+ }
3220
+ case "at_least_pct_lower": {
3221
+ const pct = Math.max(0, (_c = rp.pct) != null ? _c : 0);
3222
+ return cRate <= pRate * (1 - pct / 100);
3223
+ }
3224
+ default:
3225
+ return false;
3226
+ }
3227
+ }
3228
+
3229
+ // src/utils/build-order-snapshot.ts
3230
+ function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
3231
+ var _a, _b, _c, _d, _e, _f, _g;
3232
+ const mode = (_a = settings.mode) != null ? _a : "prod";
3233
+ const hostDefaultQty = Number.isFinite(
3234
+ (_b = settings.hostDefaultQuantity) != null ? _b : 1
3235
+ ) ? settings.hostDefaultQuantity : 1;
3236
+ const fbSettings = {
3237
+ requireConstraintFit: true,
3238
+ ratePolicy: { kind: "lte_primary" },
3239
+ selectionStrategy: "priority",
3240
+ mode: mode === "dev" ? "dev" : "strict",
3241
+ ...(_c = settings.fallback) != null ? _c : {}
3242
+ };
3243
+ const builtAt = (/* @__PURE__ */ new Date()).toISOString();
3244
+ const tagId = selection.activeTagId;
3245
+ const selectedOptionKeys = toSelectedOptionKeys(
3246
+ selection.optionSelectionsByFieldId
3247
+ );
3248
+ const visibleFieldIds = builder.visibleFields(
3249
+ tagId,
3250
+ selectedOptionKeys
3251
+ );
3252
+ const tagById = new Map(
3253
+ ((_d = props.filters) != null ? _d : []).map((t) => [t.id, t])
3254
+ );
3255
+ const fieldById = new Map(
3256
+ ((_e = props.fields) != null ? _e : []).map((f) => [f.id, f])
3257
+ );
3258
+ const tagConstraints = (_g = (_f = tagById.get(tagId)) == null ? void 0 : _f.constraints) != null ? _g : void 0;
3259
+ const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
3260
+ var _a2;
3261
+ const optIds = isOptionBased(f) ? (_a2 = selection.optionSelectionsByFieldId[f.id]) != null ? _a2 : [] : void 0;
3262
+ return {
3263
+ id: f.id,
3264
+ type: String(f.type),
3265
+ ...optIds && optIds.length ? { selectedOptions: optIds } : {}
3266
+ };
3267
+ });
3268
+ const { formValues, selections } = buildInputs(
3269
+ visibleFieldIds,
3270
+ fieldById,
3271
+ selection
3272
+ );
3273
+ const qtyRes = resolveQuantity(
3274
+ visibleFieldIds,
3275
+ fieldById,
3276
+ selection,
3277
+ hostDefaultQty
3278
+ );
3279
+ const quantity = qtyRes.quantity;
3280
+ const quantitySource = qtyRes.source;
3281
+ const { serviceMap, servicesList } = resolveServices(
3282
+ tagId,
3283
+ visibleFieldIds,
3284
+ selection,
3285
+ tagById,
3286
+ fieldById
3287
+ );
3288
+ const { min, max } = resolveMinMax(servicesList, services);
3289
+ const prunedFallbacks = pruneFallbacksConservative(
3290
+ props.fallbacks,
3291
+ { tagId, constraints: tagConstraints, serviceMap, servicesList },
3292
+ services,
3293
+ fbSettings
3294
+ );
3295
+ const utilities = collectUtilityLineItems(
3296
+ visibleFieldIds,
3297
+ fieldById,
3298
+ selection,
3299
+ quantity
3300
+ );
3301
+ const warnings = mode === "dev" ? buildDevWarnings(
3302
+ props,
3303
+ services,
3304
+ tagId,
3305
+ serviceMap,
3306
+ prunedFallbacks.original,
3307
+ prunedFallbacks.pruned,
3308
+ fieldById,
3309
+ visibleFieldIds,
3310
+ selection
3311
+ ) : void 0;
3312
+ const snapshotPolicy = toSnapshotPolicy(fbSettings);
3313
+ const meta = {
3314
+ schema_version: props.schema_version,
3315
+ workspaceId: settings.workspaceId,
3316
+ builder: settings.builderCommit ? { commit: settings.builderCommit } : void 0,
3317
+ context: {
3318
+ tag: tagId,
3319
+ constraints: tagConstraints != null ? tagConstraints : {},
3320
+ nodeContexts: buildNodeContexts(
3321
+ tagId,
3322
+ visibleFieldIds,
3323
+ fieldById,
3324
+ selection
3325
+ ),
3326
+ policy: snapshotPolicy
3327
+ }
3328
+ };
3329
+ const snapshot = {
3330
+ version: "1",
3331
+ mode,
3332
+ builtAt,
3333
+ selection: {
3334
+ tag: tagId,
3335
+ fields: selectionFields
3336
+ },
3337
+ inputs: {
3338
+ form: formValues,
3339
+ selections
3340
+ },
3341
+ min,
3342
+ max: max != null ? max : min,
3343
+ quantity,
3344
+ quantitySource,
3345
+ services: servicesList,
3346
+ serviceMap,
3347
+ ...prunedFallbacks.pruned ? { fallbacks: prunedFallbacks.pruned } : {},
3348
+ ...utilities.length ? { utilities } : {},
3349
+ ...warnings ? { warnings } : {},
3350
+ meta
3351
+ };
3352
+ return snapshot;
3353
+ }
3354
+ function isOptionBased(f) {
3355
+ const hasOptions = Array.isArray(f.options) && f.options.length > 0;
3356
+ return hasOptions || isMultiField(f);
3357
+ }
3358
+ function toSelectedOptionKeys(byField) {
3359
+ const keys = [];
3360
+ for (const [fieldId, optionIds] of Object.entries(byField != null ? byField : {})) {
3361
+ for (const optId of optionIds != null ? optionIds : []) {
3362
+ keys.push(`${fieldId}::${optId}`);
3363
+ }
3364
+ }
3365
+ return keys;
3366
+ }
3367
+ function buildInputs(visibleFieldIds, fieldById, selection) {
3368
+ const formValues = {};
3369
+ const selections = {};
3370
+ for (const fid of visibleFieldIds) {
3371
+ const f = fieldById.get(fid);
3372
+ if (!f) continue;
3373
+ const selOptIds = selection.optionSelectionsByFieldId[fid];
3374
+ if (selOptIds && selOptIds.length) {
3375
+ selections[fid] = [...selOptIds];
3376
+ }
3377
+ if (!isOptionBased(f)) {
3378
+ const name = f.name;
3379
+ const val = selection.formValuesByFieldId[fid];
3380
+ if (!name || val === void 0) continue;
3381
+ formValues[name] = val;
3382
+ }
3383
+ }
3384
+ return { formValues, selections };
3385
+ }
3386
+ function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
3387
+ var _a;
3388
+ for (const fid of visibleFieldIds) {
3389
+ const f = fieldById.get(fid);
3390
+ if (!f) continue;
3391
+ const rule = readQuantityRule(
3392
+ (_a = f.meta) == null ? void 0 : _a.quantity
3393
+ );
3394
+ if (!rule) continue;
3395
+ const raw = selection.formValuesByFieldId[fid];
3396
+ const evaluated = evaluateQuantityRule(rule, raw);
3397
+ if (Number.isFinite(evaluated) && evaluated > 0) {
3398
+ return {
3399
+ quantity: evaluated,
3400
+ source: { kind: "field", id: f.id, rule }
3401
+ };
3402
+ }
3403
+ }
3404
+ return {
3405
+ quantity: hostDefault,
3406
+ source: { kind: "default", defaultedFromHost: true }
3407
+ };
3408
+ }
3409
+ function readQuantityRule(v) {
3410
+ if (!v || typeof v !== "object") return void 0;
3411
+ const src = v;
3412
+ if (src.valueBy !== "value" && src.valueBy !== "length" && src.valueBy !== "eval")
3413
+ return void 0;
3414
+ const out = { valueBy: src.valueBy };
3415
+ if (src.code && typeof src.code === "string") out.code = src.code;
3416
+ return out;
3417
+ }
3418
+ function evaluateQuantityRule(rule, raw) {
3419
+ switch (rule.valueBy) {
3420
+ case "value": {
3421
+ const n = Number(Array.isArray(raw) ? raw[0] : raw);
3422
+ return Number.isFinite(n) ? n : NaN;
3423
+ }
3424
+ case "length": {
3425
+ if (Array.isArray(raw)) return raw.length;
3426
+ if (typeof raw === "string") return raw.length;
3427
+ return NaN;
3428
+ }
3429
+ case "eval": {
3430
+ try {
3431
+ if (!rule.code || typeof rule.code !== "string") return NaN;
3432
+ const fn = new Function(
3433
+ "value",
3434
+ "values",
3435
+ `return (function(){ ${rule.code}
3436
+ })()`
3437
+ );
3438
+ const single = Array.isArray(raw) ? raw[0] : raw;
3439
+ const values = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
3440
+ const out = fn(single, values);
3441
+ const n = Number(out);
3442
+ return Number.isFinite(n) ? n : NaN;
3443
+ } catch {
3444
+ return NaN;
3445
+ }
3446
+ }
3447
+ default:
3448
+ return NaN;
3449
+ }
3450
+ }
3451
+ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById) {
3452
+ var _a;
3453
+ const serviceMap = {};
3454
+ const ordered = [];
3455
+ const tag = tagById.get(tagId);
3456
+ let primary;
3457
+ let primaryOrigin;
3458
+ if ((tag == null ? void 0 : tag.service_id) !== void 0) {
3459
+ primary = tag.service_id;
3460
+ primaryOrigin = "tag";
3461
+ }
3462
+ const optionVisit = buildOptionVisitOrder(selection, fieldById);
3463
+ for (const { fieldId, optionId } of optionVisit) {
3464
+ if (!visibleFieldIds.includes(fieldId)) continue;
3465
+ const f = fieldById.get(fieldId);
3466
+ if (!f || !Array.isArray(f.options)) continue;
3467
+ const opt = f.options.find((o) => o.id === optionId);
3468
+ if (!opt) continue;
3469
+ const role = (_a = opt.pricing_role) != null ? _a : "base";
3470
+ const sid = opt.service_id;
3471
+ if (role === "utility") continue;
3472
+ if (sid !== void 0) {
3473
+ if (primary === void 0 || primaryOrigin === "tag") {
3474
+ primary = sid;
3475
+ primaryOrigin = "option";
3476
+ ordered.length = 0;
3477
+ ordered.push(primary);
3478
+ } else {
3479
+ ordered.push(sid);
3480
+ }
3481
+ pushService(serviceMap, optionId, sid);
3482
+ }
3483
+ }
3484
+ if (primaryOrigin !== "option" && primary !== void 0) {
3485
+ ordered.unshift(primary);
3486
+ pushService(serviceMap, tagId, primary);
3487
+ } else {
3488
+ }
3489
+ const servicesList = dedupeByString(ordered);
3490
+ return { serviceMap, servicesList };
3491
+ }
3492
+ function buildOptionVisitOrder(selection, fieldById) {
3493
+ var _a;
3494
+ if (selection.optionTraversalOrder && selection.optionTraversalOrder.length) {
3495
+ return selection.optionTraversalOrder.slice();
3496
+ }
3497
+ const out = [];
3498
+ for (const [fid, optIds] of Object.entries(
3499
+ (_a = selection.optionSelectionsByFieldId) != null ? _a : {}
3500
+ )) {
3501
+ const f = fieldById.get(fid);
3502
+ if (!f) continue;
3503
+ for (const oid of optIds != null ? optIds : [])
3504
+ out.push({ fieldId: fid, optionId: oid });
3505
+ }
3506
+ return out;
3507
+ }
3508
+ function pushService(map, nodeId, sid) {
3509
+ if (!map[nodeId]) map[nodeId] = [];
3510
+ map[nodeId].push(sid);
3511
+ }
3512
+ function dedupeByString(arr) {
3513
+ const s = /* @__PURE__ */ new Set();
3514
+ const out = [];
3515
+ for (const v of arr) {
3516
+ const key = String(v);
3517
+ if (s.has(key)) continue;
3518
+ s.add(key);
3519
+ out.push(v);
3520
+ }
3521
+ return out;
3522
+ }
3523
+ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
3524
+ var _a, _b;
3525
+ if (!fallbacks) return { pruned: void 0, original: void 0 };
3526
+ try {
3527
+ const { props: prunedProps } = pruneInvalidNodeFallbacks(
3528
+ {
3529
+ filters: [],
3530
+ fields: [],
3531
+ schema_version: "1.0",
3532
+ fallbacks
3533
+ },
3534
+ svcMap,
3535
+ policy
3536
+ );
3537
+ return {
3538
+ pruned: prunedProps.fallbacks,
3539
+ original: fallbacks
3540
+ };
3541
+ } catch {
3542
+ const out = {};
3543
+ const requireFit = (_a = policy.requireConstraintFit) != null ? _a : true;
3544
+ if (fallbacks.nodes) {
3545
+ const keptNodes = {};
3546
+ for (const [nodeId, candidates] of Object.entries(
3547
+ fallbacks.nodes
3548
+ )) {
3549
+ if (!env.serviceMap[nodeId]) continue;
3550
+ const primary = ((_b = env.serviceMap[nodeId]) != null ? _b : [])[0];
3551
+ const kept = [];
3552
+ for (const cand of candidates != null ? candidates : []) {
3553
+ if (!rateOk(svcMap, cand, primary, policy)) continue;
3554
+ if (requireFit && env.constraints && !constraintFitOk(svcMap, cand, env.constraints))
3555
+ continue;
3556
+ kept.push(cand);
3557
+ }
3558
+ if (kept.length) keptNodes[nodeId] = kept;
3559
+ }
3560
+ if (Object.keys(keptNodes).length) out.nodes = keptNodes;
3561
+ }
3562
+ if (fallbacks.global) {
3563
+ const keptGlobal = {};
3564
+ const present = new Set(env.servicesList.map((sid) => String(sid)));
3565
+ for (const [primary, cands] of Object.entries(
3566
+ fallbacks.global
3567
+ )) {
3568
+ if (!present.has(String(primary))) continue;
3569
+ const primId = isFiniteNumber3(primary) ? Number(primary) : primary;
3570
+ const kept = [];
3571
+ for (const cand of cands != null ? cands : []) {
3572
+ if (!rateOk(svcMap, cand, primId, policy)) continue;
3573
+ if (requireFit && env.constraints && !constraintFitOk(svcMap, cand, env.constraints))
3574
+ continue;
3575
+ kept.push(cand);
3576
+ }
3577
+ if (kept.length) keptGlobal[primId] = kept;
3578
+ }
3579
+ if (Object.keys(keptGlobal).length)
3580
+ out.global = keptGlobal;
3581
+ }
3582
+ return {
3583
+ pruned: Object.keys(out).length ? out : void 0,
3584
+ original: fallbacks
3585
+ };
3586
+ }
3587
+ }
3588
+ function isFiniteNumber3(v) {
3589
+ return typeof v === "number" && Number.isFinite(v);
3590
+ }
3591
+ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, quantity) {
3592
+ var _a, _b, _c, _d, _e;
3593
+ const items = [];
3594
+ for (const fid of visibleFieldIds) {
3595
+ const f = fieldById.get(fid);
3596
+ if (!f) continue;
3597
+ const isUtilityField = ((_a = f.pricing_role) != null ? _a : "base") === "utility";
3598
+ const marker = readUtilityMarker((_b = f.meta) == null ? void 0 : _b.utility);
3599
+ if (isUtilityField && marker) {
3600
+ const val = selection.formValuesByFieldId[f.id];
3601
+ const item = buildUtilityItemFromMarker(
3602
+ f.id,
3603
+ marker,
3604
+ quantity,
3605
+ val
3606
+ );
3607
+ if (item) items.push(item);
3608
+ }
3609
+ if (Array.isArray(f.options) && f.options.length) {
3610
+ const selectedOptIds = (_c = selection.optionSelectionsByFieldId[f.id]) != null ? _c : [];
3611
+ if (selectedOptIds.length) {
3612
+ const optById = new Map(
3613
+ f.options.map((o) => [o.id, o])
3614
+ );
3615
+ for (const oid of selectedOptIds) {
3616
+ const opt = optById.get(oid);
3617
+ if (!opt) continue;
3618
+ if (((_d = opt.pricing_role) != null ? _d : "base") !== "utility") continue;
3619
+ const om = readUtilityMarker((_e = opt.meta) == null ? void 0 : _e.utility);
3620
+ if (!om) continue;
3621
+ const parentVal = selection.formValuesByFieldId[f.id];
3622
+ const item = buildUtilityItemFromMarker(
3623
+ opt.id,
3624
+ om,
3625
+ quantity,
3626
+ parentVal
3627
+ );
3628
+ if (item) items.push(item);
3629
+ }
3630
+ }
3631
+ }
3632
+ }
3633
+ return items;
3634
+ }
3635
+ function readUtilityMarker(v) {
3636
+ if (!v || typeof v !== "object") return void 0;
3637
+ const src = v;
3638
+ if (!src.mode || typeof src.rate !== "number" || !Number.isFinite(src.rate))
3639
+ return void 0;
3640
+ if (src.mode !== "flat" && src.mode !== "per_quantity" && src.mode !== "per_value" && src.mode !== "percent")
3641
+ return void 0;
3642
+ const out = { mode: src.mode, rate: src.rate };
3643
+ if (src.valueBy === "value" || src.valueBy === "length" || src.valueBy === "eval")
3644
+ out.valueBy = src.valueBy;
3645
+ if (src.code && typeof src.code === "string") out.code = src.code;
3646
+ return out;
3647
+ }
3648
+ function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
3649
+ var _a, _b;
3650
+ const base = {
3651
+ nodeId,
3652
+ mode: marker.mode,
3653
+ rate: marker.rate,
3654
+ inputs: { quantity }
3655
+ };
3656
+ if (marker.mode === "per_value") {
3657
+ base.inputs.valueBy = (_a = marker.valueBy) != null ? _a : "value";
3658
+ if (marker.valueBy === "length") {
3659
+ base.inputs.value = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : 0;
3660
+ } else if (marker.valueBy === "eval") {
3661
+ base.inputs.evalCodeUsed = true;
3662
+ } else {
3663
+ base.inputs.value = Array.isArray(value) ? (_b = value[0]) != null ? _b : null : value != null ? value : null;
3664
+ }
3665
+ }
3666
+ return base;
3667
+ }
3668
+ function buildNodeContexts(tagId, visibleFieldIds, fieldById, selection) {
3669
+ var _a;
3670
+ const ctx = {};
3671
+ ctx[tagId] = tagId;
3672
+ for (const fid of visibleFieldIds) {
3673
+ const f = fieldById.get(fid);
3674
+ if (!f) continue;
3675
+ const binds = normalizeBindIds(f.bind_id);
3676
+ const applicable = binds.has(tagId);
3677
+ const selectedOptIds = (_a = selection.optionSelectionsByFieldId[fid]) != null ? _a : [];
3678
+ for (const oid of selectedOptIds) {
3679
+ ctx[oid] = applicable ? tagId : null;
3680
+ }
3681
+ }
3682
+ return ctx;
3683
+ }
3684
+ function normalizeBindIds(bind) {
3685
+ const out = /* @__PURE__ */ new Set();
3686
+ if (!bind) return out;
3687
+ if (Array.isArray(bind)) {
3688
+ for (const b of bind) if (b) out.add(String(b));
3689
+ } else {
3690
+ out.add(String(bind));
3691
+ }
3692
+ return out;
3693
+ }
3694
+ function buildDevWarnings(props, svcMap, _tagId, _snapshotServiceMap, originalFallbacks, _prunedFallbacks, fieldById, visibleFieldIds, selection) {
3695
+ const out = {};
3696
+ const maybeCollectFailed = globalThis.collectFailedFallbacks;
3697
+ try {
3698
+ if (maybeCollectFailed && originalFallbacks) {
3699
+ const diags = maybeCollectFailed(
3700
+ {
3701
+ ...props,
3702
+ fallbacks: originalFallbacks
3703
+ },
3704
+ svcMap,
3705
+ { mode: "dev" }
3706
+ );
3707
+ if (diags && diags.length) {
3708
+ out.fallbacks = diags;
3709
+ }
3710
+ }
3711
+ } catch {
3712
+ }
3713
+ const utilityWarnings = [];
3714
+ for (const fid of visibleFieldIds) {
3715
+ const f = fieldById.get(fid);
3716
+ if (!f) continue;
3717
+ const hasVal = selection.formValuesByFieldId[fid] !== void 0;
3718
+ if (hasVal && !f.name && !isOptionBased(f)) {
3719
+ utilityWarnings.push({
3720
+ nodeId: fid,
3721
+ reason: "missing_field_name_for_form_value"
3722
+ });
3723
+ }
3724
+ }
3725
+ if (utilityWarnings.length) {
3726
+ out.utility = utilityWarnings;
3727
+ }
3728
+ if (!out.fallbacks && !out.utility) return void 0;
3729
+ return out;
3730
+ }
3731
+ function toSnapshotPolicy(settings) {
3732
+ var _a, _b, _c;
3733
+ const requireConstraintFit = (_a = settings.requireConstraintFit) != null ? _a : true;
3734
+ const rp = (_b = settings.ratePolicy) != null ? _b : { kind: "lte_primary" };
3735
+ switch (rp.kind) {
3736
+ case "lte_primary":
3737
+ return {
3738
+ ratePolicy: { kind: "lte_primary" },
3739
+ requireConstraintFit
3740
+ };
3741
+ case "within_pct":
3742
+ return {
3743
+ ratePolicy: {
3744
+ kind: "lte_primary",
3745
+ thresholdPct: Math.max(0, (_c = rp.pct) != null ? _c : 0)
3746
+ },
3747
+ requireConstraintFit
3748
+ };
3749
+ case "at_least_pct_lower":
3750
+ return {
3751
+ ratePolicy: { kind: "lte_primary" },
3752
+ requireConstraintFit
3753
+ };
3754
+ default:
3755
+ return {
3756
+ ratePolicy: { kind: "lte_primary" },
3757
+ requireConstraintFit
3758
+ };
3759
+ }
3760
+ }
3761
+ function getCap2(map, id) {
3762
+ const direct = map[id];
3763
+ if (direct) return direct;
3764
+ const strKey = String(id);
3765
+ const byStr = map[strKey];
3766
+ if (byStr) return byStr;
3767
+ const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
3768
+ if (Number.isFinite(n)) {
3769
+ const byNum = map[n];
3770
+ if (byNum) return byNum;
3771
+ }
3772
+ return void 0;
3773
+ }
3774
+ function resolveMinMax(servicesList, services) {
3775
+ let min = void 0;
3776
+ let max = void 0;
3777
+ for (const sid of servicesList) {
3778
+ const cap = getCap2(services, sid);
3779
+ if (!cap) continue;
3780
+ if (typeof cap.min === "number" && Number.isFinite(cap.min)) {
3781
+ min = min === void 0 ? cap.min : Math.min(min, cap.min);
3782
+ }
3783
+ if (typeof cap.max === "number" && Number.isFinite(cap.max)) {
3784
+ max = max === void 0 ? cap.max : Math.max(max, cap.max);
3785
+ }
3786
+ }
3787
+ return { min: min != null ? min : 1, ...max !== void 0 ? { max } : {} };
3788
+ }
2889
3789
  export {
3790
+ buildOrderSnapshot,
2890
3791
  collectFailedFallbacks,
2891
3792
  createBuilder,
2892
3793
  createNodeIndex,
@@ -2894,6 +3795,7 @@ export {
2894
3795
  normalise,
2895
3796
  resolveServiceFallback,
2896
3797
  validate,
3798
+ validateAsync,
2897
3799
  validateRateCoherenceDeep
2898
3800
  };
2899
3801
  //# sourceMappingURL=index.js.map