@timeax/digital-service-engine 0.0.1 → 0.0.3

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.
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/core/index.ts
21
21
  var core_exports = {};
22
22
  __export(core_exports, {
23
+ buildOrderSnapshot: () => buildOrderSnapshot,
23
24
  collectFailedFallbacks: () => collectFailedFallbacks,
24
25
  createBuilder: () => createBuilder,
25
26
  createNodeIndex: () => createNodeIndex,
@@ -27,6 +28,7 @@ __export(core_exports, {
27
28
  normalise: () => normalise,
28
29
  resolveServiceFallback: () => resolveServiceFallback,
29
30
  validate: () => validate,
31
+ validateAsync: () => validateAsync,
30
32
  validateRateCoherenceDeep: () => validateRateCoherenceDeep
31
33
  });
32
34
  module.exports = __toCommonJS(core_exports);
@@ -313,11 +315,6 @@ function hasAnyServiceOption(f) {
313
315
  var _a;
314
316
  return ((_a = f.options) != null ? _a : []).some((o) => isFiniteNumber(o.service_id));
315
317
  }
316
- function isBoundTo(f, tagId) {
317
- const b = f.bind_id;
318
- if (!b) return false;
319
- return Array.isArray(b) ? b.includes(tagId) : b === tagId;
320
- }
321
318
  function getByPath(obj, path) {
322
319
  if (!path) return void 0;
323
320
  const parts = path.split(".");
@@ -403,33 +400,214 @@ function withAffected(details, ids) {
403
400
  return { ...details != null ? details : {}, affectedIds: ids };
404
401
  }
405
402
 
403
+ // src/core/node-map.ts
404
+ function buildNodeMap(props) {
405
+ var _a, _b, _c;
406
+ const map = /* @__PURE__ */ new Map();
407
+ for (const t of (_a = props.filters) != null ? _a : []) {
408
+ if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
409
+ }
410
+ for (const f of (_b = props.fields) != null ? _b : []) {
411
+ if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
412
+ for (const o of (_c = f.options) != null ? _c : []) {
413
+ if (!map.has(o.id))
414
+ map.set(o.id, {
415
+ kind: "option",
416
+ id: o.id,
417
+ node: o,
418
+ fieldId: f.id
419
+ });
420
+ }
421
+ }
422
+ return map;
423
+ }
424
+ function resolveTrigger(trigger, nodeMap) {
425
+ const idx = trigger.indexOf("::");
426
+ if (idx !== -1) {
427
+ const fieldId = trigger.slice(0, idx);
428
+ const optionId = trigger.slice(idx + 2);
429
+ return { kind: "composite", triggerKey: trigger, fieldId, optionId };
430
+ }
431
+ const direct = nodeMap.get(trigger);
432
+ if (!direct) return void 0;
433
+ if (direct.kind === "option") {
434
+ return {
435
+ kind: "option",
436
+ triggerKey: trigger,
437
+ id: direct.id,
438
+ fieldId: direct.fieldId
439
+ };
440
+ }
441
+ return { kind: direct.kind, triggerKey: trigger, id: direct.id };
442
+ }
443
+
444
+ // src/core/visibility.ts
445
+ function visibleFieldIdsUnder(props, tagId, opts = {}) {
446
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
447
+ const tags = (_a = props.filters) != null ? _a : [];
448
+ const fields = (_b = props.fields) != null ? _b : [];
449
+ const tagById = new Map(tags.map((t) => [t.id, t]));
450
+ const tag = tagById.get(tagId);
451
+ if (!tag) return [];
452
+ const nodeMap = buildNodeMap(props);
453
+ const lineageDepth = /* @__PURE__ */ new Map();
454
+ {
455
+ const guard = /* @__PURE__ */ new Set();
456
+ let cur = tag;
457
+ let d = 0;
458
+ while (cur && !guard.has(cur.id)) {
459
+ lineageDepth.set(cur.id, d++);
460
+ guard.add(cur.id);
461
+ const parentId = cur.bind_id;
462
+ cur = parentId ? tagById.get(parentId) : void 0;
463
+ }
464
+ }
465
+ const isTagInLineage = (id) => lineageDepth.has(id);
466
+ const fieldById = new Map(fields.map((f) => [f.id, f]));
467
+ const ownerDepthForField = (f) => {
468
+ const b = f.bind_id;
469
+ if (!b) return void 0;
470
+ if (typeof b === "string") return lineageDepth.get(b);
471
+ let best = void 0;
472
+ for (const id of b) {
473
+ const d = lineageDepth.get(id);
474
+ if (d == null) continue;
475
+ if (best == null || d < best) best = d;
476
+ }
477
+ return best;
478
+ };
479
+ const ownerDepthForTriggerKey = (triggerKey) => {
480
+ const t = resolveTrigger(triggerKey, nodeMap);
481
+ if (!t) return void 0;
482
+ if (t.kind === "composite") {
483
+ const f = fieldById.get(t.fieldId);
484
+ if (!f) return void 0;
485
+ return ownerDepthForField(f);
486
+ }
487
+ if (t.kind === "field") {
488
+ const f = fieldById.get(t.id);
489
+ if (!f || f.button !== true) return void 0;
490
+ return ownerDepthForField(f);
491
+ }
492
+ if (t.kind === "option") {
493
+ const f = t.fieldId ? fieldById.get(t.fieldId) : void 0;
494
+ if (!f) return void 0;
495
+ return ownerDepthForField(f);
496
+ }
497
+ return void 0;
498
+ };
499
+ const tagInclude = new Set((_c = tag.includes) != null ? _c : []);
500
+ const tagExclude = new Set((_d = tag.excludes) != null ? _d : []);
501
+ const selected = (_e = opts.selectedKeys) != null ? _e : /* @__PURE__ */ new Set();
502
+ const incMap = (_f = props.includes_for_buttons) != null ? _f : {};
503
+ const excMap = (_g = props.excludes_for_buttons) != null ? _g : {};
504
+ const relevantTriggersInOrder = [];
505
+ for (const key of selected) {
506
+ const d = ownerDepthForTriggerKey(key);
507
+ if (d == null) continue;
508
+ relevantTriggersInOrder.push(key);
509
+ }
510
+ const visible = /* @__PURE__ */ new Set();
511
+ const isBoundToLineage = (f) => {
512
+ const b = f.bind_id;
513
+ if (!b) return false;
514
+ if (typeof b === "string") return isTagInLineage(b);
515
+ for (const id of b) if (isTagInLineage(id)) return true;
516
+ return false;
517
+ };
518
+ for (const f of fields) {
519
+ if (isBoundToLineage(f)) visible.add(f.id);
520
+ if (tagInclude.has(f.id)) visible.add(f.id);
521
+ }
522
+ for (const id of tagExclude) visible.delete(id);
523
+ const decide = /* @__PURE__ */ new Map();
524
+ const applyDecision = (fieldId, next) => {
525
+ const prev = decide.get(fieldId);
526
+ if (!prev) return void decide.set(fieldId, next);
527
+ if (next.depth < prev.depth) return void decide.set(fieldId, next);
528
+ if (next.depth > prev.depth) return;
529
+ if (prev.kind === "include" && next.kind === "exclude") {
530
+ decide.set(fieldId, next);
531
+ }
532
+ };
533
+ const revealedOrder = [];
534
+ const revealedSeen = /* @__PURE__ */ new Set();
535
+ for (const triggerKey of relevantTriggersInOrder) {
536
+ const depth = ownerDepthForTriggerKey(triggerKey);
537
+ if (depth == null) continue;
538
+ for (const id of (_h = incMap[triggerKey]) != null ? _h : []) {
539
+ applyDecision(id, { depth, kind: "include" });
540
+ if (!revealedSeen.has(id)) {
541
+ revealedSeen.add(id);
542
+ revealedOrder.push(id);
543
+ }
544
+ }
545
+ for (const id of (_i = excMap[triggerKey]) != null ? _i : []) {
546
+ applyDecision(id, { depth, kind: "exclude" });
547
+ }
548
+ }
549
+ for (const [fid, d] of decide) {
550
+ if (d.kind === "include") visible.add(fid);
551
+ else visible.delete(fid);
552
+ }
553
+ const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
554
+ const order = (_j = props.order_for_tags) == null ? void 0 : _j[tagId];
555
+ if (order && order.length) {
556
+ const ordered = order.filter((fid) => visible.has(fid));
557
+ const orderedSet = new Set(ordered);
558
+ const rest = base.filter((fid) => !orderedSet.has(fid));
559
+ return [...ordered, ...rest];
560
+ }
561
+ return base;
562
+ }
563
+ function visibleFieldsUnder(props, tagId, opts = {}) {
564
+ var _a;
565
+ const ids = visibleFieldIdsUnder(props, tagId, opts);
566
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
567
+ return ids.map((id) => fieldById.get(id)).filter(Boolean);
568
+ }
569
+
406
570
  // src/core/validate/steps/visibility.ts
407
571
  function createFieldsVisibleUnder(v) {
408
572
  return (tagId) => {
409
- var _a, _b, _c, _d, _e, _f;
410
- const tag = v.tagById.get(tagId);
411
- const includesTag = new Set((_a = tag == null ? void 0 : tag.includes) != null ? _a : []);
412
- const excludesTag = new Set((_b = tag == null ? void 0 : tag.excludes) != null ? _b : []);
413
- const incForOpt = (_c = v.props.includes_for_buttons) != null ? _c : {};
414
- const excForOpt = (_d = v.props.excludes_for_buttons) != null ? _d : {};
415
- const includesOpt = /* @__PURE__ */ new Set();
416
- const excludesOpt = /* @__PURE__ */ new Set();
417
- for (const key of v.selectedKeys) {
418
- for (const id of (_e = incForOpt[key]) != null ? _e : []) includesOpt.add(id);
419
- for (const id of (_f = excForOpt[key]) != null ? _f : []) excludesOpt.add(id);
420
- }
421
- const merged = /* @__PURE__ */ new Map();
422
- for (const f of v.fields) {
423
- if (isBoundTo(f, tagId)) merged.set(f.id, f);
424
- if (includesTag.has(f.id)) merged.set(f.id, f);
425
- if (includesOpt.has(f.id)) merged.set(f.id, f);
426
- }
427
- for (const id of excludesTag) merged.delete(id);
428
- for (const id of excludesOpt) merged.delete(id);
429
- return Array.from(merged.values());
573
+ return visibleFieldsUnder(v.props, tagId, {
574
+ selectedKeys: v.selectedKeys
575
+ });
430
576
  };
431
577
  }
432
- function validateVisibility(v) {
578
+ function stableKeyOfSelection(keys) {
579
+ return Array.from(keys).sort().join("|");
580
+ }
581
+ function resolveRootTags(tags) {
582
+ const roots = tags.filter((t) => !t.bind_id);
583
+ return roots.length ? roots : tags.slice(0, 1);
584
+ }
585
+ function isEffectfulTrigger(v, trigger) {
586
+ var _a, _b, _c, _d, _e, _f;
587
+ const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
588
+ const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
589
+ 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;
590
+ }
591
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
592
+ var _a;
593
+ const visible = visibleFieldsUnder(v.props, tagId, {
594
+ selectedKeys
595
+ });
596
+ const triggers = [];
597
+ for (const f of visible) {
598
+ if (f.button === true) {
599
+ const t = f.id;
600
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
601
+ }
602
+ for (const o of (_a = f.options) != null ? _a : []) {
603
+ const t = `${f.id}::${o.id}`;
604
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
605
+ }
606
+ }
607
+ triggers.sort();
608
+ return triggers;
609
+ }
610
+ function runVisibilityRulesOnce(v) {
433
611
  var _a, _b, _c, _d, _e;
434
612
  for (const t of v.tags) {
435
613
  const visible = v.fieldsVisibleUnder(t.id);
@@ -504,12 +682,85 @@ function validateVisibility(v) {
504
682
  }
505
683
  }
506
684
  }
685
+ function dedupeErrorsInPlace(v, startIndex) {
686
+ const seen = /* @__PURE__ */ new Set();
687
+ const keyOfErr = (e) => {
688
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
689
+ 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 : "";
690
+ const other = (_g = (_f = e == null ? void 0 : e.details) == null ? void 0 : _f.other) != null ? _g : "";
691
+ 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 : ""}`;
692
+ };
693
+ const kept = [];
694
+ for (let i = startIndex; i < v.errors.length; i++) {
695
+ const e = v.errors[i];
696
+ const k = keyOfErr(e);
697
+ if (seen.has(k)) continue;
698
+ seen.add(k);
699
+ kept.push(e);
700
+ }
701
+ v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
702
+ }
703
+ function validateVisibility(v, options = {}) {
704
+ var _a, _b, _c;
705
+ const simulate = options.simulate === true;
706
+ if (!simulate) {
707
+ runVisibilityRulesOnce(v);
708
+ return;
709
+ }
710
+ const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
711
+ const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
712
+ const onlyEffectful = options.onlyEffectfulTriggers !== false;
713
+ const roots = resolveRootTags(v.tags);
714
+ const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
715
+ const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
716
+ const errorsStart = v.errors.length;
717
+ const visited = /* @__PURE__ */ new Set();
718
+ const stack = [];
719
+ for (const rt of rootTags) {
720
+ stack.push({
721
+ rootTagId: rt.id,
722
+ selected: new Set(originalSelected),
723
+ depth: 0
724
+ });
725
+ }
726
+ let validatedStates = 0;
727
+ while (stack.length) {
728
+ if (validatedStates >= maxStates) break;
729
+ const state = stack.pop();
730
+ const sig = stableKeyOfSelection(state.selected);
731
+ if (visited.has(sig)) continue;
732
+ visited.add(sig);
733
+ v.selectedKeys = state.selected;
734
+ validatedStates++;
735
+ runVisibilityRulesOnce(v);
736
+ if (state.depth >= maxDepth) continue;
737
+ const triggers = collectSelectableTriggersInContext(
738
+ v,
739
+ state.rootTagId,
740
+ state.selected,
741
+ onlyEffectful
742
+ );
743
+ for (let i = triggers.length - 1; i >= 0; i--) {
744
+ const trig = triggers[i];
745
+ if (state.selected.has(trig)) continue;
746
+ const next = new Set(state.selected);
747
+ next.add(trig);
748
+ stack.push({
749
+ rootTagId: state.rootTagId,
750
+ selected: next,
751
+ depth: state.depth + 1
752
+ });
753
+ }
754
+ }
755
+ v.selectedKeys = originalSelected;
756
+ dedupeErrorsInPlace(v, errorsStart);
757
+ }
507
758
 
508
759
  // src/core/validate/steps/structure.ts
509
760
  function validateStructure(v) {
510
761
  const tags = v.tags;
511
762
  const fields = v.fields;
512
- if (!tags.some((t) => t.id === "root")) {
763
+ if (!tags.some((t) => t.id === "t:root")) {
513
764
  v.errors.push({
514
765
  code: "root_missing",
515
766
  severity: "error",
@@ -699,58 +950,91 @@ function validateIdentity(v) {
699
950
  }
700
951
 
701
952
  // src/core/validate/steps/option-maps.ts
953
+ function parseFieldOptionKey(key) {
954
+ const idx = key.indexOf("::");
955
+ if (idx === -1) return null;
956
+ const fieldId = key.slice(0, idx).trim();
957
+ const optionId = key.slice(idx + 2).trim();
958
+ if (!fieldId || !optionId) return null;
959
+ return { fieldId, optionId };
960
+ }
961
+ function hasOption(v, fid, oid) {
962
+ var _a;
963
+ const f = v.fieldById.get(fid);
964
+ if (!f) return false;
965
+ return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
966
+ }
702
967
  function validateOptionMaps(v) {
703
968
  var _a, _b;
704
969
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
705
970
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
706
- const parseKey = (key) => {
707
- const parts = key.split("::");
708
- const fid = parts[0];
709
- const oid = parts[1];
710
- if (!fid || !oid) return null;
711
- return { fieldId: fid, optionId: oid };
712
- };
713
- const hasOption = (fid, oid) => {
714
- var _a2;
715
- const f = v.fieldById.get(fid);
716
- if (!f) return false;
717
- return !!((_a2 = f.options) != null ? _a2 : []).find((o) => o.id === oid);
971
+ 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.`;
972
+ const validateTriggerKey = (key) => {
973
+ const ref = v.nodeMap.get(key);
974
+ if (ref) {
975
+ if (ref.kind === "option") {
976
+ return {
977
+ ok: true,
978
+ nodeId: ref.fieldId,
979
+ affected: [ref.fieldId, ref.id]
980
+ };
981
+ }
982
+ if (ref.kind === "field") {
983
+ const isButton2 = ref.node.button === true;
984
+ if (!isButton2)
985
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
986
+ return { ok: true, nodeId: ref.id, affected: [ref.id] };
987
+ }
988
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
989
+ }
990
+ const p = parseFieldOptionKey(key);
991
+ if (!p) return { ok: false };
992
+ if (!hasOption(v, p.fieldId, p.optionId))
993
+ return {
994
+ ok: false,
995
+ nodeId: p.fieldId,
996
+ affected: [p.fieldId, p.optionId]
997
+ };
998
+ return {
999
+ ok: true,
1000
+ nodeId: p.fieldId,
1001
+ affected: [p.fieldId, p.optionId]
1002
+ };
718
1003
  };
719
- const badKeyMessage = (key) => `Invalid option-map key "${key}". Expected "fieldId::optionId" pointing to an existing option.`;
720
1004
  for (const k of Object.keys(incMap)) {
721
- const p = parseKey(k);
722
- if (!p || !hasOption(p.fieldId, p.optionId)) {
1005
+ const r = validateTriggerKey(k);
1006
+ if (!r.ok) {
723
1007
  v.errors.push({
724
1008
  code: "bad_option_key",
725
1009
  severity: "error",
726
1010
  message: badKeyMessage(k),
727
- details: { key: k }
1011
+ nodeId: r.nodeId,
1012
+ details: withAffected({ key: k }, r.affected)
728
1013
  });
729
1014
  }
730
1015
  }
731
1016
  for (const k of Object.keys(excMap)) {
732
- const p = parseKey(k);
733
- if (!p || !hasOption(p.fieldId, p.optionId)) {
1017
+ const r = validateTriggerKey(k);
1018
+ if (!r.ok) {
734
1019
  v.errors.push({
735
1020
  code: "bad_option_key",
736
1021
  severity: "error",
737
1022
  message: badKeyMessage(k),
738
- details: { key: k }
1023
+ nodeId: r.nodeId,
1024
+ details: withAffected({ key: k }, r.affected)
739
1025
  });
740
1026
  }
741
1027
  }
742
1028
  for (const k of Object.keys(incMap)) {
743
- if (k in excMap) {
744
- const p = parseKey(k);
745
- const affected = p ? [p.fieldId, p.optionId] : void 0;
746
- v.errors.push({
747
- code: "option_include_exclude_conflict",
748
- severity: "error",
749
- message: `Option-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
750
- nodeId: p == null ? void 0 : p.fieldId,
751
- details: withAffected({ key: k }, affected)
752
- });
753
- }
1029
+ if (!(k in excMap)) continue;
1030
+ const r = validateTriggerKey(k);
1031
+ v.errors.push({
1032
+ code: "option_include_exclude_conflict",
1033
+ severity: "error",
1034
+ message: `Trigger-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
1035
+ nodeId: r.nodeId,
1036
+ details: withAffected({ key: k }, r.affected)
1037
+ });
754
1038
  }
755
1039
  }
756
1040
 
@@ -1655,8 +1939,23 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
1655
1939
  }
1656
1940
 
1657
1941
  // src/core/validate/index.ts
1942
+ function readVisibilitySimOpts(ctx) {
1943
+ const c = ctx;
1944
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
1945
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
1946
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
1947
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
1948
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
1949
+ return {
1950
+ simulate,
1951
+ maxStates,
1952
+ maxDepth,
1953
+ simulateAllRoots,
1954
+ onlyEffectfulTriggers
1955
+ };
1956
+ }
1658
1957
  function validate(props, ctx = {}) {
1659
- var _a, _b;
1958
+ var _a, _b, _c;
1660
1959
  const errors = [];
1661
1960
  const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
1662
1961
  const selectedKeys = new Set(
@@ -1670,6 +1969,7 @@ function validate(props, ctx = {}) {
1670
1969
  for (const f of fields) fieldById.set(f.id, f);
1671
1970
  const v = {
1672
1971
  props,
1972
+ nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
1673
1973
  options: ctx,
1674
1974
  errors,
1675
1975
  serviceMap,
@@ -1684,7 +1984,8 @@ function validate(props, ctx = {}) {
1684
1984
  validateIdentity(v);
1685
1985
  validateOptionMaps(v);
1686
1986
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
1687
- validateVisibility(v);
1987
+ const visSim = readVisibilitySimOpts(ctx);
1988
+ validateVisibility(v, visSim);
1688
1989
  applyPolicies(
1689
1990
  v.errors,
1690
1991
  v.props,
@@ -1703,6 +2004,17 @@ function validate(props, ctx = {}) {
1703
2004
  validateFallbacks(v);
1704
2005
  return v.errors;
1705
2006
  }
2007
+ async function validateAsync(props, ctx = {}) {
2008
+ await Promise.resolve();
2009
+ if (typeof requestAnimationFrame === "function") {
2010
+ await new Promise(
2011
+ (resolve) => requestAnimationFrame(() => resolve())
2012
+ );
2013
+ } else {
2014
+ await new Promise((resolve) => setTimeout(resolve, 0));
2015
+ }
2016
+ return validate(props, ctx);
2017
+ }
1706
2018
 
1707
2019
  // src/core/builder.ts
1708
2020
  function createBuilder(opts = {}) {
@@ -1720,11 +2032,21 @@ var BuilderImpl = class {
1720
2032
  this.optionOwnerById = /* @__PURE__ */ new Map();
1721
2033
  this.history = [];
1722
2034
  this.future = [];
2035
+ this._nodemap = null;
1723
2036
  var _a;
1724
2037
  this.options = { ...opts };
1725
2038
  this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
1726
2039
  }
1727
2040
  /* ───── lifecycle ─────────────────────────────────────────────────────── */
2041
+ isTagId(id) {
2042
+ return this.tagById.has(id);
2043
+ }
2044
+ isFieldId(id) {
2045
+ return this.fieldById.has(id);
2046
+ }
2047
+ isOptionId(id) {
2048
+ return this.optionOwnerById.has(id);
2049
+ }
1728
2050
  load(raw) {
1729
2051
  const next = normalise(raw, {
1730
2052
  defaultPricingRole: "base",
@@ -1944,129 +2266,16 @@ var BuilderImpl = class {
1944
2266
  return validate(this.props, this.options);
1945
2267
  }
1946
2268
  visibleFields(tagId, selectedKeys) {
1947
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
1948
- const props = this.props;
1949
- const tags = (_a = props.filters) != null ? _a : [];
1950
- const fields = (_b = props.fields) != null ? _b : [];
1951
- const tagById = new Map(tags.map((t) => [t.id, t]));
1952
- const tag = tagById.get(tagId);
1953
- if (!tag) return [];
1954
- const lineageDepth = /* @__PURE__ */ new Map();
1955
- {
1956
- const guard = /* @__PURE__ */ new Set();
1957
- let cur = tag;
1958
- let d = 0;
1959
- while (cur && !guard.has(cur.id)) {
1960
- lineageDepth.set(cur.id, d++);
1961
- guard.add(cur.id);
1962
- const parentId = cur.bind_id;
1963
- cur = parentId ? tagById.get(parentId) : void 0;
1964
- }
1965
- }
1966
- const isTagInLineage = (id) => lineageDepth.has(id);
1967
- const fieldById = new Map(fields.map((f) => [f.id, f]));
1968
- const optionOwnerFieldId = /* @__PURE__ */ new Map();
1969
- for (const f of fields) {
1970
- for (const o of (_c = f.options) != null ? _c : []) optionOwnerFieldId.set(o.id, f.id);
1971
- }
1972
- const ownerDepthForField = (f) => {
1973
- const b = f.bind_id;
1974
- if (!b) return void 0;
1975
- if (typeof b === "string") return lineageDepth.get(b);
1976
- let best = void 0;
1977
- for (const id of b) {
1978
- const d = lineageDepth.get(id);
1979
- if (d == null) continue;
1980
- if (best == null || d < best) best = d;
1981
- }
1982
- return best;
1983
- };
1984
- const ownerDepthForTrigger = (triggerId) => {
1985
- if (triggerId.startsWith("o:")) {
1986
- const fid = optionOwnerFieldId.get(triggerId);
1987
- if (!fid) return void 0;
1988
- const f2 = fieldById.get(fid);
1989
- if (!f2) return void 0;
1990
- return ownerDepthForField(f2);
1991
- }
1992
- const f = fieldById.get(triggerId);
1993
- if (!f || f.button !== true) return void 0;
1994
- return ownerDepthForField(f);
1995
- };
1996
- const tagInclude = new Set((_d = tag.includes) != null ? _d : []);
1997
- const tagExclude = new Set((_e = tag.excludes) != null ? _e : []);
1998
- const selected = new Set(
1999
- (_f = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _f : []
2000
- );
2001
- const incMap = (_g = props.includes_for_buttons) != null ? _g : {};
2002
- const excMap = (_h = props.excludes_for_buttons) != null ? _h : {};
2003
- const relevantTriggersInOrder = [];
2004
- for (const key of selected) {
2005
- const d = ownerDepthForTrigger(key);
2006
- if (d == null) continue;
2007
- relevantTriggersInOrder.push(key);
2008
- }
2009
- const visible = /* @__PURE__ */ new Set();
2010
- const isBoundToLineage = (f) => {
2011
- const b = f.bind_id;
2012
- if (!b) return false;
2013
- if (typeof b === "string") return isTagInLineage(b);
2014
- for (const id of b) if (isTagInLineage(id)) return true;
2015
- return false;
2016
- };
2017
- for (const f of fields) {
2018
- if (isBoundToLineage(f)) visible.add(f.id);
2019
- if (tagInclude.has(f.id)) visible.add(f.id);
2020
- }
2021
- for (const id of tagExclude) visible.delete(id);
2022
- const decide = /* @__PURE__ */ new Map();
2023
- const applyDecision = (fieldId, next) => {
2024
- const prev = decide.get(fieldId);
2025
- if (!prev) {
2026
- decide.set(fieldId, next);
2027
- return;
2028
- }
2029
- if (next.depth < prev.depth) {
2030
- decide.set(fieldId, next);
2031
- return;
2032
- }
2033
- if (next.depth > prev.depth) return;
2034
- if (prev.kind === "include" && next.kind === "exclude") {
2035
- decide.set(fieldId, next);
2036
- }
2037
- };
2038
- const revealedOrder = [];
2039
- const revealedSeen = /* @__PURE__ */ new Set();
2040
- for (const triggerId of relevantTriggersInOrder) {
2041
- const depth = ownerDepthForTrigger(triggerId);
2042
- if (depth == null) continue;
2043
- for (const id of (_i = incMap[triggerId]) != null ? _i : []) {
2044
- applyDecision(id, { depth, kind: "include" });
2045
- if (!revealedSeen.has(id)) {
2046
- revealedSeen.add(id);
2047
- revealedOrder.push(id);
2048
- }
2049
- }
2050
- for (const id of (_j = excMap[triggerId]) != null ? _j : []) {
2051
- applyDecision(id, { depth, kind: "exclude" });
2052
- }
2053
- }
2054
- for (const [fid, d] of decide) {
2055
- if (d.kind === "include") visible.add(fid);
2056
- else visible.delete(fid);
2057
- }
2058
- const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
2059
- const order = (_k = props.order_for_tags) == null ? void 0 : _k[tagId];
2060
- if (order && order.length) {
2061
- const ordered = order.filter((fid) => visible.has(fid));
2062
- const orderedSet = new Set(ordered);
2063
- const rest2 = base.filter((fid) => !orderedSet.has(fid));
2064
- return [...ordered, ...rest2];
2065
- }
2066
- const promoted = revealedOrder.filter((fid) => visible.has(fid));
2067
- const promotedSet = new Set(promoted);
2068
- const rest = base.filter((fid) => !promotedSet.has(fid));
2069
- return [...promoted, ...rest];
2269
+ var _a;
2270
+ return visibleFieldIdsUnder(this.props, tagId, {
2271
+ selectedKeys: new Set(
2272
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
2273
+ )
2274
+ });
2275
+ }
2276
+ getNodeMap() {
2277
+ if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
2278
+ return this._nodemap;
2070
2279
  }
2071
2280
  /* ───── history ─────────────────────────────────────────────────────── */
2072
2281
  undo() {
@@ -2090,6 +2299,7 @@ var BuilderImpl = class {
2090
2299
  this.tagById.clear();
2091
2300
  this.fieldById.clear();
2092
2301
  this.optionOwnerById.clear();
2302
+ this._nodemap = null;
2093
2303
  for (const t of this.props.filters) this.tagById.set(t.id, t);
2094
2304
  for (const f of this.props.fields) {
2095
2305
  this.fieldById.set(f.id, f);
@@ -2571,6 +2781,7 @@ var toBindList = (b) => {
2571
2781
  function createNodeIndex(builder) {
2572
2782
  var _a, _b, _c, _d;
2573
2783
  const props = builder.getProps();
2784
+ const nodeMap = builder.getNodeMap();
2574
2785
  const tags = (_a = props.filters) != null ? _a : [];
2575
2786
  const fields = (_b = props.fields) != null ? _b : [];
2576
2787
  const tagById = new Map(tags.map((t) => [t.id, t]));
@@ -2774,7 +2985,9 @@ function createNodeIndex(builder) {
2774
2985
  return parentById.get(id) === tid;
2775
2986
  },
2776
2987
  getDescendant(descendantId) {
2777
- return this.getDescendants().find((item) => item.id == descendantId);
2988
+ return this.getDescendants().find(
2989
+ (item) => item.id == descendantId
2990
+ );
2778
2991
  },
2779
2992
  getDescendants() {
2780
2993
  const results = [];
@@ -2783,7 +2996,9 @@ function createNodeIndex(builder) {
2783
2996
  const node2 = getField(fieldId);
2784
2997
  if (!node2) continue;
2785
2998
  const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, id);
2786
- results.push(explicit ? node2 : { ...node2, isInherited: true });
2999
+ results.push(
3000
+ explicit ? node2 : { ...node2, isInherited: true }
3001
+ );
2787
3002
  }
2788
3003
  return Object.freeze(results);
2789
3004
  }
@@ -2832,7 +3047,9 @@ function createNodeIndex(builder) {
2832
3047
  return false;
2833
3048
  },
2834
3049
  getDescendant(descendantId, context) {
2835
- return this.getDescendants(context).find((item) => item.id == descendantId);
3050
+ return this.getDescendants(context).find(
3051
+ (item) => item.id == descendantId
3052
+ );
2836
3053
  },
2837
3054
  getDescendants(tagId) {
2838
3055
  return resolveDescendants(id, includes, tagId, !isButton2);
@@ -2874,7 +3091,9 @@ function createNodeIndex(builder) {
2874
3091
  return owner.isBoundTo(tagId);
2875
3092
  },
2876
3093
  getDescendant(descendantId, context) {
2877
- return this.getDescendants(context).find((item) => item.id == descendantId);
3094
+ return this.getDescendants(context).find(
3095
+ (item) => item.id == descendantId
3096
+ );
2878
3097
  },
2879
3098
  getDescendants(tagId) {
2880
3099
  return resolveDescendants(id, includes, tagId);
@@ -2885,7 +3104,7 @@ function createNodeIndex(builder) {
2885
3104
  return node;
2886
3105
  };
2887
3106
  const getNode = (input) => {
2888
- var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
3107
+ var _a2, _b2, _c2, _d2, _e;
2889
3108
  if (typeof input !== "string") {
2890
3109
  if ("bind_id" in input && !("type" in input))
2891
3110
  return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
@@ -2895,11 +3114,7 @@ function createNodeIndex(builder) {
2895
3114
  }
2896
3115
  const cached = nodeCache.get(input);
2897
3116
  if (cached) return cached;
2898
- const id = input;
2899
- if (id.startsWith("t:")) return (_d2 = getTag(id)) != null ? _d2 : mkUnknown(id);
2900
- if (id.startsWith("f:")) return (_e = getField(id)) != null ? _e : mkUnknown(id);
2901
- if (id.startsWith("o:")) return (_f = getOption(id)) != null ? _f : mkUnknown(id);
2902
- return (_i = (_h = (_g = getTag(id)) != null ? _g : getField(id)) != null ? _h : getOption(id)) != null ? _i : mkUnknown(id);
3117
+ return (_e = (_d2 = nodeMap.get(input)) == null ? void 0 : _d2.node) != null ? _e : mkUnknown(input);
2903
3118
  };
2904
3119
  const mkUnknown = (id) => {
2905
3120
  const u = {
@@ -2919,8 +3134,696 @@ function createNodeIndex(builder) {
2919
3134
  getOption
2920
3135
  };
2921
3136
  }
3137
+
3138
+ // src/utils/prune-fallbacks.ts
3139
+ function pruneInvalidNodeFallbacks(props, services, settings) {
3140
+ var _a, _b;
3141
+ const fb = props.fallbacks;
3142
+ if (!(fb == null ? void 0 : fb.nodes) || Object.keys(fb.nodes).length === 0) {
3143
+ return { props, removed: [] };
3144
+ }
3145
+ const nodeContexts = /* @__PURE__ */ new Map();
3146
+ const nodePrimary = /* @__PURE__ */ new Map();
3147
+ for (const nodeId of Object.keys(fb.nodes)) {
3148
+ const tag = props.filters.find((t) => t.id === nodeId);
3149
+ if (tag) {
3150
+ nodeContexts.set(nodeId, [tag.id]);
3151
+ nodePrimary.set(nodeId, tag.service_id);
3152
+ continue;
3153
+ }
3154
+ const field = props.fields.find((f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId));
3155
+ if (field) {
3156
+ const contexts = toBindArray(field.bind_id);
3157
+ nodeContexts.set(nodeId, contexts);
3158
+ const opt = field.options.find((o) => o.id === nodeId);
3159
+ nodePrimary.set(nodeId, opt.service_id);
3160
+ continue;
3161
+ }
3162
+ nodeContexts.set(nodeId, []);
3163
+ nodePrimary.set(nodeId, void 0);
3164
+ }
3165
+ const diags = collectFailedFallbacks(props, services, { ...settings, mode: "dev" });
3166
+ const failuresByPair = /* @__PURE__ */ new Map();
3167
+ const totalContextsByNode = /* @__PURE__ */ new Map();
3168
+ for (const [nodeId, ctxs] of nodeContexts.entries()) {
3169
+ totalContextsByNode.set(nodeId, Math.max(1, ctxs.length));
3170
+ }
3171
+ for (const d of diags) {
3172
+ if (d.scope !== "node") continue;
3173
+ const key = `${d.nodeId}::${String(d.candidate)}`;
3174
+ let rec = failuresByPair.get(key);
3175
+ if (!rec) {
3176
+ rec = { reasons: /* @__PURE__ */ new Set(), contexts: /* @__PURE__ */ new Set() };
3177
+ failuresByPair.set(key, rec);
3178
+ }
3179
+ rec.reasons.add(d.reason);
3180
+ if (d.tagContext) rec.contexts.add(d.tagContext);
3181
+ }
3182
+ const prunedNodes = {};
3183
+ const removed = [];
3184
+ for (const [nodeId, list] of Object.entries(fb.nodes)) {
3185
+ const contexts = (_a = nodeContexts.get(nodeId)) != null ? _a : [];
3186
+ const totalContexts = Math.max(1, contexts.length);
3187
+ const keep = [];
3188
+ for (const cand of list) {
3189
+ const key = `${nodeId}::${String(cand)}`;
3190
+ const rec = failuresByPair.get(key);
3191
+ if (!rec) {
3192
+ keep.push(cand);
3193
+ continue;
3194
+ }
3195
+ const failedContextsCount = rec.contexts.size > 0 ? rec.contexts.size : totalContexts;
3196
+ const failsAll = failedContextsCount >= totalContexts;
3197
+ if (failsAll) {
3198
+ removed.push({
3199
+ nodeId,
3200
+ candidate: cand,
3201
+ reasons: Array.from(rec.reasons),
3202
+ contexts: contexts.length ? contexts.slice() : void 0
3203
+ });
3204
+ } else {
3205
+ keep.push(cand);
3206
+ }
3207
+ }
3208
+ if (keep.length) prunedNodes[nodeId] = keep;
3209
+ }
3210
+ const outProps = {
3211
+ ...props,
3212
+ fallbacks: {
3213
+ ...((_b = props.fallbacks) == null ? void 0 : _b.global) ? { global: props.fallbacks.global } : {},
3214
+ ...Object.keys(prunedNodes).length ? { nodes: prunedNodes } : {}
3215
+ }
3216
+ };
3217
+ return { props: outProps, removed };
3218
+ }
3219
+ function toBindArray(bind) {
3220
+ if (!bind) return [];
3221
+ return Array.isArray(bind) ? bind.slice() : [bind];
3222
+ }
3223
+
3224
+ // src/utils/util.ts
3225
+ function toFiniteNumber(v) {
3226
+ const n = Number(v);
3227
+ return Number.isFinite(n) ? n : NaN;
3228
+ }
3229
+ function constraintFitOk(svcMap, candidate, constraints) {
3230
+ const cap = svcMap[Number(candidate)];
3231
+ if (!cap) return false;
3232
+ if (constraints.dripfeed === true && !cap.dripfeed) return false;
3233
+ if (constraints.refill === true && !cap.refill) return false;
3234
+ return !(constraints.cancel === true && !cap.cancel);
3235
+ }
3236
+ function rateOk(svcMap, candidate, primary, policy) {
3237
+ var _a, _b, _c;
3238
+ const cand = svcMap[Number(candidate)];
3239
+ const prim = svcMap[Number(primary)];
3240
+ if (!cand || !prim) return false;
3241
+ const cRate = toFiniteNumber(cand.rate);
3242
+ const pRate = toFiniteNumber(prim.rate);
3243
+ if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
3244
+ const rp = (_a = policy.ratePolicy) != null ? _a : { kind: "lte_primary" };
3245
+ switch (rp.kind) {
3246
+ case "lte_primary":
3247
+ return cRate <= pRate;
3248
+ case "within_pct": {
3249
+ const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
3250
+ return cRate <= pRate * (1 + pct / 100);
3251
+ }
3252
+ case "at_least_pct_lower": {
3253
+ const pct = Math.max(0, (_c = rp.pct) != null ? _c : 0);
3254
+ return cRate <= pRate * (1 - pct / 100);
3255
+ }
3256
+ default:
3257
+ return false;
3258
+ }
3259
+ }
3260
+
3261
+ // src/utils/build-order-snapshot.ts
3262
+ function buildOrderSnapshot(props, builder, selection, services, settings = {}) {
3263
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3264
+ const mode = (_a = settings.mode) != null ? _a : "prod";
3265
+ const hostDefaultQty = Number.isFinite(
3266
+ (_b = settings.hostDefaultQuantity) != null ? _b : 1
3267
+ ) ? settings.hostDefaultQuantity : 1;
3268
+ const fbSettings = {
3269
+ requireConstraintFit: true,
3270
+ ratePolicy: { kind: "lte_primary" },
3271
+ selectionStrategy: "priority",
3272
+ mode: mode === "dev" ? "dev" : "strict",
3273
+ ...(_c = settings.fallback) != null ? _c : {}
3274
+ };
3275
+ const builtAt = (/* @__PURE__ */ new Date()).toISOString();
3276
+ const tagId = selection.activeTagId;
3277
+ const selectedButtonKeys = (_d = selection.selectedKeys) != null ? _d : toSelectedOptionKeys(selection.optionSelectionsByFieldId);
3278
+ const visibleFieldIds = builder.visibleFields(
3279
+ tagId,
3280
+ selectedButtonKeys
3281
+ );
3282
+ const tagById = new Map(
3283
+ ((_e = props.filters) != null ? _e : []).map((t) => [t.id, t])
3284
+ );
3285
+ const fieldById = new Map(
3286
+ ((_f = props.fields) != null ? _f : []).map((f) => [f.id, f])
3287
+ );
3288
+ const tagConstraints = (_h = (_g = tagById.get(tagId)) == null ? void 0 : _g.constraints) != null ? _h : void 0;
3289
+ const selectionFields = visibleFieldIds.map((fid) => fieldById.get(fid)).filter((f) => !!f).map((f) => {
3290
+ var _a2;
3291
+ const optIds = isOptionBased(f) ? (_a2 = selection.optionSelectionsByFieldId[f.id]) != null ? _a2 : [] : void 0;
3292
+ return {
3293
+ id: f.id,
3294
+ type: String(f.type),
3295
+ ...optIds && optIds.length ? { selectedOptions: optIds } : {}
3296
+ };
3297
+ });
3298
+ const { formValues, selections } = buildInputs(
3299
+ visibleFieldIds,
3300
+ fieldById,
3301
+ selection
3302
+ );
3303
+ const qtyRes = resolveQuantity(
3304
+ visibleFieldIds,
3305
+ fieldById,
3306
+ selection,
3307
+ hostDefaultQty
3308
+ );
3309
+ const quantity = qtyRes.quantity;
3310
+ const quantitySource = qtyRes.source;
3311
+ const { serviceMap, servicesList } = resolveServices(
3312
+ tagId,
3313
+ visibleFieldIds,
3314
+ selection,
3315
+ tagById,
3316
+ fieldById
3317
+ );
3318
+ const { min, max } = resolveMinMax(servicesList, services);
3319
+ const prunedFallbacks = pruneFallbacksConservative(
3320
+ props.fallbacks,
3321
+ { tagId, constraints: tagConstraints, serviceMap, servicesList },
3322
+ services,
3323
+ fbSettings
3324
+ );
3325
+ const utilities = collectUtilityLineItems(
3326
+ visibleFieldIds,
3327
+ fieldById,
3328
+ selection,
3329
+ quantity
3330
+ );
3331
+ const warnings = mode === "dev" ? buildDevWarnings(
3332
+ props,
3333
+ services,
3334
+ tagId,
3335
+ serviceMap,
3336
+ prunedFallbacks.original,
3337
+ prunedFallbacks.pruned,
3338
+ fieldById,
3339
+ visibleFieldIds,
3340
+ selection
3341
+ ) : void 0;
3342
+ const snapshotPolicy = toSnapshotPolicy(fbSettings);
3343
+ const meta = {
3344
+ schema_version: props.schema_version,
3345
+ workspaceId: settings.workspaceId,
3346
+ builder: settings.builderCommit ? { commit: settings.builderCommit } : void 0,
3347
+ context: {
3348
+ tag: tagId,
3349
+ constraints: tagConstraints != null ? tagConstraints : {},
3350
+ nodeContexts: buildNodeContexts(
3351
+ tagId,
3352
+ visibleFieldIds,
3353
+ fieldById,
3354
+ selection
3355
+ ),
3356
+ policy: snapshotPolicy
3357
+ }
3358
+ };
3359
+ const snapshot = {
3360
+ version: "1",
3361
+ mode,
3362
+ builtAt,
3363
+ selection: {
3364
+ tag: tagId,
3365
+ buttons: selectedButtonKeys,
3366
+ fields: selectionFields
3367
+ },
3368
+ inputs: {
3369
+ form: formValues,
3370
+ selections
3371
+ },
3372
+ min,
3373
+ max: max != null ? max : min,
3374
+ quantity,
3375
+ quantitySource,
3376
+ services: servicesList,
3377
+ serviceMap,
3378
+ ...prunedFallbacks.pruned ? { fallbacks: prunedFallbacks.pruned } : {},
3379
+ ...utilities.length ? { utilities } : {},
3380
+ ...warnings ? { warnings } : {},
3381
+ meta
3382
+ };
3383
+ return snapshot;
3384
+ }
3385
+ function isOptionBased(f) {
3386
+ const hasOptions = Array.isArray(f.options) && f.options.length > 0;
3387
+ return hasOptions || isMultiField(f);
3388
+ }
3389
+ function toSelectedOptionKeys(byField) {
3390
+ const keys = [];
3391
+ for (const [fieldId, optionIds] of Object.entries(byField != null ? byField : {})) {
3392
+ for (const optId of optionIds != null ? optionIds : []) {
3393
+ keys.push(`${fieldId}::${optId}`);
3394
+ }
3395
+ }
3396
+ return keys;
3397
+ }
3398
+ function isServicedBased(field) {
3399
+ if (field.service_id) return true;
3400
+ return !!(field.options && field.options.some((item) => item.service_id));
3401
+ }
3402
+ function buildInputs(visibleFieldIds, fieldById, selection) {
3403
+ const formValues = {};
3404
+ const selections = {};
3405
+ for (const fid of visibleFieldIds) {
3406
+ const f = fieldById.get(fid);
3407
+ if (!f) continue;
3408
+ const selOptIds = selection.optionSelectionsByFieldId[fid];
3409
+ if (selOptIds && selOptIds.length) {
3410
+ selections[fid] = [...selOptIds];
3411
+ }
3412
+ if (!isServicedBased(f)) {
3413
+ const name = f.name;
3414
+ const val = selection.formValuesByFieldId[fid];
3415
+ if (!name || val === void 0) continue;
3416
+ formValues[name] = val;
3417
+ }
3418
+ }
3419
+ return { formValues, selections };
3420
+ }
3421
+ function resolveQuantity(visibleFieldIds, fieldById, selection, hostDefault) {
3422
+ var _a;
3423
+ for (const fid of visibleFieldIds) {
3424
+ const f = fieldById.get(fid);
3425
+ if (!f) continue;
3426
+ const rule = readQuantityRule(
3427
+ (_a = f.meta) == null ? void 0 : _a.quantity
3428
+ );
3429
+ if (!rule) continue;
3430
+ const raw = selection.formValuesByFieldId[fid];
3431
+ const evaluated = evaluateQuantityRule(rule, raw);
3432
+ if (Number.isFinite(evaluated) && evaluated > 0) {
3433
+ return {
3434
+ quantity: evaluated,
3435
+ source: { kind: "field", id: f.id, rule }
3436
+ };
3437
+ }
3438
+ }
3439
+ return {
3440
+ quantity: hostDefault,
3441
+ source: { kind: "default", defaultedFromHost: true }
3442
+ };
3443
+ }
3444
+ function readQuantityRule(v) {
3445
+ if (!v || typeof v !== "object") return void 0;
3446
+ const src = v;
3447
+ if (src.valueBy !== "value" && src.valueBy !== "length" && src.valueBy !== "eval")
3448
+ return void 0;
3449
+ const out = { valueBy: src.valueBy };
3450
+ if (src.code && typeof src.code === "string") out.code = src.code;
3451
+ return out;
3452
+ }
3453
+ function evaluateQuantityRule(rule, raw) {
3454
+ switch (rule.valueBy) {
3455
+ case "value": {
3456
+ const n = Number(Array.isArray(raw) ? raw[0] : raw);
3457
+ return Number.isFinite(n) ? n : NaN;
3458
+ }
3459
+ case "length": {
3460
+ if (Array.isArray(raw)) return raw.length;
3461
+ if (typeof raw === "string") return raw.length;
3462
+ return NaN;
3463
+ }
3464
+ case "eval": {
3465
+ try {
3466
+ if (!rule.code || typeof rule.code !== "string") return NaN;
3467
+ const fn = new Function(
3468
+ "value",
3469
+ "values",
3470
+ `return (function(){ ${rule.code}
3471
+ })()`
3472
+ );
3473
+ const single = Array.isArray(raw) ? raw[0] : raw;
3474
+ const values = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
3475
+ const out = fn(single, values);
3476
+ const n = Number(out);
3477
+ return Number.isFinite(n) ? n : NaN;
3478
+ } catch {
3479
+ return NaN;
3480
+ }
3481
+ }
3482
+ default:
3483
+ return NaN;
3484
+ }
3485
+ }
3486
+ function resolveServices(tagId, visibleFieldIds, selection, tagById, fieldById) {
3487
+ var _a;
3488
+ const serviceMap = {};
3489
+ const ordered = [];
3490
+ const tag = tagById.get(tagId);
3491
+ let primary;
3492
+ let primaryOrigin;
3493
+ if ((tag == null ? void 0 : tag.service_id) !== void 0) {
3494
+ primary = tag.service_id;
3495
+ primaryOrigin = "tag";
3496
+ }
3497
+ const optionVisit = buildOptionVisitOrder(selection, fieldById);
3498
+ for (const { fieldId, optionId } of optionVisit) {
3499
+ if (!visibleFieldIds.includes(fieldId)) continue;
3500
+ const f = fieldById.get(fieldId);
3501
+ if (!f || !Array.isArray(f.options)) continue;
3502
+ const opt = f.options.find((o) => o.id === optionId);
3503
+ if (!opt) continue;
3504
+ const role = (_a = opt.pricing_role) != null ? _a : "base";
3505
+ const sid = opt.service_id;
3506
+ if (role === "utility") continue;
3507
+ if (sid !== void 0) {
3508
+ if (primary === void 0 || primaryOrigin === "tag") {
3509
+ primary = sid;
3510
+ primaryOrigin = "option";
3511
+ ordered.length = 0;
3512
+ ordered.push(primary);
3513
+ } else {
3514
+ ordered.push(sid);
3515
+ }
3516
+ pushService(serviceMap, optionId, sid);
3517
+ }
3518
+ }
3519
+ if (primaryOrigin !== "option" && primary !== void 0) {
3520
+ ordered.unshift(primary);
3521
+ pushService(serviceMap, tagId, primary);
3522
+ } else {
3523
+ }
3524
+ const servicesList = dedupeByString(ordered);
3525
+ return { serviceMap, servicesList };
3526
+ }
3527
+ function buildOptionVisitOrder(selection, fieldById) {
3528
+ var _a;
3529
+ if (selection.optionTraversalOrder && selection.optionTraversalOrder.length) {
3530
+ return selection.optionTraversalOrder.slice();
3531
+ }
3532
+ const out = [];
3533
+ for (const [fid, optIds] of Object.entries(
3534
+ (_a = selection.optionSelectionsByFieldId) != null ? _a : {}
3535
+ )) {
3536
+ const f = fieldById.get(fid);
3537
+ if (!f) continue;
3538
+ for (const oid of optIds != null ? optIds : [])
3539
+ out.push({ fieldId: fid, optionId: oid });
3540
+ }
3541
+ return out;
3542
+ }
3543
+ function pushService(map, nodeId, sid) {
3544
+ if (!map[nodeId]) map[nodeId] = [];
3545
+ map[nodeId].push(sid);
3546
+ }
3547
+ function dedupeByString(arr) {
3548
+ const s = /* @__PURE__ */ new Set();
3549
+ const out = [];
3550
+ for (const v of arr) {
3551
+ const key = String(v);
3552
+ if (s.has(key)) continue;
3553
+ s.add(key);
3554
+ out.push(v);
3555
+ }
3556
+ return out;
3557
+ }
3558
+ function pruneFallbacksConservative(fallbacks, env, svcMap, policy) {
3559
+ var _a, _b;
3560
+ if (!fallbacks) return { pruned: void 0, original: void 0 };
3561
+ try {
3562
+ const { props: prunedProps } = pruneInvalidNodeFallbacks(
3563
+ {
3564
+ filters: [],
3565
+ fields: [],
3566
+ schema_version: "1.0",
3567
+ fallbacks
3568
+ },
3569
+ svcMap,
3570
+ policy
3571
+ );
3572
+ return {
3573
+ pruned: prunedProps.fallbacks,
3574
+ original: fallbacks
3575
+ };
3576
+ } catch {
3577
+ const out = {};
3578
+ const requireFit = (_a = policy.requireConstraintFit) != null ? _a : true;
3579
+ if (fallbacks.nodes) {
3580
+ const keptNodes = {};
3581
+ for (const [nodeId, candidates] of Object.entries(
3582
+ fallbacks.nodes
3583
+ )) {
3584
+ if (!env.serviceMap[nodeId]) continue;
3585
+ const primary = ((_b = env.serviceMap[nodeId]) != null ? _b : [])[0];
3586
+ const kept = [];
3587
+ for (const cand of candidates != null ? candidates : []) {
3588
+ if (!rateOk(svcMap, cand, primary, policy)) continue;
3589
+ if (requireFit && env.constraints && !constraintFitOk(svcMap, cand, env.constraints))
3590
+ continue;
3591
+ kept.push(cand);
3592
+ }
3593
+ if (kept.length) keptNodes[nodeId] = kept;
3594
+ }
3595
+ if (Object.keys(keptNodes).length) out.nodes = keptNodes;
3596
+ }
3597
+ if (fallbacks.global) {
3598
+ const keptGlobal = {};
3599
+ const present = new Set(env.servicesList.map((sid) => String(sid)));
3600
+ for (const [primary, cands] of Object.entries(
3601
+ fallbacks.global
3602
+ )) {
3603
+ if (!present.has(String(primary))) continue;
3604
+ const primId = isFiniteNumber3(primary) ? Number(primary) : primary;
3605
+ const kept = [];
3606
+ for (const cand of cands != null ? cands : []) {
3607
+ if (!rateOk(svcMap, cand, primId, policy)) continue;
3608
+ if (requireFit && env.constraints && !constraintFitOk(svcMap, cand, env.constraints))
3609
+ continue;
3610
+ kept.push(cand);
3611
+ }
3612
+ if (kept.length) keptGlobal[primId] = kept;
3613
+ }
3614
+ if (Object.keys(keptGlobal).length)
3615
+ out.global = keptGlobal;
3616
+ }
3617
+ return {
3618
+ pruned: Object.keys(out).length ? out : void 0,
3619
+ original: fallbacks
3620
+ };
3621
+ }
3622
+ }
3623
+ function isFiniteNumber3(v) {
3624
+ return typeof v === "number" && Number.isFinite(v);
3625
+ }
3626
+ function collectUtilityLineItems(visibleFieldIds, fieldById, selection, quantity) {
3627
+ var _a, _b, _c, _d, _e;
3628
+ const items = [];
3629
+ for (const fid of visibleFieldIds) {
3630
+ const f = fieldById.get(fid);
3631
+ if (!f) continue;
3632
+ const isUtilityField = ((_a = f.pricing_role) != null ? _a : "base") === "utility";
3633
+ const marker = readUtilityMarker((_b = f.meta) == null ? void 0 : _b.utility);
3634
+ if (isUtilityField && marker) {
3635
+ const val = selection.formValuesByFieldId[f.id];
3636
+ const item = buildUtilityItemFromMarker(
3637
+ f.id,
3638
+ marker,
3639
+ quantity,
3640
+ val
3641
+ );
3642
+ if (item) items.push(item);
3643
+ }
3644
+ if (Array.isArray(f.options) && f.options.length) {
3645
+ const selectedOptIds = (_c = selection.optionSelectionsByFieldId[f.id]) != null ? _c : [];
3646
+ if (selectedOptIds.length) {
3647
+ const optById = new Map(
3648
+ f.options.map((o) => [o.id, o])
3649
+ );
3650
+ for (const oid of selectedOptIds) {
3651
+ const opt = optById.get(oid);
3652
+ if (!opt) continue;
3653
+ if (((_d = opt.pricing_role) != null ? _d : "base") !== "utility") continue;
3654
+ const om = readUtilityMarker((_e = opt.meta) == null ? void 0 : _e.utility);
3655
+ if (!om) continue;
3656
+ const parentVal = selection.formValuesByFieldId[f.id];
3657
+ const item = buildUtilityItemFromMarker(
3658
+ opt.id,
3659
+ om,
3660
+ quantity,
3661
+ parentVal
3662
+ );
3663
+ if (item) items.push(item);
3664
+ }
3665
+ }
3666
+ }
3667
+ }
3668
+ return items;
3669
+ }
3670
+ function readUtilityMarker(v) {
3671
+ if (!v || typeof v !== "object") return void 0;
3672
+ const src = v;
3673
+ if (!src.mode || typeof src.rate !== "number" || !Number.isFinite(src.rate))
3674
+ return void 0;
3675
+ if (src.mode !== "flat" && src.mode !== "per_quantity" && src.mode !== "per_value" && src.mode !== "percent")
3676
+ return void 0;
3677
+ const out = { mode: src.mode, rate: src.rate };
3678
+ if (src.valueBy === "value" || src.valueBy === "length" || src.valueBy === "eval")
3679
+ out.valueBy = src.valueBy;
3680
+ if (src.code && typeof src.code === "string") out.code = src.code;
3681
+ return out;
3682
+ }
3683
+ function buildUtilityItemFromMarker(nodeId, marker, quantity, value) {
3684
+ var _a, _b;
3685
+ const base = {
3686
+ nodeId,
3687
+ mode: marker.mode,
3688
+ rate: marker.rate,
3689
+ inputs: { quantity }
3690
+ };
3691
+ if (marker.mode === "per_value") {
3692
+ base.inputs.valueBy = (_a = marker.valueBy) != null ? _a : "value";
3693
+ if (marker.valueBy === "length") {
3694
+ base.inputs.value = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : 0;
3695
+ } else if (marker.valueBy === "eval") {
3696
+ base.inputs.evalCodeUsed = true;
3697
+ } else {
3698
+ base.inputs.value = Array.isArray(value) ? (_b = value[0]) != null ? _b : null : value != null ? value : null;
3699
+ }
3700
+ }
3701
+ return base;
3702
+ }
3703
+ function buildNodeContexts(tagId, visibleFieldIds, fieldById, selection) {
3704
+ var _a;
3705
+ const ctx = {};
3706
+ ctx[tagId] = tagId;
3707
+ for (const fid of visibleFieldIds) {
3708
+ const f = fieldById.get(fid);
3709
+ if (!f) continue;
3710
+ const binds = normalizeBindIds(f.bind_id);
3711
+ const applicable = binds.has(tagId);
3712
+ const selectedOptIds = (_a = selection.optionSelectionsByFieldId[fid]) != null ? _a : [];
3713
+ for (const oid of selectedOptIds) {
3714
+ ctx[oid] = applicable ? tagId : null;
3715
+ }
3716
+ }
3717
+ return ctx;
3718
+ }
3719
+ function normalizeBindIds(bind) {
3720
+ const out = /* @__PURE__ */ new Set();
3721
+ if (!bind) return out;
3722
+ if (Array.isArray(bind)) {
3723
+ for (const b of bind) if (b) out.add(String(b));
3724
+ } else {
3725
+ out.add(String(bind));
3726
+ }
3727
+ return out;
3728
+ }
3729
+ function buildDevWarnings(props, svcMap, _tagId, _snapshotServiceMap, originalFallbacks, _prunedFallbacks, fieldById, visibleFieldIds, selection) {
3730
+ const out = {};
3731
+ const maybeCollectFailed = globalThis.collectFailedFallbacks;
3732
+ try {
3733
+ if (maybeCollectFailed && originalFallbacks) {
3734
+ const diags = maybeCollectFailed(
3735
+ {
3736
+ ...props,
3737
+ fallbacks: originalFallbacks
3738
+ },
3739
+ svcMap,
3740
+ { mode: "dev" }
3741
+ );
3742
+ if (diags && diags.length) {
3743
+ out.fallbacks = diags;
3744
+ }
3745
+ }
3746
+ } catch {
3747
+ }
3748
+ const utilityWarnings = [];
3749
+ for (const fid of visibleFieldIds) {
3750
+ const f = fieldById.get(fid);
3751
+ if (!f) continue;
3752
+ const hasVal = selection.formValuesByFieldId[fid] !== void 0;
3753
+ if (hasVal && !f.name && !isOptionBased(f)) {
3754
+ utilityWarnings.push({
3755
+ nodeId: fid,
3756
+ reason: "missing_field_name_for_form_value"
3757
+ });
3758
+ }
3759
+ }
3760
+ if (utilityWarnings.length) {
3761
+ out.utility = utilityWarnings;
3762
+ }
3763
+ if (!out.fallbacks && !out.utility) return void 0;
3764
+ return out;
3765
+ }
3766
+ function toSnapshotPolicy(settings) {
3767
+ var _a, _b, _c;
3768
+ const requireConstraintFit = (_a = settings.requireConstraintFit) != null ? _a : true;
3769
+ const rp = (_b = settings.ratePolicy) != null ? _b : { kind: "lte_primary" };
3770
+ switch (rp.kind) {
3771
+ case "lte_primary":
3772
+ return {
3773
+ ratePolicy: { kind: "lte_primary" },
3774
+ requireConstraintFit
3775
+ };
3776
+ case "within_pct":
3777
+ return {
3778
+ ratePolicy: {
3779
+ kind: "lte_primary",
3780
+ thresholdPct: Math.max(0, (_c = rp.pct) != null ? _c : 0)
3781
+ },
3782
+ requireConstraintFit
3783
+ };
3784
+ case "at_least_pct_lower":
3785
+ return {
3786
+ ratePolicy: { kind: "lte_primary" },
3787
+ requireConstraintFit
3788
+ };
3789
+ default:
3790
+ return {
3791
+ ratePolicy: { kind: "lte_primary" },
3792
+ requireConstraintFit
3793
+ };
3794
+ }
3795
+ }
3796
+ function getCap2(map, id) {
3797
+ const direct = map[id];
3798
+ if (direct) return direct;
3799
+ const strKey = String(id);
3800
+ const byStr = map[strKey];
3801
+ if (byStr) return byStr;
3802
+ const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
3803
+ if (Number.isFinite(n)) {
3804
+ const byNum = map[n];
3805
+ if (byNum) return byNum;
3806
+ }
3807
+ return void 0;
3808
+ }
3809
+ function resolveMinMax(servicesList, services) {
3810
+ let min = void 0;
3811
+ let max = void 0;
3812
+ for (const sid of servicesList) {
3813
+ const cap = getCap2(services, sid);
3814
+ if (!cap) continue;
3815
+ if (typeof cap.min === "number" && Number.isFinite(cap.min)) {
3816
+ min = min === void 0 ? cap.min : Math.min(min, cap.min);
3817
+ }
3818
+ if (typeof cap.max === "number" && Number.isFinite(cap.max)) {
3819
+ max = max === void 0 ? cap.max : Math.max(max, cap.max);
3820
+ }
3821
+ }
3822
+ return { min: min != null ? min : 1, ...max !== void 0 ? { max } : {} };
3823
+ }
2922
3824
  // Annotate the CommonJS export names for ESM import in node:
2923
3825
  0 && (module.exports = {
3826
+ buildOrderSnapshot,
2924
3827
  collectFailedFallbacks,
2925
3828
  createBuilder,
2926
3829
  createNodeIndex,
@@ -2928,6 +3831,7 @@ function createNodeIndex(builder) {
2928
3831
  normalise,
2929
3832
  resolveServiceFallback,
2930
3833
  validate,
3834
+ validateAsync,
2931
3835
  validateRateCoherenceDeep
2932
3836
  });
2933
3837
  //# sourceMappingURL=index.cjs.map