@timeax/digital-service-engine 0.0.1
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.
- package/dist/core/index.cjs +2933 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +507 -0
- package/dist/core/index.d.ts +507 -0
- package/dist/core/index.js +2899 -0
- package/dist/core/index.js.map +1 -0
- package/dist/react/index.cjs +4015 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +1572 -0
- package/dist/react/index.d.ts +1572 -0
- package/dist/react/index.js +3984 -0
- package/dist/react/index.js.map +1 -0
- package/dist/schema/index.cjs +19 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +671 -0
- package/dist/schema/index.d.ts +671 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/workspace/index.cjs +9755 -0
- package/dist/workspace/index.cjs.map +1 -0
- package/dist/workspace/index.d.cts +1995 -0
- package/dist/workspace/index.d.ts +1995 -0
- package/dist/workspace/index.js +9711 -0
- package/dist/workspace/index.js.map +1 -0
- package/package.json +97 -0
- package/schema/editor-snapshot.schema.json +1138 -0
- package/schema/policies.schema.json +148 -0
- package/schema/service-props.schema.json +772 -0
|
@@ -0,0 +1,2933 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core/index.ts
|
|
21
|
+
var core_exports = {};
|
|
22
|
+
__export(core_exports, {
|
|
23
|
+
collectFailedFallbacks: () => collectFailedFallbacks,
|
|
24
|
+
createBuilder: () => createBuilder,
|
|
25
|
+
createNodeIndex: () => createNodeIndex,
|
|
26
|
+
getEligibleFallbacks: () => getEligibleFallbacks,
|
|
27
|
+
normalise: () => normalise,
|
|
28
|
+
resolveServiceFallback: () => resolveServiceFallback,
|
|
29
|
+
validate: () => validate,
|
|
30
|
+
validateRateCoherenceDeep: () => validateRateCoherenceDeep
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(core_exports);
|
|
33
|
+
|
|
34
|
+
// src/core/normalise.ts
|
|
35
|
+
var import_lodash_es = require("lodash-es");
|
|
36
|
+
function normalise(input, opts = {}) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
const defRole = (_a = opts.defaultPricingRole) != null ? _a : "base";
|
|
39
|
+
const constraints = (_b = opts.constraints) != null ? _b : ["refill", "cancel", "dripfeed"];
|
|
40
|
+
const obj = toObject(input);
|
|
41
|
+
const rawFilters = Array.isArray(obj.filters) ? obj.filters : [];
|
|
42
|
+
const rawFields = Array.isArray(obj.fields) ? obj.fields : [];
|
|
43
|
+
const includes_for_buttons = toStringArrayMap(
|
|
44
|
+
obj.includes_for_buttons
|
|
45
|
+
);
|
|
46
|
+
const excludes_for_buttons = toStringArrayMap(
|
|
47
|
+
obj.excludes_for_buttons
|
|
48
|
+
);
|
|
49
|
+
let filters = rawFilters.map((t) => coerceTag(t, constraints));
|
|
50
|
+
const fields = rawFields.map((f) => coerceField(f, defRole));
|
|
51
|
+
if (!filters.some((t) => t.id === "t:root")) {
|
|
52
|
+
filters = [{ id: "t:root", label: "Root" }, ...filters];
|
|
53
|
+
}
|
|
54
|
+
const fallbacks = coerceFallbacks(obj.fallbacks);
|
|
55
|
+
const out = {
|
|
56
|
+
filters,
|
|
57
|
+
fields,
|
|
58
|
+
order_for_tags: obj.order_for_tags,
|
|
59
|
+
...isNonEmpty(includes_for_buttons) && { includes_for_buttons },
|
|
60
|
+
...isNonEmpty(excludes_for_buttons) && { excludes_for_buttons },
|
|
61
|
+
...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
|
|
62
|
+
fallbacks
|
|
63
|
+
},
|
|
64
|
+
schema_version: typeof obj.schema_version === "string" ? obj.schema_version : "1.0"
|
|
65
|
+
};
|
|
66
|
+
propagateConstraints(out, constraints);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function propagateConstraints(props, flagKeys) {
|
|
70
|
+
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
71
|
+
if (!tags.length) return;
|
|
72
|
+
const byId = new Map(tags.map((t) => [t.id, t]));
|
|
73
|
+
const children = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const t of tags) {
|
|
75
|
+
const pid = t.bind_id;
|
|
76
|
+
if (!pid || !byId.has(pid)) continue;
|
|
77
|
+
if (!children.has(pid)) children.set(pid, []);
|
|
78
|
+
children.get(pid).push(t);
|
|
79
|
+
}
|
|
80
|
+
const roots = tags.filter((t) => !t.bind_id || !byId.has(t.bind_id));
|
|
81
|
+
const starts = roots.length ? roots : tags;
|
|
82
|
+
const visited = /* @__PURE__ */ new Set();
|
|
83
|
+
const visit = (tag, inherited) => {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
if (visited.has(tag.id)) return;
|
|
86
|
+
visited.add(tag.id);
|
|
87
|
+
const local = (0, import_lodash_es.cloneDeep)((_a = tag.constraints) != null ? _a : {});
|
|
88
|
+
if (tag.constraints_overrides) {
|
|
89
|
+
for (const [k, over] of Object.entries(tag.constraints_overrides)) {
|
|
90
|
+
if (over) local[k] = over.from;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const next = {};
|
|
94
|
+
const origin = {};
|
|
95
|
+
const overrides = {};
|
|
96
|
+
for (const k of flagKeys) {
|
|
97
|
+
const inh = inherited[k];
|
|
98
|
+
const prev = local[k];
|
|
99
|
+
if (inh) {
|
|
100
|
+
if (prev === void 0 || prev === inh.val) {
|
|
101
|
+
next[k] = inh.val;
|
|
102
|
+
origin[k] = inh.origin;
|
|
103
|
+
} else {
|
|
104
|
+
next[k] = inh.val;
|
|
105
|
+
origin[k] = inh.origin;
|
|
106
|
+
overrides[k] = {
|
|
107
|
+
from: prev,
|
|
108
|
+
to: inh.val,
|
|
109
|
+
origin: inh.origin
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
} else if (prev !== void 0) {
|
|
113
|
+
next[k] = prev;
|
|
114
|
+
origin[k] = tag.id;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
tag.constraints = Object.keys(next).length ? next : void 0;
|
|
118
|
+
tag.constraints_origin = Object.keys(origin).length ? origin : void 0;
|
|
119
|
+
tag.constraints_overrides = Object.keys(overrides).length ? overrides : void 0;
|
|
120
|
+
const passDown = { ...inherited };
|
|
121
|
+
for (const k of flagKeys) {
|
|
122
|
+
if (next[k] !== void 0 && origin[k] !== void 0) {
|
|
123
|
+
passDown[k] = { val: next[k], origin: origin[k] };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const c of (_b = children.get(tag.id)) != null ? _b : []) visit(c, passDown);
|
|
127
|
+
};
|
|
128
|
+
for (const r of starts) visit(r, {});
|
|
129
|
+
}
|
|
130
|
+
function coerceTag(src, flagKeys) {
|
|
131
|
+
if (!src || typeof src !== "object") src = {};
|
|
132
|
+
const id = str(src.id);
|
|
133
|
+
const label = str(src.label);
|
|
134
|
+
const bind_id = str(src.bind_id) || (id == "t:root" ? void 0 : "t:root");
|
|
135
|
+
const service_id = toNumberOrUndefined(src.service_id);
|
|
136
|
+
const includes = toStringArray(src.includes);
|
|
137
|
+
const excludes = toStringArray(src.excludes);
|
|
138
|
+
let constraints = void 0;
|
|
139
|
+
if (src.constraints && typeof src.constraints === "object") {
|
|
140
|
+
constraints = {};
|
|
141
|
+
for (const k of flagKeys) {
|
|
142
|
+
const v = src.constraints[k];
|
|
143
|
+
if (v !== void 0) {
|
|
144
|
+
constraints[k] = bool(v);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(constraints).length === 0) {
|
|
148
|
+
constraints = void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const constraints_overrides = src.constraints_overrides && typeof src.constraints_overrides === "object" ? src.constraints_overrides : void 0;
|
|
152
|
+
const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
|
|
153
|
+
const tag = {
|
|
154
|
+
id: "",
|
|
155
|
+
label: "",
|
|
156
|
+
...id && { id },
|
|
157
|
+
...label && { label },
|
|
158
|
+
...bind_id && { bind_id },
|
|
159
|
+
...service_id !== void 0 && { service_id },
|
|
160
|
+
...constraints && { constraints },
|
|
161
|
+
...constraints_overrides && { constraints_overrides },
|
|
162
|
+
...includes.length && { includes: dedupe(includes) },
|
|
163
|
+
...excludes.length && { excludes: dedupe(excludes) },
|
|
164
|
+
...meta && { meta }
|
|
165
|
+
};
|
|
166
|
+
return tag;
|
|
167
|
+
}
|
|
168
|
+
function coerceField(src, defRole) {
|
|
169
|
+
if (!src || typeof src !== "object") src = {};
|
|
170
|
+
const bind_id = normaliseBindId(src.bind_id);
|
|
171
|
+
const type = str(src.type) || "text";
|
|
172
|
+
const id = str(src.id);
|
|
173
|
+
const name = typeof src.name === "string" ? src.name : void 0;
|
|
174
|
+
const label = str(src.label) || "";
|
|
175
|
+
const required = !!src.required;
|
|
176
|
+
const ui = src.ui && typeof src.ui === "object" ? src.ui : void 0;
|
|
177
|
+
const defaults = src.defaults && typeof src.defaults === "object" ? src.defaults : void 0;
|
|
178
|
+
const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : defRole;
|
|
179
|
+
const srcHasOptions = Array.isArray(src.options) && src.options.length > 0;
|
|
180
|
+
const options = srcHasOptions ? src.options.map((o) => coerceOption(o, pricing_role)) : void 0;
|
|
181
|
+
const component = type === "custom" ? str(src.component) || void 0 : void 0;
|
|
182
|
+
const meta = src.meta && typeof src.meta === "object" ? { ...src.meta } : void 0;
|
|
183
|
+
const button = srcHasOptions ? true : src.button === true;
|
|
184
|
+
const field_service_id_raw = toNumberOrUndefined(src.service_id);
|
|
185
|
+
const field_service_id = button && pricing_role !== "utility" && field_service_id_raw !== void 0 ? field_service_id_raw : void 0;
|
|
186
|
+
const field = {
|
|
187
|
+
id,
|
|
188
|
+
type,
|
|
189
|
+
...bind_id !== void 0 && { bind_id },
|
|
190
|
+
...name && { name },
|
|
191
|
+
...options && options.length && { options },
|
|
192
|
+
...component && { component },
|
|
193
|
+
pricing_role,
|
|
194
|
+
label,
|
|
195
|
+
required,
|
|
196
|
+
...ui && { ui },
|
|
197
|
+
...defaults && { defaults },
|
|
198
|
+
...meta && { meta },
|
|
199
|
+
...button ? { button } : {},
|
|
200
|
+
...field_service_id !== void 0 && { service_id: field_service_id }
|
|
201
|
+
};
|
|
202
|
+
return field;
|
|
203
|
+
}
|
|
204
|
+
function coerceOption(src, inheritRole) {
|
|
205
|
+
if (!src || typeof src !== "object") src = {};
|
|
206
|
+
const id = str(src.id);
|
|
207
|
+
const label = str(src.label);
|
|
208
|
+
const service_id = toNumberOrUndefined(src.service_id);
|
|
209
|
+
const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
|
|
210
|
+
const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
|
|
211
|
+
const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
|
|
212
|
+
const option = {
|
|
213
|
+
id: "",
|
|
214
|
+
label: "",
|
|
215
|
+
...id && { id },
|
|
216
|
+
...label && { label },
|
|
217
|
+
...value !== void 0 && { value },
|
|
218
|
+
...service_id !== void 0 && { service_id },
|
|
219
|
+
pricing_role,
|
|
220
|
+
...meta && { meta }
|
|
221
|
+
};
|
|
222
|
+
return option;
|
|
223
|
+
}
|
|
224
|
+
function coerceFallbacks(src) {
|
|
225
|
+
if (!src || typeof src !== "object") return void 0;
|
|
226
|
+
const out = {};
|
|
227
|
+
const g = src.global;
|
|
228
|
+
const n = src.nodes;
|
|
229
|
+
if (g && typeof g === "object") {
|
|
230
|
+
const rg = {};
|
|
231
|
+
for (const [k, v] of Object.entries(g)) {
|
|
232
|
+
const key = String(k);
|
|
233
|
+
const arr = toServiceIdArray(v);
|
|
234
|
+
const clean = dedupe(arr.filter((x) => String(x) !== key));
|
|
235
|
+
if (clean.length) rg[key] = clean;
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(rg).length) out.global = rg;
|
|
238
|
+
}
|
|
239
|
+
if (n && typeof n === "object") {
|
|
240
|
+
const rn = {};
|
|
241
|
+
for (const [nodeId, v] of Object.entries(n)) {
|
|
242
|
+
const key = String(nodeId);
|
|
243
|
+
const arr = toServiceIdArray(v);
|
|
244
|
+
const clean = dedupe(arr.filter((x) => String(x) !== key));
|
|
245
|
+
if (clean.length) rn[key] = clean;
|
|
246
|
+
}
|
|
247
|
+
if (Object.keys(rn).length) out.nodes = rn;
|
|
248
|
+
}
|
|
249
|
+
return out.nodes || out.global ? out : void 0;
|
|
250
|
+
}
|
|
251
|
+
function toObject(input) {
|
|
252
|
+
if (input && typeof input === "object")
|
|
253
|
+
return input;
|
|
254
|
+
throw new TypeError("normalise(): expected an object payload");
|
|
255
|
+
}
|
|
256
|
+
function normaliseBindId(bind) {
|
|
257
|
+
if (typeof bind === "string" && bind.trim()) return bind.trim();
|
|
258
|
+
if (Array.isArray(bind)) {
|
|
259
|
+
const arr = dedupe(bind.map((b) => String(b).trim()).filter(Boolean));
|
|
260
|
+
if (arr.length === 0) return void 0;
|
|
261
|
+
if (arr.length === 1) return arr[0];
|
|
262
|
+
return arr;
|
|
263
|
+
}
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
function toStringArrayMap(src) {
|
|
267
|
+
if (!src || typeof src !== "object") return void 0;
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const [k, v] of Object.entries(src)) {
|
|
270
|
+
if (!k) continue;
|
|
271
|
+
const arr = toStringArray(v);
|
|
272
|
+
if (arr.length) out[k] = dedupe(arr);
|
|
273
|
+
}
|
|
274
|
+
return Object.keys(out).length ? out : void 0;
|
|
275
|
+
}
|
|
276
|
+
function toStringArray(v) {
|
|
277
|
+
if (!Array.isArray(v)) return [];
|
|
278
|
+
return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
|
|
279
|
+
}
|
|
280
|
+
function toNumberOrUndefined(v) {
|
|
281
|
+
if (v === null || v === void 0) return void 0;
|
|
282
|
+
const n = Number(v);
|
|
283
|
+
return Number.isFinite(n) ? n : void 0;
|
|
284
|
+
}
|
|
285
|
+
function str(v) {
|
|
286
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
function bool(v) {
|
|
290
|
+
if (v === void 0) return void 0;
|
|
291
|
+
return !!v;
|
|
292
|
+
}
|
|
293
|
+
function dedupe(arr) {
|
|
294
|
+
return Array.from(new Set(arr));
|
|
295
|
+
}
|
|
296
|
+
function isNonEmpty(obj) {
|
|
297
|
+
return !!obj && Object.keys(obj).length > 0;
|
|
298
|
+
}
|
|
299
|
+
function toServiceIdArray(v) {
|
|
300
|
+
if (!Array.isArray(v)) return [];
|
|
301
|
+
return v.map(
|
|
302
|
+
(x) => typeof x === "number" || typeof x === "string" ? x : String(x)
|
|
303
|
+
).filter(
|
|
304
|
+
(x) => x !== "" && x !== null && x !== void 0
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/core/validate/shared.ts
|
|
309
|
+
function isFiniteNumber(v) {
|
|
310
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
311
|
+
}
|
|
312
|
+
function hasAnyServiceOption(f) {
|
|
313
|
+
var _a;
|
|
314
|
+
return ((_a = f.options) != null ? _a : []).some((o) => isFiniteNumber(o.service_id));
|
|
315
|
+
}
|
|
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
|
+
function getByPath(obj, path) {
|
|
322
|
+
if (!path) return void 0;
|
|
323
|
+
const parts = path.split(".");
|
|
324
|
+
let cur = obj;
|
|
325
|
+
for (const p of parts) {
|
|
326
|
+
if (cur == null) return void 0;
|
|
327
|
+
cur = cur[p];
|
|
328
|
+
}
|
|
329
|
+
return cur;
|
|
330
|
+
}
|
|
331
|
+
function jsonStable(v) {
|
|
332
|
+
try {
|
|
333
|
+
return JSON.stringify(v);
|
|
334
|
+
} catch {
|
|
335
|
+
return String(v);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function eqValue(a, b) {
|
|
339
|
+
if (Object.is(a, b)) return true;
|
|
340
|
+
return jsonStable(a) === jsonStable(b);
|
|
341
|
+
}
|
|
342
|
+
function includesValue(arr, needle) {
|
|
343
|
+
for (const v of arr) {
|
|
344
|
+
if (eqValue(v, needle)) return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
function matchesWhere(svc, where) {
|
|
349
|
+
var _a;
|
|
350
|
+
if (!where || where.length === 0) return true;
|
|
351
|
+
const root = { service: svc };
|
|
352
|
+
for (const clause of where) {
|
|
353
|
+
const path = clause.path;
|
|
354
|
+
const op = (_a = clause.op) != null ? _a : "eq";
|
|
355
|
+
const value = clause.value;
|
|
356
|
+
const cur = getByPath(root, path);
|
|
357
|
+
if (op === "exists") {
|
|
358
|
+
if (cur === void 0 || cur === null) return false;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (op === "truthy") {
|
|
362
|
+
if (!cur) return false;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (op === "falsy") {
|
|
366
|
+
if (cur) return false;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (op === "in" || op === "nin") {
|
|
370
|
+
const list = Array.isArray(value) ? value : [];
|
|
371
|
+
const hit = includesValue(list, cur);
|
|
372
|
+
if (op === "in" && !hit) return false;
|
|
373
|
+
if (op === "nin" && hit) return false;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (op === "neq") {
|
|
377
|
+
if (eqValue(cur, value)) return false;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (op === "sw") {
|
|
381
|
+
const s = String(cur != null ? cur : "");
|
|
382
|
+
const prefix = String(value != null ? value : "");
|
|
383
|
+
if (!s.startsWith(prefix)) return false;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!eqValue(cur, value)) return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
function serviceFlagState(svc, flagId) {
|
|
391
|
+
const flags = svc.flags;
|
|
392
|
+
const entry = flags && typeof flags === "object" ? flags[flagId] : void 0;
|
|
393
|
+
const enabled = entry && typeof entry === "object" ? entry.enabled : void 0;
|
|
394
|
+
if (enabled === true) return true;
|
|
395
|
+
if (enabled === false) return false;
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
function isServiceFlagEnabled(svc, flagId) {
|
|
399
|
+
return serviceFlagState(svc, flagId) === true;
|
|
400
|
+
}
|
|
401
|
+
function withAffected(details, ids) {
|
|
402
|
+
if (!ids || ids.length <= 1) return details;
|
|
403
|
+
return { ...details != null ? details : {}, affectedIds: ids };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/core/validate/steps/visibility.ts
|
|
407
|
+
function createFieldsVisibleUnder(v) {
|
|
408
|
+
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());
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function validateVisibility(v) {
|
|
433
|
+
var _a, _b, _c, _d, _e;
|
|
434
|
+
for (const t of v.tags) {
|
|
435
|
+
const visible = v.fieldsVisibleUnder(t.id);
|
|
436
|
+
const seen = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const f of visible) {
|
|
438
|
+
const label = ((_a = f.label) != null ? _a : "").trim();
|
|
439
|
+
if (!label) continue;
|
|
440
|
+
if (seen.has(label)) {
|
|
441
|
+
const otherId = seen.get(label);
|
|
442
|
+
v.errors.push({
|
|
443
|
+
code: "duplicate_visible_label",
|
|
444
|
+
severity: "error",
|
|
445
|
+
message: `Duplicate visible label "${label}" under tag "${t.id}".`,
|
|
446
|
+
nodeId: f.id,
|
|
447
|
+
details: withAffected(
|
|
448
|
+
{ tagId: t.id, other: otherId, label },
|
|
449
|
+
otherId ? [t.id, f.id, otherId] : [t.id, f.id]
|
|
450
|
+
)
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
seen.set(label, f.id);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const t of v.tags) {
|
|
458
|
+
const visible = v.fieldsVisibleUnder(t.id);
|
|
459
|
+
const markers = [];
|
|
460
|
+
for (const f of visible) {
|
|
461
|
+
const q = (_b = f.meta) == null ? void 0 : _b.quantity;
|
|
462
|
+
if (q) markers.push(f.id);
|
|
463
|
+
}
|
|
464
|
+
if (markers.length > 1) {
|
|
465
|
+
v.errors.push({
|
|
466
|
+
code: "quantity_multiple_markers",
|
|
467
|
+
severity: "error",
|
|
468
|
+
message: `Multiple quantity markers found under tag "${t.id}". Only one is allowed per visible group.`,
|
|
469
|
+
nodeId: t.id,
|
|
470
|
+
details: withAffected({ tagId: t.id, markers }, [
|
|
471
|
+
t.id,
|
|
472
|
+
...markers
|
|
473
|
+
])
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const t of v.tags) {
|
|
478
|
+
const visible = v.fieldsVisibleUnder(t.id);
|
|
479
|
+
let hasBase = false;
|
|
480
|
+
let hasUtility = false;
|
|
481
|
+
const utilityOptionIds = [];
|
|
482
|
+
for (const f of visible) {
|
|
483
|
+
for (const o of (_c = f.options) != null ? _c : []) {
|
|
484
|
+
if (!isFiniteNumber(o.service_id)) continue;
|
|
485
|
+
const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
|
|
486
|
+
if (role === "base") hasBase = true;
|
|
487
|
+
else if (role === "utility") {
|
|
488
|
+
hasUtility = true;
|
|
489
|
+
utilityOptionIds.push(o.id);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (hasUtility && !hasBase) {
|
|
494
|
+
v.errors.push({
|
|
495
|
+
code: "utility_without_base",
|
|
496
|
+
severity: "error",
|
|
497
|
+
message: `Utility-priced options exist under tag "${t.id}" but no base-priced options were found in the same visible group.`,
|
|
498
|
+
nodeId: t.id,
|
|
499
|
+
details: withAffected({ tagId: t.id, utilityOptionIds }, [
|
|
500
|
+
t.id,
|
|
501
|
+
...utilityOptionIds
|
|
502
|
+
])
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/core/validate/steps/structure.ts
|
|
509
|
+
function validateStructure(v) {
|
|
510
|
+
const tags = v.tags;
|
|
511
|
+
const fields = v.fields;
|
|
512
|
+
if (!tags.some((t) => t.id === "root")) {
|
|
513
|
+
v.errors.push({
|
|
514
|
+
code: "root_missing",
|
|
515
|
+
severity: "error",
|
|
516
|
+
message: 'Missing required root tag with id "root".'
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
520
|
+
const visited = /* @__PURE__ */ new Set();
|
|
521
|
+
const hasCycleFrom = (id) => {
|
|
522
|
+
var _a;
|
|
523
|
+
if (visiting.has(id)) return true;
|
|
524
|
+
if (visited.has(id)) return false;
|
|
525
|
+
visiting.add(id);
|
|
526
|
+
const parent = (_a = v.tagById.get(id)) == null ? void 0 : _a.bind_id;
|
|
527
|
+
if (parent && v.tagById.has(parent) && hasCycleFrom(parent))
|
|
528
|
+
return true;
|
|
529
|
+
visiting.delete(id);
|
|
530
|
+
visited.add(id);
|
|
531
|
+
return false;
|
|
532
|
+
};
|
|
533
|
+
for (const t of tags) {
|
|
534
|
+
if (hasCycleFrom(t.id)) {
|
|
535
|
+
v.errors.push({
|
|
536
|
+
code: "cycle_in_tags",
|
|
537
|
+
severity: "error",
|
|
538
|
+
message: `Cycle detected in tag parentage starting at tag "${t.id}".`,
|
|
539
|
+
nodeId: t.id,
|
|
540
|
+
details: { tagId: t.id }
|
|
541
|
+
});
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const t of tags) {
|
|
546
|
+
if (t.bind_id && !v.tagById.has(t.bind_id)) {
|
|
547
|
+
v.errors.push({
|
|
548
|
+
code: "bad_bind_reference",
|
|
549
|
+
severity: "error",
|
|
550
|
+
message: `Tag "${t.id}" binds to missing parent tag "${t.bind_id}".`,
|
|
551
|
+
nodeId: t.id,
|
|
552
|
+
details: withAffected({ ref: t.bind_id }, [t.id])
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const f of fields) {
|
|
557
|
+
const b = f.bind_id;
|
|
558
|
+
if (Array.isArray(b)) {
|
|
559
|
+
for (const id of b) {
|
|
560
|
+
if (!v.tagById.has(id)) {
|
|
561
|
+
v.errors.push({
|
|
562
|
+
code: "bad_bind_reference",
|
|
563
|
+
severity: "error",
|
|
564
|
+
message: `Field "${f.id}" binds to missing tag "${id}".`,
|
|
565
|
+
nodeId: f.id,
|
|
566
|
+
details: withAffected({ ref: id }, [f.id])
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} else if (typeof b === "string") {
|
|
571
|
+
if (!v.tagById.has(b)) {
|
|
572
|
+
v.errors.push({
|
|
573
|
+
code: "bad_bind_reference",
|
|
574
|
+
severity: "error",
|
|
575
|
+
message: `Field "${f.id}" binds to missing tag "${b}".`,
|
|
576
|
+
nodeId: f.id,
|
|
577
|
+
details: withAffected({ ref: b }, [f.id])
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
void isFiniteNumber;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/core/validate/steps/identity.ts
|
|
586
|
+
function validateIdentity(v) {
|
|
587
|
+
var _a, _b;
|
|
588
|
+
const tags = v.tags;
|
|
589
|
+
const fields = v.fields;
|
|
590
|
+
{
|
|
591
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const t of tags) {
|
|
594
|
+
if (seen.has(t.id)) {
|
|
595
|
+
v.errors.push({
|
|
596
|
+
code: "duplicate_id",
|
|
597
|
+
severity: "error",
|
|
598
|
+
message: `Duplicate id "${t.id}" found (tag).`,
|
|
599
|
+
nodeId: t.id
|
|
600
|
+
// we only know the id itself; no other id to point at
|
|
601
|
+
});
|
|
602
|
+
} else {
|
|
603
|
+
seen.add(t.id);
|
|
604
|
+
firstSeen.set(t.id, "tag");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const f of fields) {
|
|
608
|
+
if (seen.has(f.id)) {
|
|
609
|
+
const kind = (_a = firstSeen.get(f.id)) != null ? _a : "tag/field";
|
|
610
|
+
v.errors.push({
|
|
611
|
+
code: "duplicate_id",
|
|
612
|
+
severity: "error",
|
|
613
|
+
message: `Duplicate id "${f.id}" found (field) \u2014 already used by a ${kind}.`,
|
|
614
|
+
nodeId: f.id
|
|
615
|
+
});
|
|
616
|
+
} else {
|
|
617
|
+
seen.add(f.id);
|
|
618
|
+
firstSeen.set(f.id, "field");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
{
|
|
623
|
+
const seen = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const t of tags) {
|
|
625
|
+
if (!t.label || !t.label.trim()) {
|
|
626
|
+
v.errors.push({
|
|
627
|
+
code: "label_missing",
|
|
628
|
+
severity: "error",
|
|
629
|
+
message: `Tag "${t.id}" is missing a label.`,
|
|
630
|
+
nodeId: t.id,
|
|
631
|
+
details: { kind: "tag" }
|
|
632
|
+
});
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const k = t.label;
|
|
636
|
+
if (seen.has(k)) {
|
|
637
|
+
const otherId = seen.get(k);
|
|
638
|
+
v.errors.push({
|
|
639
|
+
code: "duplicate_tag_label",
|
|
640
|
+
severity: "error",
|
|
641
|
+
message: `Duplicate tag label "${k}" found on tag "${t.id}".`,
|
|
642
|
+
nodeId: t.id,
|
|
643
|
+
details: withAffected(
|
|
644
|
+
{ other: otherId, label: k },
|
|
645
|
+
otherId ? [t.id, otherId] : void 0
|
|
646
|
+
)
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
seen.set(k, t.id);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
{
|
|
654
|
+
const seenNames = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const f of fields) {
|
|
656
|
+
if (!f.label || !f.label.trim()) {
|
|
657
|
+
v.errors.push({
|
|
658
|
+
code: "label_missing",
|
|
659
|
+
severity: "error",
|
|
660
|
+
message: `Field "${f.id}" is missing a label.`,
|
|
661
|
+
nodeId: f.id,
|
|
662
|
+
details: { kind: "field" }
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const isUserInput = !!f.name && !hasAnyServiceOption(f);
|
|
666
|
+
if (isUserInput && f.name) {
|
|
667
|
+
const k = f.name;
|
|
668
|
+
if (seenNames.has(k)) {
|
|
669
|
+
const otherId = seenNames.get(k);
|
|
670
|
+
v.errors.push({
|
|
671
|
+
code: "duplicate_field_name",
|
|
672
|
+
severity: "error",
|
|
673
|
+
message: `Duplicate field name "${k}" found on field "${f.id}".`,
|
|
674
|
+
nodeId: f.id,
|
|
675
|
+
details: withAffected(
|
|
676
|
+
{ other: otherId, name: k },
|
|
677
|
+
otherId ? [f.id, otherId] : void 0
|
|
678
|
+
)
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
seenNames.set(k, f.id);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const f of fields) {
|
|
687
|
+
for (const o of (_b = f.options) != null ? _b : []) {
|
|
688
|
+
if (!o.label || !o.label.trim()) {
|
|
689
|
+
v.errors.push({
|
|
690
|
+
code: "label_missing",
|
|
691
|
+
severity: "error",
|
|
692
|
+
message: `Option "${o.id}" (field "${f.id}") is missing a label.`,
|
|
693
|
+
nodeId: o.id,
|
|
694
|
+
details: { kind: "option", fieldId: f.id }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/core/validate/steps/option-maps.ts
|
|
702
|
+
function validateOptionMaps(v) {
|
|
703
|
+
var _a, _b;
|
|
704
|
+
const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
|
|
705
|
+
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);
|
|
718
|
+
};
|
|
719
|
+
const badKeyMessage = (key) => `Invalid option-map key "${key}". Expected "fieldId::optionId" pointing to an existing option.`;
|
|
720
|
+
for (const k of Object.keys(incMap)) {
|
|
721
|
+
const p = parseKey(k);
|
|
722
|
+
if (!p || !hasOption(p.fieldId, p.optionId)) {
|
|
723
|
+
v.errors.push({
|
|
724
|
+
code: "bad_option_key",
|
|
725
|
+
severity: "error",
|
|
726
|
+
message: badKeyMessage(k),
|
|
727
|
+
details: { key: k }
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const k of Object.keys(excMap)) {
|
|
732
|
+
const p = parseKey(k);
|
|
733
|
+
if (!p || !hasOption(p.fieldId, p.optionId)) {
|
|
734
|
+
v.errors.push({
|
|
735
|
+
code: "bad_option_key",
|
|
736
|
+
severity: "error",
|
|
737
|
+
message: badKeyMessage(k),
|
|
738
|
+
details: { key: k }
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
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
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/core/validate/steps/service-vs-input.ts
|
|
758
|
+
function validateServiceVsUserInput(v) {
|
|
759
|
+
for (const f of v.fields) {
|
|
760
|
+
const anySvc = hasAnyServiceOption(f);
|
|
761
|
+
const hasName = !!(f.name && f.name.trim());
|
|
762
|
+
if (f.type === "custom" && anySvc) {
|
|
763
|
+
v.errors.push({
|
|
764
|
+
code: "user_input_field_has_service_option",
|
|
765
|
+
severity: "error",
|
|
766
|
+
message: `Custom field "${f.id}" cannot map service options.`,
|
|
767
|
+
nodeId: f.id,
|
|
768
|
+
details: { reason: "custom_cannot_map_service" }
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (!hasName) {
|
|
772
|
+
if (!anySvc) {
|
|
773
|
+
v.errors.push({
|
|
774
|
+
code: "service_field_missing_service_id",
|
|
775
|
+
severity: "error",
|
|
776
|
+
message: `Service-backed field "${f.id}" has no "name" and must provide at least one option with a service_id.`,
|
|
777
|
+
nodeId: f.id
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
if (anySvc) {
|
|
782
|
+
v.errors.push({
|
|
783
|
+
code: "user_input_field_has_service_option",
|
|
784
|
+
severity: "error",
|
|
785
|
+
message: `User-input field "${f.id}" has a name and must not include any options with service_id.`,
|
|
786
|
+
nodeId: f.id
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/core/validate/steps/utility.ts
|
|
794
|
+
function validateUtilityMarkers(v) {
|
|
795
|
+
var _a, _b, _c, _d;
|
|
796
|
+
const ALLOWED_UTILITY_MODES = /* @__PURE__ */ new Set([
|
|
797
|
+
"flat",
|
|
798
|
+
"per_quantity",
|
|
799
|
+
"per_value",
|
|
800
|
+
"percent"
|
|
801
|
+
]);
|
|
802
|
+
for (const f of v.fields) {
|
|
803
|
+
const optsArr = Array.isArray(f.options) ? f.options : [];
|
|
804
|
+
for (const o of optsArr) {
|
|
805
|
+
const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
|
|
806
|
+
const hasService = isFiniteNumber(o.service_id);
|
|
807
|
+
const util = (_c = o.meta) == null ? void 0 : _c.utility;
|
|
808
|
+
if (role === "utility" && hasService) {
|
|
809
|
+
v.errors.push({
|
|
810
|
+
code: "utility_with_service_id",
|
|
811
|
+
severity: "error",
|
|
812
|
+
message: `Utility-priced option "${o.id}" (field "${f.id}") must not reference a service_id.`,
|
|
813
|
+
nodeId: o.id,
|
|
814
|
+
details: withAffected(
|
|
815
|
+
{
|
|
816
|
+
fieldId: f.id,
|
|
817
|
+
optionId: o.id,
|
|
818
|
+
service_id: o.service_id
|
|
819
|
+
},
|
|
820
|
+
[f.id, o.id]
|
|
821
|
+
)
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (util) {
|
|
825
|
+
const mode = util.mode;
|
|
826
|
+
const rate = util.rate;
|
|
827
|
+
if (!isFiniteNumber(rate)) {
|
|
828
|
+
v.errors.push({
|
|
829
|
+
code: "utility_missing_rate",
|
|
830
|
+
severity: "error",
|
|
831
|
+
message: `Utility definition for option "${o.id}" (field "${f.id}") is missing a valid rate.`,
|
|
832
|
+
nodeId: o.id,
|
|
833
|
+
details: withAffected(
|
|
834
|
+
{ fieldId: f.id, optionId: o.id },
|
|
835
|
+
[f.id, o.id]
|
|
836
|
+
)
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (!ALLOWED_UTILITY_MODES.has(String(mode))) {
|
|
840
|
+
v.errors.push({
|
|
841
|
+
code: "utility_invalid_mode",
|
|
842
|
+
severity: "error",
|
|
843
|
+
message: `Utility definition for option "${o.id}" (field "${f.id}") has invalid mode "${String(
|
|
844
|
+
mode
|
|
845
|
+
)}".`,
|
|
846
|
+
nodeId: o.id,
|
|
847
|
+
details: withAffected(
|
|
848
|
+
{ fieldId: f.id, optionId: o.id, mode },
|
|
849
|
+
[f.id, o.id]
|
|
850
|
+
)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (const f of v.fields) {
|
|
857
|
+
const util = (_d = f.meta) == null ? void 0 : _d.utility;
|
|
858
|
+
if (!util) continue;
|
|
859
|
+
const mode = util.mode;
|
|
860
|
+
const rate = util.rate;
|
|
861
|
+
if (!isFiniteNumber(rate)) {
|
|
862
|
+
v.errors.push({
|
|
863
|
+
code: "utility_missing_rate",
|
|
864
|
+
severity: "error",
|
|
865
|
+
message: `Utility definition for field "${f.id}" is missing a valid rate.`,
|
|
866
|
+
nodeId: f.id,
|
|
867
|
+
details: withAffected({ fieldId: f.id }, [f.id])
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
if (!ALLOWED_UTILITY_MODES.has(String(mode))) {
|
|
871
|
+
v.errors.push({
|
|
872
|
+
code: "utility_invalid_mode",
|
|
873
|
+
severity: "error",
|
|
874
|
+
message: `Utility definition for field "${f.id}" has invalid mode "${String(
|
|
875
|
+
mode
|
|
876
|
+
)}".`,
|
|
877
|
+
nodeId: f.id,
|
|
878
|
+
details: withAffected({ fieldId: f.id, mode }, [f.id])
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/utils/index.ts
|
|
885
|
+
function isMultiField(f) {
|
|
886
|
+
var _a;
|
|
887
|
+
const t = (f.type || "").toLowerCase();
|
|
888
|
+
const metaMulti = !!((_a = f.meta) == null ? void 0 : _a.multi);
|
|
889
|
+
return t === "multiselect" || t === "checkbox" || metaMulti;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/core/validate/steps/rates.ts
|
|
893
|
+
function validateRates(v) {
|
|
894
|
+
var _a, _b, _c, _d;
|
|
895
|
+
for (const f of v.fields) {
|
|
896
|
+
if (!isMultiField(f)) continue;
|
|
897
|
+
const baseRates = /* @__PURE__ */ new Set();
|
|
898
|
+
const contributingOptionIds = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const o of (_a = f.options) != null ? _a : []) {
|
|
900
|
+
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
901
|
+
if (role !== "base") continue;
|
|
902
|
+
const sid = o.service_id;
|
|
903
|
+
if (!isFiniteNumber(sid)) continue;
|
|
904
|
+
const rate = (_d = v.serviceMap[sid]) == null ? void 0 : _d.rate;
|
|
905
|
+
if (isFiniteNumber(rate)) {
|
|
906
|
+
baseRates.add(Number(rate));
|
|
907
|
+
contributingOptionIds.add(o.id);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (baseRates.size > 1) {
|
|
911
|
+
const affectedIds = [
|
|
912
|
+
f.id,
|
|
913
|
+
...Array.from(contributingOptionIds)
|
|
914
|
+
];
|
|
915
|
+
v.errors.push({
|
|
916
|
+
code: "rate_mismatch_across_base",
|
|
917
|
+
severity: "error",
|
|
918
|
+
message: `Base options under field "${f.id}" resolve to different service rates.`,
|
|
919
|
+
nodeId: f.id,
|
|
920
|
+
details: withAffected(
|
|
921
|
+
{
|
|
922
|
+
fieldId: f.id,
|
|
923
|
+
rates: Array.from(baseRates.values()),
|
|
924
|
+
optionIds: Array.from(contributingOptionIds.values())
|
|
925
|
+
},
|
|
926
|
+
affectedIds.length > 1 ? affectedIds : void 0
|
|
927
|
+
)
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/core/validate/steps/constraints.ts
|
|
934
|
+
function constraintKeysInChain(v, tagId) {
|
|
935
|
+
const keys = [];
|
|
936
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
937
|
+
let cur = tagId;
|
|
938
|
+
const seenTags = /* @__PURE__ */ new Set();
|
|
939
|
+
while (cur && !seenTags.has(cur)) {
|
|
940
|
+
seenTags.add(cur);
|
|
941
|
+
const t = v.tagById.get(cur);
|
|
942
|
+
const c = t == null ? void 0 : t.constraints;
|
|
943
|
+
if (c && typeof c === "object") {
|
|
944
|
+
for (const k of Object.keys(c)) {
|
|
945
|
+
if (!seenKeys.has(k)) {
|
|
946
|
+
seenKeys.add(k);
|
|
947
|
+
keys.push(k);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
cur = t == null ? void 0 : t.bind_id;
|
|
952
|
+
}
|
|
953
|
+
return keys;
|
|
954
|
+
}
|
|
955
|
+
function effectiveConstraints(v, tagId) {
|
|
956
|
+
var _a;
|
|
957
|
+
const out = {};
|
|
958
|
+
const keys = constraintKeysInChain(v, tagId);
|
|
959
|
+
for (const key of keys) {
|
|
960
|
+
let cur = tagId;
|
|
961
|
+
const seen = /* @__PURE__ */ new Set();
|
|
962
|
+
while (cur && !seen.has(cur)) {
|
|
963
|
+
seen.add(cur);
|
|
964
|
+
const t = v.tagById.get(cur);
|
|
965
|
+
const val = (_a = t == null ? void 0 : t.constraints) == null ? void 0 : _a[key];
|
|
966
|
+
if (val === true || val === false) {
|
|
967
|
+
out[key] = val;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
cur = t == null ? void 0 : t.bind_id;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return out;
|
|
974
|
+
}
|
|
975
|
+
function validateConstraints(v) {
|
|
976
|
+
var _a, _b;
|
|
977
|
+
for (const t of v.tags) {
|
|
978
|
+
const eff = effectiveConstraints(v, t.id);
|
|
979
|
+
const hasAnyRequired = Object.values(eff).some(
|
|
980
|
+
(x) => x === true
|
|
981
|
+
);
|
|
982
|
+
if (!hasAnyRequired) continue;
|
|
983
|
+
const visible = v.fieldsVisibleUnder(t.id);
|
|
984
|
+
for (const f of visible) {
|
|
985
|
+
for (const o of (_a = f.options) != null ? _a : []) {
|
|
986
|
+
if (!isFiniteNumber(o.service_id)) continue;
|
|
987
|
+
const svc = v.serviceMap[o.service_id];
|
|
988
|
+
if (!svc || typeof svc !== "object") continue;
|
|
989
|
+
for (const [k, val] of Object.entries(eff)) {
|
|
990
|
+
if (val === true && !isServiceFlagEnabled(svc, k)) {
|
|
991
|
+
v.errors.push({
|
|
992
|
+
code: "unsupported_constraint",
|
|
993
|
+
severity: "error",
|
|
994
|
+
message: `Service option "${o.id}" under tag "${t.id}" does not support required constraint "${k}".`,
|
|
995
|
+
nodeId: t.id,
|
|
996
|
+
details: withAffected(
|
|
997
|
+
{
|
|
998
|
+
flag: k,
|
|
999
|
+
serviceId: o.service_id,
|
|
1000
|
+
fieldId: f.id,
|
|
1001
|
+
optionId: o.id
|
|
1002
|
+
},
|
|
1003
|
+
[t.id, f.id, o.id]
|
|
1004
|
+
)
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
for (const t of v.tags) {
|
|
1012
|
+
const sid = t.service_id;
|
|
1013
|
+
if (!isFiniteNumber(sid)) continue;
|
|
1014
|
+
const svc = v.serviceMap[Number(sid)];
|
|
1015
|
+
if (!svc || typeof svc !== "object") continue;
|
|
1016
|
+
const eff = effectiveConstraints(v, t.id);
|
|
1017
|
+
for (const [k, val] of Object.entries(eff)) {
|
|
1018
|
+
if (val === true && !isServiceFlagEnabled(svc, k)) {
|
|
1019
|
+
v.errors.push({
|
|
1020
|
+
code: "unsupported_constraint",
|
|
1021
|
+
severity: "error",
|
|
1022
|
+
message: `Tag "${t.id}" maps to service "${String(
|
|
1023
|
+
sid
|
|
1024
|
+
)}" which does not support required constraint "${k}".`,
|
|
1025
|
+
nodeId: t.id,
|
|
1026
|
+
details: { flag: k, serviceId: sid }
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
for (const t of v.tags) {
|
|
1032
|
+
const ov = t.constraints_overrides;
|
|
1033
|
+
if (!ov || typeof ov !== "object") continue;
|
|
1034
|
+
for (const k of Object.keys(ov)) {
|
|
1035
|
+
const row = ov[k];
|
|
1036
|
+
if (!row) continue;
|
|
1037
|
+
const from = row.from === true;
|
|
1038
|
+
const to = row.to === true;
|
|
1039
|
+
const origin = String((_b = row.origin) != null ? _b : "");
|
|
1040
|
+
v.errors.push({
|
|
1041
|
+
code: "constraint_overridden",
|
|
1042
|
+
severity: "warning",
|
|
1043
|
+
message: origin ? `Constraint "${k}" on tag "${t.id}" was overridden by ancestor "${origin}" (${String(from)} \u2192 ${String(
|
|
1044
|
+
to
|
|
1045
|
+
)}).` : `Constraint "${k}" on tag "${t.id}" was overridden by an ancestor (${String(from)} \u2192 ${String(to)}).`,
|
|
1046
|
+
nodeId: t.id,
|
|
1047
|
+
details: withAffected(
|
|
1048
|
+
{ flag: k, from, to, origin },
|
|
1049
|
+
origin ? [t.id, origin] : void 0
|
|
1050
|
+
)
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/core/validate/steps/custom.ts
|
|
1057
|
+
function validateCustomFields(v) {
|
|
1058
|
+
for (const f of v.fields) {
|
|
1059
|
+
if (f.type !== "custom") continue;
|
|
1060
|
+
if (!f.component || !String(f.component).trim()) {
|
|
1061
|
+
v.errors.push({
|
|
1062
|
+
code: "custom_component_missing",
|
|
1063
|
+
severity: "error",
|
|
1064
|
+
message: `Custom field "${f.id}" is missing a valid component reference.`,
|
|
1065
|
+
nodeId: f.id
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/core/validate/steps/global-utility-guard.ts
|
|
1072
|
+
function validateGlobalUtilityGuard(v) {
|
|
1073
|
+
var _a, _b, _c;
|
|
1074
|
+
if (!v.options.globalUtilityGuard) return;
|
|
1075
|
+
let hasUtility = false;
|
|
1076
|
+
let hasBase = false;
|
|
1077
|
+
for (const f of v.fields) {
|
|
1078
|
+
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1079
|
+
if (!isFiniteNumber(o.service_id)) continue;
|
|
1080
|
+
const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
|
|
1081
|
+
if (role === "base") hasBase = true;
|
|
1082
|
+
else if (role === "utility") hasUtility = true;
|
|
1083
|
+
if (hasUtility && hasBase) break;
|
|
1084
|
+
}
|
|
1085
|
+
if (hasUtility && hasBase) break;
|
|
1086
|
+
}
|
|
1087
|
+
if (hasUtility && !hasBase) {
|
|
1088
|
+
v.errors.push({
|
|
1089
|
+
code: "utility_without_base",
|
|
1090
|
+
severity: "warning",
|
|
1091
|
+
message: "Global utility guard: utility-priced options exist but no base-priced options were found.",
|
|
1092
|
+
nodeId: "global",
|
|
1093
|
+
details: { scope: "global" }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/core/validate/steps/unbound.ts
|
|
1099
|
+
function validateUnboundFields(v) {
|
|
1100
|
+
var _a, _b;
|
|
1101
|
+
const boundFieldIds = /* @__PURE__ */ new Set();
|
|
1102
|
+
for (const f of v.fields) {
|
|
1103
|
+
if (f.bind_id) boundFieldIds.add(f.id);
|
|
1104
|
+
}
|
|
1105
|
+
const includedByTag = /* @__PURE__ */ new Set();
|
|
1106
|
+
for (const t of v.tags) {
|
|
1107
|
+
for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
|
|
1108
|
+
}
|
|
1109
|
+
const includedByOption = /* @__PURE__ */ new Set();
|
|
1110
|
+
for (const arr of Object.values((_b = v.props.includes_for_buttons) != null ? _b : {})) {
|
|
1111
|
+
for (const id of arr != null ? arr : []) includedByOption.add(id);
|
|
1112
|
+
}
|
|
1113
|
+
for (const f of v.fields) {
|
|
1114
|
+
if (!boundFieldIds.has(f.id) && !includedByTag.has(f.id) && !includedByOption.has(f.id)) {
|
|
1115
|
+
v.errors.push({
|
|
1116
|
+
code: "field_unbound",
|
|
1117
|
+
severity: "error",
|
|
1118
|
+
message: `Field "${f.id}" is unbound: it is not bound to any tag and not included by tags or option maps.`,
|
|
1119
|
+
nodeId: f.id,
|
|
1120
|
+
details: withAffected(
|
|
1121
|
+
{
|
|
1122
|
+
fieldId: f.id,
|
|
1123
|
+
bound: false,
|
|
1124
|
+
// exposing these helps editors explain "why"
|
|
1125
|
+
includedByTag: includedByTag.has(f.id),
|
|
1126
|
+
includedByOption: includedByOption.has(f.id)
|
|
1127
|
+
},
|
|
1128
|
+
[f.id]
|
|
1129
|
+
)
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/core/validate/steps/fallbacks.ts
|
|
1136
|
+
function codeForReason(reason) {
|
|
1137
|
+
switch (reason) {
|
|
1138
|
+
case "unknown_service":
|
|
1139
|
+
return "fallback_unknown_service";
|
|
1140
|
+
case "no_primary":
|
|
1141
|
+
return "fallback_no_primary";
|
|
1142
|
+
case "rate_violation":
|
|
1143
|
+
return "fallback_rate_violation";
|
|
1144
|
+
case "constraint_mismatch":
|
|
1145
|
+
return "fallback_constraint_mismatch";
|
|
1146
|
+
case "cycle":
|
|
1147
|
+
return "fallback_cycle";
|
|
1148
|
+
default:
|
|
1149
|
+
return "fallback_bad_node";
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function messageFor(code, d) {
|
|
1153
|
+
const n = d.nodeId ? `node "${String(d.nodeId)}"` : "node";
|
|
1154
|
+
switch (code) {
|
|
1155
|
+
case "fallback_unknown_service":
|
|
1156
|
+
return `Fallback candidate "${String(
|
|
1157
|
+
d.candidate
|
|
1158
|
+
)}" is unknown for ${n}.`;
|
|
1159
|
+
case "fallback_no_primary":
|
|
1160
|
+
return `Fallback rule has no primary service for ${n}.`;
|
|
1161
|
+
case "fallback_rate_violation":
|
|
1162
|
+
return `Fallback candidate "${String(
|
|
1163
|
+
d.candidate
|
|
1164
|
+
)}" violates the base-rate rules for ${n}.`;
|
|
1165
|
+
case "fallback_constraint_mismatch":
|
|
1166
|
+
return `Fallback candidate "${String(
|
|
1167
|
+
d.candidate
|
|
1168
|
+
)}" does not satisfy required constraints for ${n}.`;
|
|
1169
|
+
case "fallback_cycle":
|
|
1170
|
+
return `Fallback rules contain a cycle for ${n}.`;
|
|
1171
|
+
default:
|
|
1172
|
+
return `Fallback rule is invalid for ${n}.`;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function validateFallbacks(v) {
|
|
1176
|
+
var _a, _b, _c, _d;
|
|
1177
|
+
const mode = (_b = (_a = v.options.fallbackSettings) == null ? void 0 : _a.mode) != null ? _b : "strict";
|
|
1178
|
+
if (!v.props.fallbacks) return;
|
|
1179
|
+
const diags = collectFailedFallbacks(v.props, (_c = v.options.serviceMap) != null ? _c : {}, {
|
|
1180
|
+
...v.options.fallbackSettings,
|
|
1181
|
+
mode: "dev"
|
|
1182
|
+
});
|
|
1183
|
+
if (mode !== "strict") return;
|
|
1184
|
+
for (const d of diags) {
|
|
1185
|
+
if (d.scope === "global") continue;
|
|
1186
|
+
const code = codeForReason(
|
|
1187
|
+
String((_d = d.reason) != null ? _d : "fallback_bad_node")
|
|
1188
|
+
);
|
|
1189
|
+
const nodeId = d.nodeId ? String(d.nodeId) : void 0;
|
|
1190
|
+
const tagContext = d.tagContext;
|
|
1191
|
+
const affectedIds = [];
|
|
1192
|
+
if (nodeId) affectedIds.push(nodeId);
|
|
1193
|
+
if (typeof tagContext === "string" && tagContext && tagContext !== nodeId)
|
|
1194
|
+
affectedIds.push(tagContext);
|
|
1195
|
+
v.errors.push({
|
|
1196
|
+
code,
|
|
1197
|
+
severity: "error",
|
|
1198
|
+
message: messageFor(code, {
|
|
1199
|
+
nodeId,
|
|
1200
|
+
primary: d.primary,
|
|
1201
|
+
candidate: d.candidate,
|
|
1202
|
+
tagContext,
|
|
1203
|
+
scope: d.scope
|
|
1204
|
+
}),
|
|
1205
|
+
nodeId,
|
|
1206
|
+
details: withAffected(
|
|
1207
|
+
{
|
|
1208
|
+
primary: d.primary,
|
|
1209
|
+
candidate: d.candidate,
|
|
1210
|
+
tagContext,
|
|
1211
|
+
scope: d.scope
|
|
1212
|
+
},
|
|
1213
|
+
affectedIds.length > 1 ? affectedIds : void 0
|
|
1214
|
+
)
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/core/validate/policies/collect-service-items.ts
|
|
1220
|
+
function asArray(v) {
|
|
1221
|
+
if (v === void 0) return void 0;
|
|
1222
|
+
return Array.isArray(v) ? v : [v];
|
|
1223
|
+
}
|
|
1224
|
+
function isServiceIdRef(v) {
|
|
1225
|
+
return typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
1226
|
+
}
|
|
1227
|
+
function svcSnapshot(serviceMap, sid) {
|
|
1228
|
+
const svc = serviceMap[sid];
|
|
1229
|
+
if (!svc) return { id: sid };
|
|
1230
|
+
const meta = svc.meta && typeof svc.meta === "object" ? svc.meta : {};
|
|
1231
|
+
return {
|
|
1232
|
+
...svc,
|
|
1233
|
+
id: sid,
|
|
1234
|
+
...meta
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function pushItem(out, next) {
|
|
1238
|
+
var _a;
|
|
1239
|
+
const key = `${String(next.serviceId)}|${next.role}`;
|
|
1240
|
+
const existing = out.get(key);
|
|
1241
|
+
if (!existing) {
|
|
1242
|
+
out.set(key, {
|
|
1243
|
+
tagId: next.tagId,
|
|
1244
|
+
fieldId: next.fieldId,
|
|
1245
|
+
optionId: next.optionId,
|
|
1246
|
+
nodeId: next.nodeId,
|
|
1247
|
+
serviceId: next.serviceId,
|
|
1248
|
+
role: next.role,
|
|
1249
|
+
service: next.service,
|
|
1250
|
+
affectedIds: Array.from(new Set(next.affectedIds))
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const mergedIds = Array.from(
|
|
1255
|
+
/* @__PURE__ */ new Set([...existing.affectedIds, ...next.affectedIds])
|
|
1256
|
+
);
|
|
1257
|
+
out.set(key, {
|
|
1258
|
+
...existing,
|
|
1259
|
+
tagId: (_a = existing.tagId) != null ? _a : next.tagId,
|
|
1260
|
+
affectedIds: mergedIds
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function fieldRoleOf(f, o) {
|
|
1264
|
+
var _a, _b;
|
|
1265
|
+
const roleRaw = (_b = (_a = o == null ? void 0 : o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
|
|
1266
|
+
return roleRaw === "utility" ? "utility" : "base";
|
|
1267
|
+
}
|
|
1268
|
+
function applyFilterAllowLists(tagId, fieldId, filter) {
|
|
1269
|
+
const tagAllow = asArray(filter == null ? void 0 : filter.tag_id);
|
|
1270
|
+
const fieldAllow = asArray(filter == null ? void 0 : filter.field_id);
|
|
1271
|
+
if (tagAllow) {
|
|
1272
|
+
if (!tagId) return false;
|
|
1273
|
+
if (!tagAllow.includes(tagId)) return false;
|
|
1274
|
+
}
|
|
1275
|
+
if (fieldAllow) {
|
|
1276
|
+
if (!fieldId) return false;
|
|
1277
|
+
if (!fieldAllow.includes(fieldId)) return false;
|
|
1278
|
+
}
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
function collectServiceItems(args) {
|
|
1282
|
+
var _a, _b, _c, _d, _e;
|
|
1283
|
+
const filter = args.filter;
|
|
1284
|
+
const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
|
|
1285
|
+
const where = filter == null ? void 0 : filter.where;
|
|
1286
|
+
const out = /* @__PURE__ */ new Map();
|
|
1287
|
+
const addServiceRef = (ref) => {
|
|
1288
|
+
if (roleFilter !== "both" && ref.role !== roleFilter) return;
|
|
1289
|
+
if (!applyFilterAllowLists(ref.tagId, ref.fieldId, filter)) return;
|
|
1290
|
+
const svc = args.serviceMap[ref.serviceId];
|
|
1291
|
+
if (where && svc && !matchesWhere(svc, where)) return;
|
|
1292
|
+
pushItem(out, {
|
|
1293
|
+
...ref,
|
|
1294
|
+
service: svcSnapshot(args.serviceMap, ref.serviceId)
|
|
1295
|
+
});
|
|
1296
|
+
};
|
|
1297
|
+
if (args.mode === "global") {
|
|
1298
|
+
for (const t of (_b = args.tags) != null ? _b : []) {
|
|
1299
|
+
const sid = t.service_id;
|
|
1300
|
+
if (!isServiceIdRef(sid)) continue;
|
|
1301
|
+
addServiceRef({
|
|
1302
|
+
tagId: t.id,
|
|
1303
|
+
serviceId: sid,
|
|
1304
|
+
role: "base",
|
|
1305
|
+
affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
} else if (args.mode === "visible_group") {
|
|
1309
|
+
const t = args.tag;
|
|
1310
|
+
const sid = t ? t.service_id : void 0;
|
|
1311
|
+
if (t && isServiceIdRef(sid)) {
|
|
1312
|
+
addServiceRef({
|
|
1313
|
+
tagId: t.id,
|
|
1314
|
+
serviceId: sid,
|
|
1315
|
+
role: "base",
|
|
1316
|
+
affectedIds: [`tag:${t.id}`, `service:${String(sid)}`]
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const fields = (_c = args.fields) != null ? _c : [];
|
|
1321
|
+
for (const f of fields) {
|
|
1322
|
+
const fSid = f.service_id;
|
|
1323
|
+
if (isServiceIdRef(fSid)) {
|
|
1324
|
+
addServiceRef({
|
|
1325
|
+
tagId: args.tagId,
|
|
1326
|
+
fieldId: f.id,
|
|
1327
|
+
serviceId: fSid,
|
|
1328
|
+
role: "base",
|
|
1329
|
+
affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
for (const o of (_d = f.options) != null ? _d : []) {
|
|
1333
|
+
const oSid = o.service_id;
|
|
1334
|
+
if (!isServiceIdRef(oSid)) continue;
|
|
1335
|
+
const role = fieldRoleOf(f, o);
|
|
1336
|
+
addServiceRef({
|
|
1337
|
+
tagId: args.tagId,
|
|
1338
|
+
fieldId: f.id,
|
|
1339
|
+
optionId: o.id,
|
|
1340
|
+
serviceId: oSid,
|
|
1341
|
+
role,
|
|
1342
|
+
affectedIds: [
|
|
1343
|
+
`field:${f.id}`,
|
|
1344
|
+
`option:${o.id}`,
|
|
1345
|
+
`service:${String(oSid)}`
|
|
1346
|
+
]
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const fb = args.props.fallbacks;
|
|
1351
|
+
if (!fb) return Array.from(out.values());
|
|
1352
|
+
const addFallbackNode = (nodeId, list) => {
|
|
1353
|
+
const arr = Array.isArray(list) ? list : [];
|
|
1354
|
+
for (const cand of arr) {
|
|
1355
|
+
if (!isServiceIdRef(cand)) continue;
|
|
1356
|
+
addServiceRef({
|
|
1357
|
+
tagId: args.tagId,
|
|
1358
|
+
nodeId,
|
|
1359
|
+
serviceId: cand,
|
|
1360
|
+
role: "base",
|
|
1361
|
+
affectedIds: [`fallback-node:${nodeId}`, `service:${String(cand)}`]
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
const addFallbackGlobal = (primaryKey, list) => {
|
|
1366
|
+
const primaryId = primaryKey;
|
|
1367
|
+
addServiceRef({
|
|
1368
|
+
tagId: args.tagId,
|
|
1369
|
+
nodeId: primaryKey,
|
|
1370
|
+
serviceId: primaryId,
|
|
1371
|
+
role: "base",
|
|
1372
|
+
affectedIds: [
|
|
1373
|
+
`fallback-global-primary:${primaryKey}`,
|
|
1374
|
+
`service:${String(primaryId)}`
|
|
1375
|
+
]
|
|
1376
|
+
});
|
|
1377
|
+
const arr = Array.isArray(list) ? list : [];
|
|
1378
|
+
for (const cand of arr) {
|
|
1379
|
+
if (!isServiceIdRef(cand)) continue;
|
|
1380
|
+
addServiceRef({
|
|
1381
|
+
tagId: args.tagId,
|
|
1382
|
+
nodeId: primaryKey,
|
|
1383
|
+
serviceId: cand,
|
|
1384
|
+
role: "base",
|
|
1385
|
+
affectedIds: [
|
|
1386
|
+
`fallback-global:${primaryKey}`,
|
|
1387
|
+
`service:${String(cand)}`
|
|
1388
|
+
]
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
const includeAllFallbacks = args.mode === "global";
|
|
1393
|
+
const includeGroupFallbacks = args.mode === "visible_group";
|
|
1394
|
+
const nodes = fb.nodes && typeof fb.nodes === "object" ? fb.nodes : void 0;
|
|
1395
|
+
if (nodes) {
|
|
1396
|
+
if (includeAllFallbacks) {
|
|
1397
|
+
for (const [nodeId, list] of Object.entries(nodes)) {
|
|
1398
|
+
addFallbackNode(nodeId, list);
|
|
1399
|
+
}
|
|
1400
|
+
} else if (includeGroupFallbacks) {
|
|
1401
|
+
const allowNodes = new Set(
|
|
1402
|
+
Array.isArray(args.visibleNodeIds) ? args.visibleNodeIds : []
|
|
1403
|
+
);
|
|
1404
|
+
for (const nodeId of allowNodes) {
|
|
1405
|
+
addFallbackNode(nodeId, nodes[nodeId]);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const globalFb = fb.global && typeof fb.global === "object" ? fb.global : void 0;
|
|
1410
|
+
if (globalFb) {
|
|
1411
|
+
if (includeAllFallbacks) {
|
|
1412
|
+
for (const [primaryKey, list] of Object.entries(globalFb)) {
|
|
1413
|
+
addFallbackGlobal(primaryKey, list);
|
|
1414
|
+
}
|
|
1415
|
+
} else if (includeGroupFallbacks) {
|
|
1416
|
+
const allowPrimaries = new Set(
|
|
1417
|
+
((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
|
|
1418
|
+
);
|
|
1419
|
+
for (const primaryKey of allowPrimaries) {
|
|
1420
|
+
const list = globalFb[primaryKey];
|
|
1421
|
+
if (list === void 0) continue;
|
|
1422
|
+
addFallbackGlobal(primaryKey, list);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return Array.from(out.values());
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/core/validate/policies/ops.ts
|
|
1430
|
+
function evalPolicyOp(op, values, rule) {
|
|
1431
|
+
switch (op) {
|
|
1432
|
+
case "all_equal": {
|
|
1433
|
+
const set = new Set(
|
|
1434
|
+
values.map((v) => JSON.stringify(v))
|
|
1435
|
+
);
|
|
1436
|
+
return set.size <= 1;
|
|
1437
|
+
}
|
|
1438
|
+
case "no_mix": {
|
|
1439
|
+
const set = new Set(
|
|
1440
|
+
values.map((v) => JSON.stringify(v))
|
|
1441
|
+
);
|
|
1442
|
+
return set.size <= 1;
|
|
1443
|
+
}
|
|
1444
|
+
case "unique": {
|
|
1445
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1446
|
+
for (const v of values) {
|
|
1447
|
+
const k = JSON.stringify(v);
|
|
1448
|
+
if (seen.has(k)) return false;
|
|
1449
|
+
seen.add(k);
|
|
1450
|
+
}
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
case "all_true": {
|
|
1454
|
+
return values.every((v) => v === true);
|
|
1455
|
+
}
|
|
1456
|
+
case "any_true": {
|
|
1457
|
+
return values.some((v) => v === true);
|
|
1458
|
+
}
|
|
1459
|
+
case "max_count": {
|
|
1460
|
+
const limit = typeof rule.value === "number" ? rule.value : Infinity;
|
|
1461
|
+
return values.length <= limit;
|
|
1462
|
+
}
|
|
1463
|
+
case "min_count": {
|
|
1464
|
+
const min = typeof rule.value === "number" ? rule.value : 0;
|
|
1465
|
+
return values.length >= min;
|
|
1466
|
+
}
|
|
1467
|
+
default:
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// src/core/validate/policies/apply-policies.ts
|
|
1473
|
+
function uniq(arr) {
|
|
1474
|
+
return Array.from(new Set(arr));
|
|
1475
|
+
}
|
|
1476
|
+
function stableSeverity(s) {
|
|
1477
|
+
if (s === "warning") return "warning";
|
|
1478
|
+
if (s === "error") return "error";
|
|
1479
|
+
return "error";
|
|
1480
|
+
}
|
|
1481
|
+
function defaultPolicyMessage(rule) {
|
|
1482
|
+
if (typeof rule.message === "string" && rule.message.trim())
|
|
1483
|
+
return rule.message;
|
|
1484
|
+
if (typeof rule.label === "string" && rule.label.trim())
|
|
1485
|
+
return rule.label.trim();
|
|
1486
|
+
return `Policy "${rule.id}" violated`;
|
|
1487
|
+
}
|
|
1488
|
+
function affectedFromItems(items) {
|
|
1489
|
+
var _a;
|
|
1490
|
+
const ids = [];
|
|
1491
|
+
for (const it of items) {
|
|
1492
|
+
for (const x of (_a = it.affectedIds) != null ? _a : []) ids.push(x);
|
|
1493
|
+
ids.push(`service:${String(it.serviceId)}`);
|
|
1494
|
+
}
|
|
1495
|
+
return uniq(ids);
|
|
1496
|
+
}
|
|
1497
|
+
function visibleGroupNodeIds(tag, fields) {
|
|
1498
|
+
var _a;
|
|
1499
|
+
const ids = [tag.id];
|
|
1500
|
+
for (const f of fields) {
|
|
1501
|
+
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1502
|
+
ids.push(o.id);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return uniq(ids);
|
|
1506
|
+
}
|
|
1507
|
+
function visibleGroupPrimaries(tag, fields) {
|
|
1508
|
+
var _a;
|
|
1509
|
+
const prim = [];
|
|
1510
|
+
const tagSid = tag.service_id;
|
|
1511
|
+
if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
|
|
1512
|
+
prim.push(tagSid);
|
|
1513
|
+
}
|
|
1514
|
+
for (const f of fields) {
|
|
1515
|
+
const fsid = f.service_id;
|
|
1516
|
+
if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
|
|
1517
|
+
prim.push(fsid);
|
|
1518
|
+
}
|
|
1519
|
+
for (const o of (_a = f.options) != null ? _a : []) {
|
|
1520
|
+
const osid = o.service_id;
|
|
1521
|
+
if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
|
|
1522
|
+
prim.push(osid);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return uniq(prim);
|
|
1527
|
+
}
|
|
1528
|
+
function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder, tags) {
|
|
1529
|
+
var _a, _b, _c, _d, _e;
|
|
1530
|
+
if (!(policies == null ? void 0 : policies.length)) return;
|
|
1531
|
+
const tagById = /* @__PURE__ */ new Map();
|
|
1532
|
+
for (const t of tags) tagById.set(t.id, t);
|
|
1533
|
+
for (const rule of policies) {
|
|
1534
|
+
const projPath = (_a = rule.projection) != null ? _a : "service.id";
|
|
1535
|
+
const severity = stableSeverity(
|
|
1536
|
+
rule.severity
|
|
1537
|
+
);
|
|
1538
|
+
const message = defaultPolicyMessage(rule);
|
|
1539
|
+
if (rule.scope === "global") {
|
|
1540
|
+
const tagAllow = Array.isArray(
|
|
1541
|
+
(_b = rule.filter) == null ? void 0 : _b.tag_id
|
|
1542
|
+
) ? (_c = rule.filter) == null ? void 0 : _c.tag_id : ((_d = rule.filter) == null ? void 0 : _d.tag_id) ? [rule.filter.tag_id] : void 0;
|
|
1543
|
+
let items = [];
|
|
1544
|
+
if (tagAllow && tagAllow.length) {
|
|
1545
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1546
|
+
for (const id of tagAllow) {
|
|
1547
|
+
const t = tagById.get(id);
|
|
1548
|
+
if (!t) continue;
|
|
1549
|
+
const visibleFields = fieldsVisibleUnder(t.id);
|
|
1550
|
+
const nodeIds = visibleGroupNodeIds(
|
|
1551
|
+
t,
|
|
1552
|
+
visibleFields
|
|
1553
|
+
);
|
|
1554
|
+
const primaries = visibleGroupPrimaries(
|
|
1555
|
+
t,
|
|
1556
|
+
visibleFields
|
|
1557
|
+
);
|
|
1558
|
+
const sub = collectServiceItems({
|
|
1559
|
+
mode: "visible_group",
|
|
1560
|
+
props,
|
|
1561
|
+
serviceMap,
|
|
1562
|
+
tag: t,
|
|
1563
|
+
tagId: t.id,
|
|
1564
|
+
fields: visibleFields,
|
|
1565
|
+
filter: rule.filter,
|
|
1566
|
+
visibleNodeIds: nodeIds,
|
|
1567
|
+
visiblePrimaries: primaries
|
|
1568
|
+
});
|
|
1569
|
+
for (const it of sub) {
|
|
1570
|
+
const k = `${String(it.serviceId)}|${it.role}`;
|
|
1571
|
+
const existing = merged.get(k);
|
|
1572
|
+
if (!existing) {
|
|
1573
|
+
merged.set(k, it);
|
|
1574
|
+
} else {
|
|
1575
|
+
merged.set(k, {
|
|
1576
|
+
...existing,
|
|
1577
|
+
affectedIds: uniq([
|
|
1578
|
+
...existing.affectedIds,
|
|
1579
|
+
...it.affectedIds
|
|
1580
|
+
])
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
items = Array.from(merged.values());
|
|
1586
|
+
} else {
|
|
1587
|
+
const allFields = (_e = props.fields) != null ? _e : [];
|
|
1588
|
+
items = collectServiceItems({
|
|
1589
|
+
mode: "global",
|
|
1590
|
+
props,
|
|
1591
|
+
serviceMap,
|
|
1592
|
+
tags,
|
|
1593
|
+
fields: allFields,
|
|
1594
|
+
filter: rule.filter
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const values = items.map(
|
|
1598
|
+
(it) => getByPath(it, projPath)
|
|
1599
|
+
);
|
|
1600
|
+
if (!evalPolicyOp(rule.op, values, rule)) {
|
|
1601
|
+
errors.push({
|
|
1602
|
+
code: "policy_violation",
|
|
1603
|
+
severity,
|
|
1604
|
+
message,
|
|
1605
|
+
nodeId: "global",
|
|
1606
|
+
details: {
|
|
1607
|
+
ruleId: rule.id,
|
|
1608
|
+
scope: "global",
|
|
1609
|
+
op: rule.op,
|
|
1610
|
+
projection: projPath,
|
|
1611
|
+
count: items.length,
|
|
1612
|
+
affectedIds: affectedFromItems(items)
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
for (const t of tags) {
|
|
1619
|
+
const visibleFields = fieldsVisibleUnder(t.id);
|
|
1620
|
+
const nodeIds = visibleGroupNodeIds(t, visibleFields);
|
|
1621
|
+
const primaries = visibleGroupPrimaries(t, visibleFields);
|
|
1622
|
+
const items = collectServiceItems({
|
|
1623
|
+
mode: "visible_group",
|
|
1624
|
+
props,
|
|
1625
|
+
serviceMap,
|
|
1626
|
+
tag: t,
|
|
1627
|
+
tagId: t.id,
|
|
1628
|
+
fields: visibleFields,
|
|
1629
|
+
filter: rule.filter,
|
|
1630
|
+
visibleNodeIds: nodeIds,
|
|
1631
|
+
visiblePrimaries: primaries
|
|
1632
|
+
});
|
|
1633
|
+
if (!items.length) continue;
|
|
1634
|
+
const values = items.map(
|
|
1635
|
+
(it) => getByPath(it, projPath)
|
|
1636
|
+
);
|
|
1637
|
+
if (!evalPolicyOp(rule.op, values, rule)) {
|
|
1638
|
+
errors.push({
|
|
1639
|
+
code: "policy_violation",
|
|
1640
|
+
severity,
|
|
1641
|
+
message,
|
|
1642
|
+
nodeId: t.id,
|
|
1643
|
+
details: {
|
|
1644
|
+
ruleId: rule.id,
|
|
1645
|
+
scope: "visible_group",
|
|
1646
|
+
op: rule.op,
|
|
1647
|
+
projection: projPath,
|
|
1648
|
+
count: items.length,
|
|
1649
|
+
affectedIds: affectedFromItems(items)
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/core/validate/index.ts
|
|
1658
|
+
function validate(props, ctx = {}) {
|
|
1659
|
+
var _a, _b;
|
|
1660
|
+
const errors = [];
|
|
1661
|
+
const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
|
|
1662
|
+
const selectedKeys = new Set(
|
|
1663
|
+
(_b = ctx.selectedOptionKeys) != null ? _b : []
|
|
1664
|
+
);
|
|
1665
|
+
const tags = Array.isArray(props.filters) ? props.filters : [];
|
|
1666
|
+
const fields = Array.isArray(props.fields) ? props.fields : [];
|
|
1667
|
+
const tagById = /* @__PURE__ */ new Map();
|
|
1668
|
+
const fieldById = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const t of tags) tagById.set(t.id, t);
|
|
1670
|
+
for (const f of fields) fieldById.set(f.id, f);
|
|
1671
|
+
const v = {
|
|
1672
|
+
props,
|
|
1673
|
+
options: ctx,
|
|
1674
|
+
errors,
|
|
1675
|
+
serviceMap,
|
|
1676
|
+
selectedKeys,
|
|
1677
|
+
tags,
|
|
1678
|
+
fields,
|
|
1679
|
+
tagById,
|
|
1680
|
+
fieldById,
|
|
1681
|
+
fieldsVisibleUnder: (_tagId) => []
|
|
1682
|
+
};
|
|
1683
|
+
validateStructure(v);
|
|
1684
|
+
validateIdentity(v);
|
|
1685
|
+
validateOptionMaps(v);
|
|
1686
|
+
v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
|
|
1687
|
+
validateVisibility(v);
|
|
1688
|
+
applyPolicies(
|
|
1689
|
+
v.errors,
|
|
1690
|
+
v.props,
|
|
1691
|
+
v.serviceMap,
|
|
1692
|
+
v.options.policies,
|
|
1693
|
+
v.fieldsVisibleUnder,
|
|
1694
|
+
v.tags
|
|
1695
|
+
);
|
|
1696
|
+
validateServiceVsUserInput(v);
|
|
1697
|
+
validateUtilityMarkers(v);
|
|
1698
|
+
validateRates(v);
|
|
1699
|
+
validateConstraints(v);
|
|
1700
|
+
validateCustomFields(v);
|
|
1701
|
+
validateGlobalUtilityGuard(v);
|
|
1702
|
+
validateUnboundFields(v);
|
|
1703
|
+
validateFallbacks(v);
|
|
1704
|
+
return v.errors;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/core/builder.ts
|
|
1708
|
+
function createBuilder(opts = {}) {
|
|
1709
|
+
return new BuilderImpl(opts);
|
|
1710
|
+
}
|
|
1711
|
+
var BuilderImpl = class {
|
|
1712
|
+
constructor(opts = {}) {
|
|
1713
|
+
this.props = {
|
|
1714
|
+
filters: [],
|
|
1715
|
+
fields: [],
|
|
1716
|
+
schema_version: "1.0"
|
|
1717
|
+
};
|
|
1718
|
+
this.tagById = /* @__PURE__ */ new Map();
|
|
1719
|
+
this.fieldById = /* @__PURE__ */ new Map();
|
|
1720
|
+
this.optionOwnerById = /* @__PURE__ */ new Map();
|
|
1721
|
+
this.history = [];
|
|
1722
|
+
this.future = [];
|
|
1723
|
+
var _a;
|
|
1724
|
+
this.options = { ...opts };
|
|
1725
|
+
this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
|
|
1726
|
+
}
|
|
1727
|
+
/* ───── lifecycle ─────────────────────────────────────────────────────── */
|
|
1728
|
+
load(raw) {
|
|
1729
|
+
const next = normalise(raw, {
|
|
1730
|
+
defaultPricingRole: "base",
|
|
1731
|
+
constraints: this.getConstraints().map((item) => item.label)
|
|
1732
|
+
});
|
|
1733
|
+
this.pushHistory(this.props);
|
|
1734
|
+
this.future.length = 0;
|
|
1735
|
+
this.props = next;
|
|
1736
|
+
this.rebuildIndexes();
|
|
1737
|
+
}
|
|
1738
|
+
getProps() {
|
|
1739
|
+
return this.props;
|
|
1740
|
+
}
|
|
1741
|
+
setOptions(patch) {
|
|
1742
|
+
this.options = { ...this.options, ...patch };
|
|
1743
|
+
}
|
|
1744
|
+
getServiceMap() {
|
|
1745
|
+
var _a;
|
|
1746
|
+
return (_a = this.options.serviceMap) != null ? _a : {};
|
|
1747
|
+
}
|
|
1748
|
+
getConstraints() {
|
|
1749
|
+
var _a;
|
|
1750
|
+
const serviceMap = this.getServiceMap();
|
|
1751
|
+
const out = /* @__PURE__ */ new Set();
|
|
1752
|
+
const guard = /* @__PURE__ */ new Set();
|
|
1753
|
+
for (const svc of Object.values(serviceMap)) {
|
|
1754
|
+
const flags = (_a = svc.flags) != null ? _a : {};
|
|
1755
|
+
for (const flagId of Object.keys(flags)) {
|
|
1756
|
+
if (guard.has(flagId)) continue;
|
|
1757
|
+
guard.add(flagId);
|
|
1758
|
+
out.add({
|
|
1759
|
+
id: flagId,
|
|
1760
|
+
value: flagId,
|
|
1761
|
+
label: flagId,
|
|
1762
|
+
description: flags[flagId].description
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return Array.from(out);
|
|
1767
|
+
}
|
|
1768
|
+
/* ───── querying ─────────────────────────────────────────────────────── */
|
|
1769
|
+
tree() {
|
|
1770
|
+
var _a, _b, _c, _d;
|
|
1771
|
+
const nodes = [];
|
|
1772
|
+
const edges = [];
|
|
1773
|
+
const showSet = toStringSet(this.options.showOptionNodes);
|
|
1774
|
+
for (const t of this.props.filters) {
|
|
1775
|
+
nodes.push({ id: t.id, kind: "tag", label: t.label });
|
|
1776
|
+
}
|
|
1777
|
+
for (const t of this.props.filters) {
|
|
1778
|
+
if (t.bind_id) {
|
|
1779
|
+
edges.push({
|
|
1780
|
+
from: t.bind_id,
|
|
1781
|
+
to: t.id,
|
|
1782
|
+
kind: "child"
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
for (const f of this.props.fields) {
|
|
1787
|
+
nodes.push({
|
|
1788
|
+
id: f.id,
|
|
1789
|
+
kind: "field",
|
|
1790
|
+
label: f.label,
|
|
1791
|
+
bind_type: f.pricing_role === "utility" ? "utility" : f.bind_id ? "bound" : null
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
for (const f of this.props.fields) {
|
|
1795
|
+
const b = f.bind_id;
|
|
1796
|
+
if (Array.isArray(b)) {
|
|
1797
|
+
for (const tagId of b)
|
|
1798
|
+
edges.push({
|
|
1799
|
+
from: tagId,
|
|
1800
|
+
to: f.id,
|
|
1801
|
+
kind: "bind"
|
|
1802
|
+
});
|
|
1803
|
+
} else if (typeof b === "string") {
|
|
1804
|
+
edges.push({ from: b, to: f.id, kind: "bind" });
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
for (const f of this.props.fields) {
|
|
1808
|
+
const showOptions = showSet.has(f.id);
|
|
1809
|
+
if (!showOptions) continue;
|
|
1810
|
+
if (!Array.isArray(f.options)) continue;
|
|
1811
|
+
for (const o of f.options) {
|
|
1812
|
+
nodes.push({
|
|
1813
|
+
id: o.id,
|
|
1814
|
+
kind: "option",
|
|
1815
|
+
label: o.label
|
|
1816
|
+
});
|
|
1817
|
+
const e = {
|
|
1818
|
+
from: f.id,
|
|
1819
|
+
to: o.id,
|
|
1820
|
+
kind: "option",
|
|
1821
|
+
meta: { ownerField: f.id }
|
|
1822
|
+
};
|
|
1823
|
+
edges.push(e);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
for (const t of this.props.filters) {
|
|
1827
|
+
for (const id of (_a = t.includes) != null ? _a : []) {
|
|
1828
|
+
edges.push({ from: t.id, to: id, kind: "include" });
|
|
1829
|
+
}
|
|
1830
|
+
for (const id of (_b = t.excludes) != null ? _b : []) {
|
|
1831
|
+
edges.push({ from: t.id, to: id, kind: "exclude" });
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
|
|
1835
|
+
const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
|
|
1836
|
+
const pushButtonEdge = (keyId, targetFieldId, kind) => {
|
|
1837
|
+
var _a2;
|
|
1838
|
+
const owner = this.optionOwnerById.get(keyId);
|
|
1839
|
+
const ownerFieldId = (_a2 = owner == null ? void 0 : owner.fieldId) != null ? _a2 : this.fieldById.has(keyId) ? keyId : void 0;
|
|
1840
|
+
if (!ownerFieldId) return;
|
|
1841
|
+
const fromNode = owner && showSet.has(owner.fieldId) ? keyId : ownerFieldId;
|
|
1842
|
+
const meta = owner ? showSet.has(owner.fieldId) ? {
|
|
1843
|
+
via: "option-visible",
|
|
1844
|
+
ownerField: owner.fieldId,
|
|
1845
|
+
sourceOption: keyId
|
|
1846
|
+
} : {
|
|
1847
|
+
via: "option-hidden",
|
|
1848
|
+
ownerField: owner.fieldId,
|
|
1849
|
+
sourceOption: keyId
|
|
1850
|
+
} : { via: "field-button" };
|
|
1851
|
+
const e = { from: fromNode, to: targetFieldId, kind, meta };
|
|
1852
|
+
edges.push(e);
|
|
1853
|
+
};
|
|
1854
|
+
for (const [keyId, arr] of Object.entries(incMap)) {
|
|
1855
|
+
for (const fid of arr != null ? arr : [])
|
|
1856
|
+
pushButtonEdge(keyId, fid, "include");
|
|
1857
|
+
}
|
|
1858
|
+
for (const [keyId, arr] of Object.entries(excMap)) {
|
|
1859
|
+
for (const fid of arr != null ? arr : [])
|
|
1860
|
+
pushButtonEdge(keyId, fid, "exclude");
|
|
1861
|
+
}
|
|
1862
|
+
return { nodes, edges };
|
|
1863
|
+
}
|
|
1864
|
+
cleanedProps() {
|
|
1865
|
+
var _a, _b, _c, _d, _e;
|
|
1866
|
+
const fieldIds = new Set(this.props.fields.map((f) => f.id));
|
|
1867
|
+
const optionIds = /* @__PURE__ */ new Set();
|
|
1868
|
+
this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
|
|
1869
|
+
const includedByTag = /* @__PURE__ */ new Set();
|
|
1870
|
+
const excludedAnywhere = /* @__PURE__ */ new Set();
|
|
1871
|
+
for (const t of this.props.filters) {
|
|
1872
|
+
for (const id of (_a = t.includes) != null ? _a : []) includedByTag.add(id);
|
|
1873
|
+
for (const id of (_b = t.excludes) != null ? _b : []) excludedAnywhere.add(id);
|
|
1874
|
+
}
|
|
1875
|
+
const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
|
|
1876
|
+
const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
|
|
1877
|
+
const includedByButtons = /* @__PURE__ */ new Set();
|
|
1878
|
+
const referencedKeys = /* @__PURE__ */ new Set();
|
|
1879
|
+
const referencedOwnerFields = /* @__PURE__ */ new Set();
|
|
1880
|
+
for (const [key, arr] of Object.entries(incMap)) {
|
|
1881
|
+
referencedKeys.add(key);
|
|
1882
|
+
const owner = this.optionOwnerById.get(key);
|
|
1883
|
+
if (owner) referencedOwnerFields.add(owner.fieldId);
|
|
1884
|
+
for (const fid of arr != null ? arr : []) {
|
|
1885
|
+
includedByButtons.add(fid);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
for (const [key, arr] of Object.entries(excMap)) {
|
|
1889
|
+
referencedKeys.add(key);
|
|
1890
|
+
const owner = this.optionOwnerById.get(key);
|
|
1891
|
+
if (owner) referencedOwnerFields.add(owner.fieldId);
|
|
1892
|
+
for (const fid of arr != null ? arr : []) {
|
|
1893
|
+
void fid;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
const boundIds = /* @__PURE__ */ new Set();
|
|
1897
|
+
for (const f of this.props.fields) {
|
|
1898
|
+
const b = f.bind_id;
|
|
1899
|
+
if (Array.isArray(b)) b.forEach((id) => boundIds.add(id));
|
|
1900
|
+
else if (typeof b === "string") boundIds.add(b);
|
|
1901
|
+
}
|
|
1902
|
+
const fields = this.props.fields.filter((f) => {
|
|
1903
|
+
var _a2;
|
|
1904
|
+
const isUtility = ((_a2 = f.pricing_role) != null ? _a2 : "base") === "utility";
|
|
1905
|
+
if (!isUtility) return true;
|
|
1906
|
+
const bound = !!f.bind_id;
|
|
1907
|
+
const included = includedByTag.has(f.id) || includedByButtons.has(f.id);
|
|
1908
|
+
const referenced = referencedOwnerFields.has(f.id) || referencedKeys.has(f.id);
|
|
1909
|
+
const excluded = excludedAnywhere.has(f.id);
|
|
1910
|
+
return bound || included || referenced || !excluded;
|
|
1911
|
+
});
|
|
1912
|
+
const allowedTargets = new Set(fields.map((f) => f.id));
|
|
1913
|
+
const pruneButtons = (src) => {
|
|
1914
|
+
if (!src) return void 0;
|
|
1915
|
+
const out2 = {};
|
|
1916
|
+
for (const [key, arr] of Object.entries(src)) {
|
|
1917
|
+
const keyIsValid = optionIds.has(key) || fieldIds.has(key);
|
|
1918
|
+
if (!keyIsValid) continue;
|
|
1919
|
+
const cleaned = (arr != null ? arr : []).filter(
|
|
1920
|
+
(fid) => allowedTargets.has(fid)
|
|
1921
|
+
);
|
|
1922
|
+
if (cleaned.length) out2[key] = Array.from(new Set(cleaned));
|
|
1923
|
+
}
|
|
1924
|
+
return Object.keys(out2).length ? out2 : void 0;
|
|
1925
|
+
};
|
|
1926
|
+
const includes_for_buttons = pruneButtons(
|
|
1927
|
+
this.props.includes_for_buttons
|
|
1928
|
+
);
|
|
1929
|
+
const excludes_for_buttons = pruneButtons(
|
|
1930
|
+
this.props.excludes_for_buttons
|
|
1931
|
+
);
|
|
1932
|
+
const out = {
|
|
1933
|
+
filters: this.props.filters.slice(),
|
|
1934
|
+
fields,
|
|
1935
|
+
...includes_for_buttons && { includes_for_buttons },
|
|
1936
|
+
...excludes_for_buttons && { excludes_for_buttons },
|
|
1937
|
+
schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
|
|
1938
|
+
// keep fallbacks & other maps as-is
|
|
1939
|
+
...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
|
|
1940
|
+
};
|
|
1941
|
+
return out;
|
|
1942
|
+
}
|
|
1943
|
+
errors() {
|
|
1944
|
+
return validate(this.props, this.options);
|
|
1945
|
+
}
|
|
1946
|
+
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];
|
|
2070
|
+
}
|
|
2071
|
+
/* ───── history ─────────────────────────────────────────────────────── */
|
|
2072
|
+
undo() {
|
|
2073
|
+
if (this.history.length === 0) return false;
|
|
2074
|
+
const prev = this.history.pop();
|
|
2075
|
+
this.future.push(structuredCloneSafe(this.props));
|
|
2076
|
+
this.props = prev;
|
|
2077
|
+
this.rebuildIndexes();
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
redo() {
|
|
2081
|
+
if (this.future.length === 0) return false;
|
|
2082
|
+
const next = this.future.pop();
|
|
2083
|
+
this.pushHistory(this.props);
|
|
2084
|
+
this.props = next;
|
|
2085
|
+
this.rebuildIndexes();
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
/* ───── internals ──────────────────────────────────────────────────── */
|
|
2089
|
+
rebuildIndexes() {
|
|
2090
|
+
this.tagById.clear();
|
|
2091
|
+
this.fieldById.clear();
|
|
2092
|
+
this.optionOwnerById.clear();
|
|
2093
|
+
for (const t of this.props.filters) this.tagById.set(t.id, t);
|
|
2094
|
+
for (const f of this.props.fields) {
|
|
2095
|
+
this.fieldById.set(f.id, f);
|
|
2096
|
+
if (Array.isArray(f.options)) {
|
|
2097
|
+
for (const o of f.options)
|
|
2098
|
+
this.optionOwnerById.set(o.id, { fieldId: f.id });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
pushHistory(state) {
|
|
2103
|
+
if (!state || !state.filters.length && !state.fields.length) return;
|
|
2104
|
+
this.history.push(structuredCloneSafe(state));
|
|
2105
|
+
if (this.history.length > this.historyLimit) this.history.shift();
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
function structuredCloneSafe(v) {
|
|
2109
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
2110
|
+
return globalThis.structuredClone(v);
|
|
2111
|
+
}
|
|
2112
|
+
return JSON.parse(JSON.stringify(v));
|
|
2113
|
+
}
|
|
2114
|
+
function toStringSet(v) {
|
|
2115
|
+
if (!v) return /* @__PURE__ */ new Set();
|
|
2116
|
+
if (v instanceof Set) return new Set(Array.from(v).map(String));
|
|
2117
|
+
return new Set(v.map(String));
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/core/fallback.ts
|
|
2121
|
+
var DEFAULT_SETTINGS = {
|
|
2122
|
+
requireConstraintFit: true,
|
|
2123
|
+
ratePolicy: { kind: "lte_primary" },
|
|
2124
|
+
selectionStrategy: "priority",
|
|
2125
|
+
mode: "strict"
|
|
2126
|
+
};
|
|
2127
|
+
function resolveServiceFallback(params) {
|
|
2128
|
+
var _a, _b, _c, _d, _e;
|
|
2129
|
+
const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
|
|
2130
|
+
const { primary, nodeId, tagId, services } = params;
|
|
2131
|
+
const fb = (_b = params.fallbacks) != null ? _b : {};
|
|
2132
|
+
const tried = [];
|
|
2133
|
+
const lists = [];
|
|
2134
|
+
if (nodeId && ((_c = fb.nodes) == null ? void 0 : _c[nodeId])) lists.push(fb.nodes[nodeId]);
|
|
2135
|
+
if ((_d = fb.global) == null ? void 0 : _d[primary]) lists.push(fb.global[primary]);
|
|
2136
|
+
const primaryRate = rateOf(services, primary);
|
|
2137
|
+
for (const list of lists) {
|
|
2138
|
+
for (const cand of list) {
|
|
2139
|
+
if (tried.includes(cand)) continue;
|
|
2140
|
+
tried.push(cand);
|
|
2141
|
+
const candCap = (_e = services[Number(cand)]) != null ? _e : services[cand];
|
|
2142
|
+
if (!candCap) continue;
|
|
2143
|
+
if (!passesRate(s.ratePolicy, primaryRate, candCap.rate)) continue;
|
|
2144
|
+
if (s.requireConstraintFit && tagId) {
|
|
2145
|
+
const ok = satisfiesTagConstraints(tagId, params, candCap);
|
|
2146
|
+
if (!ok) continue;
|
|
2147
|
+
}
|
|
2148
|
+
return cand;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
function collectFailedFallbacks(props, services, settings) {
|
|
2154
|
+
var _a, _b, _c;
|
|
2155
|
+
const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
|
|
2156
|
+
const out = [];
|
|
2157
|
+
const fb = (_a = props.fallbacks) != null ? _a : {};
|
|
2158
|
+
const primaryRate = (p) => rateOf(services, p);
|
|
2159
|
+
for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
|
|
2160
|
+
const { primary, tagContexts } = primaryForNode(props, nodeId);
|
|
2161
|
+
if (!primary) {
|
|
2162
|
+
out.push({
|
|
2163
|
+
scope: "node",
|
|
2164
|
+
nodeId,
|
|
2165
|
+
primary: "",
|
|
2166
|
+
candidate: "",
|
|
2167
|
+
reason: "no_primary"
|
|
2168
|
+
});
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
for (const cand of list) {
|
|
2172
|
+
const cap = getCap(services, cand);
|
|
2173
|
+
if (!cap) {
|
|
2174
|
+
out.push({
|
|
2175
|
+
scope: "node",
|
|
2176
|
+
nodeId,
|
|
2177
|
+
primary,
|
|
2178
|
+
candidate: cand,
|
|
2179
|
+
reason: "unknown_service"
|
|
2180
|
+
});
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (String(cand) === String(primary)) {
|
|
2184
|
+
out.push({
|
|
2185
|
+
scope: "node",
|
|
2186
|
+
nodeId,
|
|
2187
|
+
primary,
|
|
2188
|
+
candidate: cand,
|
|
2189
|
+
reason: "cycle"
|
|
2190
|
+
});
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
|
|
2194
|
+
out.push({
|
|
2195
|
+
scope: "node",
|
|
2196
|
+
nodeId,
|
|
2197
|
+
primary,
|
|
2198
|
+
candidate: cand,
|
|
2199
|
+
reason: "rate_violation"
|
|
2200
|
+
});
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (tagContexts.length === 0) {
|
|
2204
|
+
out.push({
|
|
2205
|
+
scope: "node",
|
|
2206
|
+
nodeId,
|
|
2207
|
+
primary,
|
|
2208
|
+
candidate: cand,
|
|
2209
|
+
reason: "no_tag_context"
|
|
2210
|
+
});
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
let anyPass = false;
|
|
2214
|
+
let anyFail = false;
|
|
2215
|
+
for (const tagId of tagContexts) {
|
|
2216
|
+
const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
|
|
2217
|
+
if (ok) anyPass = true;
|
|
2218
|
+
else {
|
|
2219
|
+
anyFail = true;
|
|
2220
|
+
out.push({
|
|
2221
|
+
scope: "node",
|
|
2222
|
+
nodeId,
|
|
2223
|
+
primary,
|
|
2224
|
+
candidate: cand,
|
|
2225
|
+
tagContext: tagId,
|
|
2226
|
+
reason: "constraint_mismatch"
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
void anyPass;
|
|
2231
|
+
void anyFail;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
|
|
2235
|
+
for (const cand of list) {
|
|
2236
|
+
const cap = getCap(services, cand);
|
|
2237
|
+
if (!cap) {
|
|
2238
|
+
out.push({
|
|
2239
|
+
scope: "global",
|
|
2240
|
+
primary,
|
|
2241
|
+
candidate: cand,
|
|
2242
|
+
reason: "unknown_service"
|
|
2243
|
+
});
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
if (String(cand) === String(primary)) {
|
|
2247
|
+
out.push({
|
|
2248
|
+
scope: "global",
|
|
2249
|
+
primary,
|
|
2250
|
+
candidate: cand,
|
|
2251
|
+
reason: "cycle"
|
|
2252
|
+
});
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
|
|
2256
|
+
out.push({
|
|
2257
|
+
scope: "global",
|
|
2258
|
+
primary,
|
|
2259
|
+
candidate: cand,
|
|
2260
|
+
reason: "rate_violation"
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return out;
|
|
2266
|
+
}
|
|
2267
|
+
function rateOf(map, id) {
|
|
2268
|
+
var _a;
|
|
2269
|
+
if (id === void 0 || id === null) return void 0;
|
|
2270
|
+
const c = getCap(map, id);
|
|
2271
|
+
return (_a = c == null ? void 0 : c.rate) != null ? _a : void 0;
|
|
2272
|
+
}
|
|
2273
|
+
function passesRate(policy, primaryRate, candRate) {
|
|
2274
|
+
if (typeof candRate !== "number" || !Number.isFinite(candRate))
|
|
2275
|
+
return false;
|
|
2276
|
+
if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
|
|
2277
|
+
return false;
|
|
2278
|
+
switch (policy.kind) {
|
|
2279
|
+
case "lte_primary":
|
|
2280
|
+
return candRate <= primaryRate;
|
|
2281
|
+
case "within_pct":
|
|
2282
|
+
return candRate <= primaryRate * (1 + policy.pct / 100);
|
|
2283
|
+
case "at_least_pct_lower":
|
|
2284
|
+
return candRate <= primaryRate * (1 - policy.pct / 100);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function getCap(map, id) {
|
|
2288
|
+
const direct = map[id];
|
|
2289
|
+
if (direct) return direct;
|
|
2290
|
+
const strKey = String(id);
|
|
2291
|
+
const byStr = map[strKey];
|
|
2292
|
+
if (byStr) return byStr;
|
|
2293
|
+
const n = typeof id === "number" ? id : typeof id === "string" ? Number(id) : Number.NaN;
|
|
2294
|
+
if (Number.isFinite(n)) {
|
|
2295
|
+
const byNum = map[n];
|
|
2296
|
+
if (byNum) return byNum;
|
|
2297
|
+
}
|
|
2298
|
+
return void 0;
|
|
2299
|
+
}
|
|
2300
|
+
function isCapFlagEnabled(cap, flagId) {
|
|
2301
|
+
var _a, _b;
|
|
2302
|
+
const fromFlags = (_b = (_a = cap.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
|
|
2303
|
+
if (fromFlags === true) return true;
|
|
2304
|
+
if (fromFlags === false) return false;
|
|
2305
|
+
const legacy = cap[flagId];
|
|
2306
|
+
return legacy === true;
|
|
2307
|
+
}
|
|
2308
|
+
function satisfiesTagConstraints(tagId, ctx, cap) {
|
|
2309
|
+
const tag = ctx.props.filters.find((t) => t.id === tagId);
|
|
2310
|
+
const eff = tag == null ? void 0 : tag.constraints;
|
|
2311
|
+
if (!eff) return true;
|
|
2312
|
+
for (const [key, value] of Object.entries(eff)) {
|
|
2313
|
+
if (value === true && !isCapFlagEnabled(cap, key)) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
function primaryForNode(props, nodeId) {
|
|
2320
|
+
const tag = props.filters.find((t) => t.id === nodeId);
|
|
2321
|
+
if (tag) {
|
|
2322
|
+
return { primary: tag.service_id, tagContexts: [tag.id] };
|
|
2323
|
+
}
|
|
2324
|
+
const field = props.fields.find(
|
|
2325
|
+
(f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId)
|
|
2326
|
+
);
|
|
2327
|
+
if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
|
|
2328
|
+
const opt = field.options.find((o) => o.id === nodeId);
|
|
2329
|
+
const contexts = bindIdsToArray(field.bind_id);
|
|
2330
|
+
return { primary: opt.service_id, tagContexts: contexts };
|
|
2331
|
+
}
|
|
2332
|
+
function bindIdsToArray(bind) {
|
|
2333
|
+
if (!bind) return [];
|
|
2334
|
+
return Array.isArray(bind) ? bind.slice() : [bind];
|
|
2335
|
+
}
|
|
2336
|
+
function getEligibleFallbacks(params) {
|
|
2337
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2338
|
+
const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
|
|
2339
|
+
const { primary, nodeId, tagId, services } = params;
|
|
2340
|
+
const fb = (_b = params.fallbacks) != null ? _b : {};
|
|
2341
|
+
const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
|
|
2342
|
+
excludes.add(String(primary));
|
|
2343
|
+
const unique = (_d = params.unique) != null ? _d : true;
|
|
2344
|
+
const lists = [];
|
|
2345
|
+
if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
|
|
2346
|
+
if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
|
|
2347
|
+
if (!lists.length) return [];
|
|
2348
|
+
const primaryRate = rateOf(services, primary);
|
|
2349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2350
|
+
const eligible = [];
|
|
2351
|
+
for (const list of lists) {
|
|
2352
|
+
for (const cand of list) {
|
|
2353
|
+
const key = String(cand);
|
|
2354
|
+
if (excludes.has(key)) continue;
|
|
2355
|
+
if (unique && seen.has(key)) continue;
|
|
2356
|
+
seen.add(key);
|
|
2357
|
+
const cap = getCap(services, cand);
|
|
2358
|
+
if (!cap) continue;
|
|
2359
|
+
if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
|
|
2360
|
+
if (s.requireConstraintFit && tagId) {
|
|
2361
|
+
const ok = satisfiesTagConstraints(
|
|
2362
|
+
tagId,
|
|
2363
|
+
{ props: params.props, services },
|
|
2364
|
+
cap
|
|
2365
|
+
);
|
|
2366
|
+
if (!ok) continue;
|
|
2367
|
+
}
|
|
2368
|
+
eligible.push(cand);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (s.selectionStrategy === "cheapest") {
|
|
2372
|
+
eligible.sort((a, b) => {
|
|
2373
|
+
var _a2, _b2;
|
|
2374
|
+
const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
|
|
2375
|
+
const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
|
|
2376
|
+
return ra - rb;
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
if (typeof params.limit === "number" && params.limit >= 0) {
|
|
2380
|
+
return eligible.slice(0, params.limit);
|
|
2381
|
+
}
|
|
2382
|
+
return eligible;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/core/rate-coherence.ts
|
|
2386
|
+
function validateRateCoherenceDeep(params) {
|
|
2387
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2388
|
+
const { builder, services, tagId } = params;
|
|
2389
|
+
const ratePolicy = (_a = params.ratePolicy) != null ? _a : { kind: "lte_primary" };
|
|
2390
|
+
const props = builder.getProps();
|
|
2391
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
2392
|
+
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2393
|
+
const tagById = new Map(((_c = props.filters) != null ? _c : []).map((t) => [t.id, t]));
|
|
2394
|
+
const tag = tagById.get(tagId);
|
|
2395
|
+
const baselineFieldIds = builder.visibleFields(tagId, []);
|
|
2396
|
+
const baselineFields = baselineFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2397
|
+
const anchors = [];
|
|
2398
|
+
for (const f of baselineFields) {
|
|
2399
|
+
if (!isButton(f)) continue;
|
|
2400
|
+
if (Array.isArray(f.options) && f.options.length) {
|
|
2401
|
+
for (const o of f.options) {
|
|
2402
|
+
anchors.push({
|
|
2403
|
+
kind: "option",
|
|
2404
|
+
id: o.id,
|
|
2405
|
+
fieldId: f.id,
|
|
2406
|
+
label: (_d = o.label) != null ? _d : o.id,
|
|
2407
|
+
service_id: numberOrUndefined(o.service_id)
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
} else {
|
|
2411
|
+
anchors.push({
|
|
2412
|
+
kind: "field",
|
|
2413
|
+
id: f.id,
|
|
2414
|
+
fieldId: f.id,
|
|
2415
|
+
label: (_e = f.label) != null ? _e : f.id,
|
|
2416
|
+
service_id: numberOrUndefined(f.service_id)
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
const diags = [];
|
|
2421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2422
|
+
for (const anchor of anchors) {
|
|
2423
|
+
const selectedKeys = anchor.kind === "option" ? [`${anchor.fieldId}::${anchor.id}`] : [anchor.fieldId];
|
|
2424
|
+
const vgFieldIds = builder.visibleFields(tagId, selectedKeys);
|
|
2425
|
+
const vgFields = vgFieldIds.map((fid) => fieldById.get(fid)).filter(Boolean);
|
|
2426
|
+
const baseCandidates = [];
|
|
2427
|
+
for (const f of vgFields) {
|
|
2428
|
+
if (!isButton(f)) continue;
|
|
2429
|
+
if (Array.isArray(f.options) && f.options.length) {
|
|
2430
|
+
for (const o of f.options) {
|
|
2431
|
+
const sid = numberOrUndefined(o.service_id);
|
|
2432
|
+
const role = normalizeRole(o.pricing_role, "base");
|
|
2433
|
+
if (sid == null || role !== "base") continue;
|
|
2434
|
+
const r = rateOf2(services, sid);
|
|
2435
|
+
if (!isFiniteNumber2(r)) continue;
|
|
2436
|
+
baseCandidates.push({
|
|
2437
|
+
kind: "option",
|
|
2438
|
+
id: o.id,
|
|
2439
|
+
label: (_f = o.label) != null ? _f : o.id,
|
|
2440
|
+
service_id: sid,
|
|
2441
|
+
rate: r
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
const sid = numberOrUndefined(f.service_id);
|
|
2446
|
+
const role = normalizeRole(f.pricing_role, "base");
|
|
2447
|
+
if (sid == null || role !== "base") continue;
|
|
2448
|
+
const r = rateOf2(services, sid);
|
|
2449
|
+
if (!isFiniteNumber2(r)) continue;
|
|
2450
|
+
baseCandidates.push({
|
|
2451
|
+
kind: "field",
|
|
2452
|
+
id: f.id,
|
|
2453
|
+
label: (_g = f.label) != null ? _g : f.id,
|
|
2454
|
+
service_id: sid,
|
|
2455
|
+
rate: r
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (baseCandidates.length === 0) continue;
|
|
2460
|
+
const anchorPrimary = anchor.service_id != null ? pickByServiceId(baseCandidates, anchor.service_id) : void 0;
|
|
2461
|
+
const primary = anchorPrimary ? anchorPrimary : baseCandidates[0];
|
|
2462
|
+
for (const cand of baseCandidates) {
|
|
2463
|
+
if (sameService(primary, cand)) continue;
|
|
2464
|
+
if (!rateOkWithPolicy(ratePolicy, cand.rate, primary.rate)) {
|
|
2465
|
+
const key = dedupeKey(tagId, anchor, primary, cand, ratePolicy);
|
|
2466
|
+
if (seen.has(key)) continue;
|
|
2467
|
+
seen.add(key);
|
|
2468
|
+
diags.push({
|
|
2469
|
+
scope: "visible_group",
|
|
2470
|
+
tagId,
|
|
2471
|
+
primary,
|
|
2472
|
+
offender: {
|
|
2473
|
+
kind: cand.kind,
|
|
2474
|
+
id: cand.id,
|
|
2475
|
+
label: cand.label,
|
|
2476
|
+
service_id: cand.service_id,
|
|
2477
|
+
rate: cand.rate
|
|
2478
|
+
},
|
|
2479
|
+
policy: ratePolicy.kind,
|
|
2480
|
+
policyPct: "pct" in ratePolicy ? ratePolicy.pct : void 0,
|
|
2481
|
+
message: explainRateMismatch(
|
|
2482
|
+
ratePolicy,
|
|
2483
|
+
primary.rate,
|
|
2484
|
+
cand.rate,
|
|
2485
|
+
describeLabel(tag)
|
|
2486
|
+
),
|
|
2487
|
+
simulationAnchor: {
|
|
2488
|
+
kind: anchor.kind,
|
|
2489
|
+
id: anchor.id,
|
|
2490
|
+
fieldId: anchor.fieldId,
|
|
2491
|
+
label: anchor.label
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return diags;
|
|
2498
|
+
}
|
|
2499
|
+
function isButton(f) {
|
|
2500
|
+
if (f.button === true) return true;
|
|
2501
|
+
return Array.isArray(f.options) && f.options.length > 0;
|
|
2502
|
+
}
|
|
2503
|
+
function normalizeRole(role, d) {
|
|
2504
|
+
return role === "utility" || role === "base" ? role : d;
|
|
2505
|
+
}
|
|
2506
|
+
function numberOrUndefined(v) {
|
|
2507
|
+
const n = Number(v);
|
|
2508
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2509
|
+
}
|
|
2510
|
+
function isFiniteNumber2(v) {
|
|
2511
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
2512
|
+
}
|
|
2513
|
+
function rateOf2(map, id) {
|
|
2514
|
+
var _a;
|
|
2515
|
+
if (id === void 0 || id === null) return void 0;
|
|
2516
|
+
const cap = (_a = map[Number(id)]) != null ? _a : map[id];
|
|
2517
|
+
return cap == null ? void 0 : cap.rate;
|
|
2518
|
+
}
|
|
2519
|
+
function pickByServiceId(arr, sid) {
|
|
2520
|
+
return arr.find((x) => x.service_id === sid);
|
|
2521
|
+
}
|
|
2522
|
+
function sameService(a, b) {
|
|
2523
|
+
return a.service_id === b.service_id;
|
|
2524
|
+
}
|
|
2525
|
+
function rateOkWithPolicy(policy, candRate, primaryRate) {
|
|
2526
|
+
var _a, _b;
|
|
2527
|
+
const rp = policy != null ? policy : { kind: "lte_primary" };
|
|
2528
|
+
switch (rp.kind) {
|
|
2529
|
+
case "lte_primary":
|
|
2530
|
+
return candRate <= primaryRate;
|
|
2531
|
+
case "within_pct": {
|
|
2532
|
+
const pct = Math.max(0, (_a = rp.pct) != null ? _a : 0);
|
|
2533
|
+
return candRate <= primaryRate * (1 + pct / 100);
|
|
2534
|
+
}
|
|
2535
|
+
case "at_least_pct_lower": {
|
|
2536
|
+
const pct = Math.max(0, (_b = rp.pct) != null ? _b : 0);
|
|
2537
|
+
return candRate <= primaryRate * (1 - pct / 100);
|
|
2538
|
+
}
|
|
2539
|
+
default:
|
|
2540
|
+
return candRate <= primaryRate;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
function describeLabel(tag) {
|
|
2544
|
+
var _a, _b;
|
|
2545
|
+
const tagName = (_b = (_a = tag == null ? void 0 : tag.label) != null ? _a : tag == null ? void 0 : tag.id) != null ? _b : "tag";
|
|
2546
|
+
return `${tagName}`;
|
|
2547
|
+
}
|
|
2548
|
+
function explainRateMismatch(policy, primary, candidate, where) {
|
|
2549
|
+
switch (policy.kind) {
|
|
2550
|
+
case "lte_primary":
|
|
2551
|
+
return `Rate coherence failed (${where}): candidate ${candidate} must be \u2264 primary ${primary}.`;
|
|
2552
|
+
case "within_pct":
|
|
2553
|
+
return `Rate coherence failed (${where}): candidate ${candidate} must be within ${policy.pct}% of primary ${primary}.`;
|
|
2554
|
+
case "at_least_pct_lower":
|
|
2555
|
+
return `Rate coherence failed (${where}): candidate ${candidate} must be at least ${policy.pct}% lower than primary ${primary}.`;
|
|
2556
|
+
default:
|
|
2557
|
+
return `Rate coherence failed (${where}): candidate ${candidate} mismatches primary ${primary}.`;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function dedupeKey(tagId, anchor, primary, cand, rp) {
|
|
2561
|
+
const rpKey = rp.kind + ("pct" in rp && typeof rp.pct === "number" ? `:${rp.pct}` : "");
|
|
2562
|
+
return `${tagId}|${anchor.kind}:${anchor.id}|p${primary.service_id}|c${cand.service_id}:${cand.id}|${rpKey}`;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/core/tag-relations.ts
|
|
2566
|
+
var toId = (x) => typeof x === "string" ? x : x.id;
|
|
2567
|
+
var toBindList = (b) => {
|
|
2568
|
+
if (!b) return [];
|
|
2569
|
+
return typeof b === "string" ? [b] : [...b];
|
|
2570
|
+
};
|
|
2571
|
+
function createNodeIndex(builder) {
|
|
2572
|
+
var _a, _b, _c, _d;
|
|
2573
|
+
const props = builder.getProps();
|
|
2574
|
+
const tags = (_a = props.filters) != null ? _a : [];
|
|
2575
|
+
const fields = (_b = props.fields) != null ? _b : [];
|
|
2576
|
+
const tagById = new Map(tags.map((t) => [t.id, t]));
|
|
2577
|
+
const fieldById = new Map(fields.map((f) => [f.id, f]));
|
|
2578
|
+
const optionById = /* @__PURE__ */ new Map();
|
|
2579
|
+
const optionOwnerFieldId = /* @__PURE__ */ new Map();
|
|
2580
|
+
for (const f of fields) {
|
|
2581
|
+
for (const o of (_c = f.options) != null ? _c : []) {
|
|
2582
|
+
optionById.set(o.id, o);
|
|
2583
|
+
optionOwnerFieldId.set(o.id, f.id);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
const parentById = /* @__PURE__ */ new Map();
|
|
2587
|
+
const childrenById = /* @__PURE__ */ new Map();
|
|
2588
|
+
for (const t of tags) {
|
|
2589
|
+
parentById.set(t.id, t.bind_id);
|
|
2590
|
+
if (t.bind_id) {
|
|
2591
|
+
const arr = (_d = childrenById.get(t.bind_id)) != null ? _d : [];
|
|
2592
|
+
arr.push(t.id);
|
|
2593
|
+
childrenById.set(t.bind_id, arr);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const nodeCache = /* @__PURE__ */ new Map();
|
|
2597
|
+
const tagNodeCache = /* @__PURE__ */ new Map();
|
|
2598
|
+
const fieldNodeCache = /* @__PURE__ */ new Map();
|
|
2599
|
+
const optionNodeCache = /* @__PURE__ */ new Map();
|
|
2600
|
+
const tagAncestorsCache = /* @__PURE__ */ new Map();
|
|
2601
|
+
const tagChildrenCache = /* @__PURE__ */ new Map();
|
|
2602
|
+
const lineageIdsCache = /* @__PURE__ */ new Map();
|
|
2603
|
+
const fieldBindIdsCache = /* @__PURE__ */ new Map();
|
|
2604
|
+
const fieldOwnerTagsCache = /* @__PURE__ */ new Map();
|
|
2605
|
+
const emptySet = Object.freeze(/* @__PURE__ */ new Set());
|
|
2606
|
+
const isFieldBoundDirectToTag = (fieldId, tagId) => {
|
|
2607
|
+
const field = fieldById.get(fieldId);
|
|
2608
|
+
if (!field) return false;
|
|
2609
|
+
const bind = field.bind_id;
|
|
2610
|
+
if (!bind) return false;
|
|
2611
|
+
if (typeof bind === "string") return bind === tagId;
|
|
2612
|
+
return bind.includes(tagId);
|
|
2613
|
+
};
|
|
2614
|
+
const resolveDescendants = (id, includes, tagId, forceSimple) => {
|
|
2615
|
+
if (!tagId || forceSimple) {
|
|
2616
|
+
const results2 = [];
|
|
2617
|
+
for (const incId of includes) {
|
|
2618
|
+
const node = getField(incId);
|
|
2619
|
+
if (node) results2.push(node);
|
|
2620
|
+
}
|
|
2621
|
+
return Object.freeze(results2);
|
|
2622
|
+
}
|
|
2623
|
+
const results = [];
|
|
2624
|
+
const visible = builder.visibleFields(tagId, [id]);
|
|
2625
|
+
for (const fieldId of visible) {
|
|
2626
|
+
const node = getField(fieldId);
|
|
2627
|
+
if (!node) continue;
|
|
2628
|
+
const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, tagId);
|
|
2629
|
+
results.push(explicit ? node : { ...node, isInherited: true });
|
|
2630
|
+
}
|
|
2631
|
+
return Object.freeze(results);
|
|
2632
|
+
};
|
|
2633
|
+
const lineageIds = (tagId) => {
|
|
2634
|
+
const cached = lineageIdsCache.get(tagId);
|
|
2635
|
+
if (cached) return cached;
|
|
2636
|
+
const out = /* @__PURE__ */ new Set();
|
|
2637
|
+
const guard = /* @__PURE__ */ new Set();
|
|
2638
|
+
let cur = tagId;
|
|
2639
|
+
while (cur && !guard.has(cur)) {
|
|
2640
|
+
out.add(cur);
|
|
2641
|
+
guard.add(cur);
|
|
2642
|
+
cur = parentById.get(cur);
|
|
2643
|
+
}
|
|
2644
|
+
lineageIdsCache.set(tagId, out);
|
|
2645
|
+
return out;
|
|
2646
|
+
};
|
|
2647
|
+
const pathBetween = (fromId, toId2) => {
|
|
2648
|
+
if (fromId === toId2) return [fromId];
|
|
2649
|
+
const guard = /* @__PURE__ */ new Set();
|
|
2650
|
+
const stack = [];
|
|
2651
|
+
let cur = toId2;
|
|
2652
|
+
while (cur && !guard.has(cur)) {
|
|
2653
|
+
stack.push(cur);
|
|
2654
|
+
if (cur === fromId) return stack.reverse();
|
|
2655
|
+
guard.add(cur);
|
|
2656
|
+
cur = parentById.get(cur);
|
|
2657
|
+
}
|
|
2658
|
+
return null;
|
|
2659
|
+
};
|
|
2660
|
+
const relationBetween = (baseId, otherId) => {
|
|
2661
|
+
if (baseId === otherId)
|
|
2662
|
+
return { kind: "self", path: [baseId], index: 0, direct: false };
|
|
2663
|
+
const down = pathBetween(baseId, otherId);
|
|
2664
|
+
if (down)
|
|
2665
|
+
return {
|
|
2666
|
+
kind: "descendant",
|
|
2667
|
+
path: down,
|
|
2668
|
+
index: down.length - 1,
|
|
2669
|
+
direct: down.length === 2
|
|
2670
|
+
};
|
|
2671
|
+
const up = pathBetween(otherId, baseId);
|
|
2672
|
+
if (up) {
|
|
2673
|
+
const p = up.slice().reverse();
|
|
2674
|
+
return {
|
|
2675
|
+
kind: "ancestor",
|
|
2676
|
+
path: p,
|
|
2677
|
+
index: p.length - 1,
|
|
2678
|
+
direct: p.length === 2
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
return { kind: "unrelated", path: [], index: -1, direct: false };
|
|
2682
|
+
};
|
|
2683
|
+
const makeAncestry = (selfOwnerTagIds) => {
|
|
2684
|
+
return (baseTag) => {
|
|
2685
|
+
const baseId = toId(baseTag);
|
|
2686
|
+
const hits = [];
|
|
2687
|
+
for (const ownerId of selfOwnerTagIds) {
|
|
2688
|
+
const owner = getTag(ownerId);
|
|
2689
|
+
if (!owner) continue;
|
|
2690
|
+
const rel = relationBetween(baseId, ownerId);
|
|
2691
|
+
hits.push({
|
|
2692
|
+
kind: rel.kind,
|
|
2693
|
+
node: owner,
|
|
2694
|
+
index: rel.index,
|
|
2695
|
+
direct: rel.direct,
|
|
2696
|
+
path: rel.path
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
return hits;
|
|
2700
|
+
};
|
|
2701
|
+
};
|
|
2702
|
+
const makeAncestryFns = (ancestryFn) => ({
|
|
2703
|
+
ancestry: ancestryFn,
|
|
2704
|
+
isAncestorOf(baseTag) {
|
|
2705
|
+
return ancestryFn(baseTag).some((h) => h.kind === "ancestor");
|
|
2706
|
+
},
|
|
2707
|
+
isDescendantOf(baseTag) {
|
|
2708
|
+
return ancestryFn(baseTag).some((h) => h.kind === "descendant");
|
|
2709
|
+
},
|
|
2710
|
+
isWithin(baseTag) {
|
|
2711
|
+
return ancestryFn(baseTag).some(
|
|
2712
|
+
(h) => h.kind === "self" || h.kind === "descendant"
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
const getTag = (id) => {
|
|
2717
|
+
var _a2, _b2;
|
|
2718
|
+
const cached = tagNodeCache.get(id);
|
|
2719
|
+
if (cached) return cached;
|
|
2720
|
+
const raw = tagById.get(id);
|
|
2721
|
+
if (!raw) return void 0;
|
|
2722
|
+
const includes = Object.freeze(new Set((_a2 = raw.includes) != null ? _a2 : []));
|
|
2723
|
+
const excludes = Object.freeze(new Set((_b2 = raw.excludes) != null ? _b2 : []));
|
|
2724
|
+
const ancestryFn = makeAncestry([id]);
|
|
2725
|
+
const node = {
|
|
2726
|
+
kind: "tag",
|
|
2727
|
+
id,
|
|
2728
|
+
raw,
|
|
2729
|
+
includes,
|
|
2730
|
+
excludes,
|
|
2731
|
+
...makeAncestryFns(ancestryFn),
|
|
2732
|
+
parent() {
|
|
2733
|
+
const pid = parentById.get(id);
|
|
2734
|
+
return pid ? getTag(pid) : void 0;
|
|
2735
|
+
},
|
|
2736
|
+
children() {
|
|
2737
|
+
var _a3;
|
|
2738
|
+
const cachedChildren = tagChildrenCache.get(id);
|
|
2739
|
+
if (cachedChildren) return cachedChildren;
|
|
2740
|
+
const childIds = (_a3 = childrenById.get(id)) != null ? _a3 : [];
|
|
2741
|
+
const arr = childIds.map((cid) => getTag(cid)).filter(Boolean);
|
|
2742
|
+
const frozen = Object.freeze(arr);
|
|
2743
|
+
tagChildrenCache.set(id, frozen);
|
|
2744
|
+
return frozen;
|
|
2745
|
+
},
|
|
2746
|
+
ancestors() {
|
|
2747
|
+
var _a3;
|
|
2748
|
+
const cachedAnc = tagAncestorsCache.get(id);
|
|
2749
|
+
if (cachedAnc) return cachedAnc;
|
|
2750
|
+
const rows = [];
|
|
2751
|
+
const guard = /* @__PURE__ */ new Set();
|
|
2752
|
+
let cur = parentById.get(id);
|
|
2753
|
+
let index = 1;
|
|
2754
|
+
while (cur && !guard.has(cur)) {
|
|
2755
|
+
const n = getTag(cur);
|
|
2756
|
+
if (!n) break;
|
|
2757
|
+
const p = (_a3 = pathBetween(id, cur)) != null ? _a3 : [id, cur];
|
|
2758
|
+
rows.push({ node: n, direct: index === 1, index, path: p });
|
|
2759
|
+
guard.add(cur);
|
|
2760
|
+
cur = parentById.get(cur);
|
|
2761
|
+
index++;
|
|
2762
|
+
}
|
|
2763
|
+
const frozen = Object.freeze(rows);
|
|
2764
|
+
tagAncestorsCache.set(id, frozen);
|
|
2765
|
+
return frozen;
|
|
2766
|
+
},
|
|
2767
|
+
pathTo(descendant) {
|
|
2768
|
+
const to = toId(descendant);
|
|
2769
|
+
return pathBetween(id, to);
|
|
2770
|
+
},
|
|
2771
|
+
isBoundTo(tagId) {
|
|
2772
|
+
const tid = toId(tagId);
|
|
2773
|
+
if (tid === id) return true;
|
|
2774
|
+
return parentById.get(id) === tid;
|
|
2775
|
+
},
|
|
2776
|
+
getDescendant(descendantId) {
|
|
2777
|
+
return this.getDescendants().find((item) => item.id == descendantId);
|
|
2778
|
+
},
|
|
2779
|
+
getDescendants() {
|
|
2780
|
+
const results = [];
|
|
2781
|
+
const visible = builder.visibleFields(id);
|
|
2782
|
+
for (const fieldId of visible) {
|
|
2783
|
+
const node2 = getField(fieldId);
|
|
2784
|
+
if (!node2) continue;
|
|
2785
|
+
const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, id);
|
|
2786
|
+
results.push(explicit ? node2 : { ...node2, isInherited: true });
|
|
2787
|
+
}
|
|
2788
|
+
return Object.freeze(results);
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
tagNodeCache.set(id, node);
|
|
2792
|
+
nodeCache.set(id, node);
|
|
2793
|
+
return node;
|
|
2794
|
+
};
|
|
2795
|
+
const getField = (id) => {
|
|
2796
|
+
var _a2, _b2, _c2, _d2;
|
|
2797
|
+
const cached = fieldNodeCache.get(id);
|
|
2798
|
+
if (cached) return cached;
|
|
2799
|
+
const raw = fieldById.get(id);
|
|
2800
|
+
if (!raw) return void 0;
|
|
2801
|
+
const isButton2 = raw.button === true;
|
|
2802
|
+
const includes = isButton2 ? Object.freeze(new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])) : emptySet;
|
|
2803
|
+
const excludes = isButton2 ? Object.freeze(new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])) : emptySet;
|
|
2804
|
+
const bindIds = () => {
|
|
2805
|
+
const cachedBind = fieldBindIdsCache.get(id);
|
|
2806
|
+
if (cachedBind) return cachedBind;
|
|
2807
|
+
const res = Object.freeze(toBindList(raw.bind_id));
|
|
2808
|
+
fieldBindIdsCache.set(id, res);
|
|
2809
|
+
return res;
|
|
2810
|
+
};
|
|
2811
|
+
const ancestryFn = makeAncestry(bindIds());
|
|
2812
|
+
const node = {
|
|
2813
|
+
kind: "field",
|
|
2814
|
+
id,
|
|
2815
|
+
raw,
|
|
2816
|
+
includes,
|
|
2817
|
+
excludes,
|
|
2818
|
+
...makeAncestryFns(ancestryFn),
|
|
2819
|
+
bindIds,
|
|
2820
|
+
ownerTags() {
|
|
2821
|
+
const cachedOwners = fieldOwnerTagsCache.get(id);
|
|
2822
|
+
if (cachedOwners) return cachedOwners;
|
|
2823
|
+
const owners = bindIds().map((tid) => getTag(tid)).filter(Boolean);
|
|
2824
|
+
const frozen = Object.freeze(owners);
|
|
2825
|
+
fieldOwnerTagsCache.set(id, frozen);
|
|
2826
|
+
return frozen;
|
|
2827
|
+
},
|
|
2828
|
+
isBoundTo(tagId) {
|
|
2829
|
+
const tid = toId(tagId);
|
|
2830
|
+
const lin = lineageIds(tid);
|
|
2831
|
+
for (const b of bindIds()) if (lin.has(b)) return true;
|
|
2832
|
+
return false;
|
|
2833
|
+
},
|
|
2834
|
+
getDescendant(descendantId, context) {
|
|
2835
|
+
return this.getDescendants(context).find((item) => item.id == descendantId);
|
|
2836
|
+
},
|
|
2837
|
+
getDescendants(tagId) {
|
|
2838
|
+
return resolveDescendants(id, includes, tagId, !isButton2);
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
fieldNodeCache.set(id, node);
|
|
2842
|
+
nodeCache.set(id, node);
|
|
2843
|
+
return node;
|
|
2844
|
+
};
|
|
2845
|
+
const getOption = (id) => {
|
|
2846
|
+
var _a2, _b2, _c2, _d2;
|
|
2847
|
+
const cached = optionNodeCache.get(id);
|
|
2848
|
+
if (cached) return cached;
|
|
2849
|
+
const raw = optionById.get(id);
|
|
2850
|
+
const ownerFieldId = optionOwnerFieldId.get(id);
|
|
2851
|
+
if (!raw || !ownerFieldId) return void 0;
|
|
2852
|
+
const owner = getField(ownerFieldId);
|
|
2853
|
+
if (!owner) return void 0;
|
|
2854
|
+
const includes = Object.freeze(
|
|
2855
|
+
new Set((_b2 = (_a2 = props.includes_for_buttons) == null ? void 0 : _a2[id]) != null ? _b2 : [])
|
|
2856
|
+
);
|
|
2857
|
+
const excludes = Object.freeze(
|
|
2858
|
+
new Set((_d2 = (_c2 = props.excludes_for_buttons) == null ? void 0 : _c2[id]) != null ? _d2 : [])
|
|
2859
|
+
);
|
|
2860
|
+
const node = {
|
|
2861
|
+
kind: "option",
|
|
2862
|
+
id,
|
|
2863
|
+
raw,
|
|
2864
|
+
includes,
|
|
2865
|
+
excludes,
|
|
2866
|
+
...makeAncestryFns((baseTag) => owner.ancestry(baseTag)),
|
|
2867
|
+
field() {
|
|
2868
|
+
return owner;
|
|
2869
|
+
},
|
|
2870
|
+
ownerTags() {
|
|
2871
|
+
return owner.ownerTags();
|
|
2872
|
+
},
|
|
2873
|
+
isBoundTo(tagId) {
|
|
2874
|
+
return owner.isBoundTo(tagId);
|
|
2875
|
+
},
|
|
2876
|
+
getDescendant(descendantId, context) {
|
|
2877
|
+
return this.getDescendants(context).find((item) => item.id == descendantId);
|
|
2878
|
+
},
|
|
2879
|
+
getDescendants(tagId) {
|
|
2880
|
+
return resolveDescendants(id, includes, tagId);
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
optionNodeCache.set(id, node);
|
|
2884
|
+
nodeCache.set(id, node);
|
|
2885
|
+
return node;
|
|
2886
|
+
};
|
|
2887
|
+
const getNode = (input) => {
|
|
2888
|
+
var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
|
|
2889
|
+
if (typeof input !== "string") {
|
|
2890
|
+
if ("bind_id" in input && !("type" in input))
|
|
2891
|
+
return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
|
|
2892
|
+
if ("type" in input)
|
|
2893
|
+
return (_b2 = getField(input.id)) != null ? _b2 : mkUnknown(input.id);
|
|
2894
|
+
return (_c2 = getOption(input.id)) != null ? _c2 : mkUnknown(input.id);
|
|
2895
|
+
}
|
|
2896
|
+
const cached = nodeCache.get(input);
|
|
2897
|
+
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);
|
|
2903
|
+
};
|
|
2904
|
+
const mkUnknown = (id) => {
|
|
2905
|
+
const u = {
|
|
2906
|
+
kind: "unknown",
|
|
2907
|
+
id,
|
|
2908
|
+
includes: emptySet,
|
|
2909
|
+
excludes: emptySet,
|
|
2910
|
+
...makeAncestryFns(() => [])
|
|
2911
|
+
};
|
|
2912
|
+
nodeCache.set(id, u);
|
|
2913
|
+
return u;
|
|
2914
|
+
};
|
|
2915
|
+
return {
|
|
2916
|
+
getNode,
|
|
2917
|
+
getTag,
|
|
2918
|
+
getField,
|
|
2919
|
+
getOption
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2923
|
+
0 && (module.exports = {
|
|
2924
|
+
collectFailedFallbacks,
|
|
2925
|
+
createBuilder,
|
|
2926
|
+
createNodeIndex,
|
|
2927
|
+
getEligibleFallbacks,
|
|
2928
|
+
normalise,
|
|
2929
|
+
resolveServiceFallback,
|
|
2930
|
+
validate,
|
|
2931
|
+
validateRateCoherenceDeep
|
|
2932
|
+
});
|
|
2933
|
+
//# sourceMappingURL=index.cjs.map
|