@traffical/core 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dedup/decision-dedup.d.ts +74 -0
- package/dist/dedup/decision-dedup.d.ts.map +1 -0
- package/dist/dedup/decision-dedup.js +132 -0
- package/dist/dedup/decision-dedup.js.map +1 -0
- package/dist/dedup/index.d.ts +5 -0
- package/dist/dedup/index.d.ts.map +1 -0
- package/dist/dedup/index.js +5 -0
- package/dist/dedup/index.js.map +1 -0
- package/dist/edge/client.d.ts +109 -0
- package/dist/edge/client.d.ts.map +1 -0
- package/dist/edge/client.js +154 -0
- package/dist/edge/client.js.map +1 -0
- package/dist/edge/index.d.ts +7 -0
- package/dist/edge/index.d.ts.map +1 -0
- package/dist/edge/index.js +7 -0
- package/dist/edge/index.js.map +1 -0
- package/dist/hashing/bucket.d.ts +56 -0
- package/dist/hashing/bucket.d.ts.map +1 -0
- package/dist/hashing/bucket.js +89 -0
- package/dist/hashing/bucket.js.map +1 -0
- package/dist/hashing/fnv1a.d.ts +17 -0
- package/dist/hashing/fnv1a.d.ts.map +1 -0
- package/dist/hashing/fnv1a.js +27 -0
- package/dist/hashing/fnv1a.js.map +1 -0
- package/dist/hashing/index.d.ts +8 -0
- package/dist/hashing/index.d.ts.map +1 -0
- package/dist/hashing/index.js +8 -0
- package/dist/hashing/index.js.map +1 -0
- package/dist/ids/index.d.ts +83 -0
- package/dist/ids/index.d.ts.map +1 -0
- package/dist/ids/index.js +165 -0
- package/dist/ids/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/resolution/conditions.d.ts +81 -0
- package/dist/resolution/conditions.d.ts.map +1 -0
- package/dist/resolution/conditions.js +197 -0
- package/dist/resolution/conditions.js.map +1 -0
- package/dist/resolution/engine.d.ts +54 -0
- package/dist/resolution/engine.d.ts.map +1 -0
- package/dist/resolution/engine.js +382 -0
- package/dist/resolution/engine.js.map +1 -0
- package/dist/resolution/index.d.ts +8 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +10 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/types/index.d.ts +440 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +51 -0
- package/src/dedup/decision-dedup.ts +175 -0
- package/src/dedup/index.ts +6 -0
- package/src/edge/client.ts +256 -0
- package/src/edge/index.ts +16 -0
- package/src/hashing/bucket.ts +115 -0
- package/src/hashing/fnv1a.test.ts +87 -0
- package/src/hashing/fnv1a.ts +31 -0
- package/src/hashing/index.ts +15 -0
- package/src/ids/index.ts +221 -0
- package/src/index.ts +136 -0
- package/src/resolution/conditions.ts +253 -0
- package/src/resolution/engine.test.ts +242 -0
- package/src/resolution/engine.ts +480 -0
- package/src/resolution/index.ts +32 -0
- package/src/types/index.ts +508 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution Engine
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for parameter resolution using layered config and policies.
|
|
5
|
+
* Implements the Google-inspired layering system where:
|
|
6
|
+
* - Parameters are partitioned into layers
|
|
7
|
+
* - Within a layer, only one policy can be active for a unit
|
|
8
|
+
* - Across layers, policies overlap freely (different parameters)
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (lowest to highest priority):
|
|
11
|
+
* 1. Caller defaults (always safe fallback)
|
|
12
|
+
* 2. Parameter defaults (from bundle)
|
|
13
|
+
* 3. Layer policies (each parameter belongs to exactly one layer)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ConfigBundle,
|
|
18
|
+
BundleParameter,
|
|
19
|
+
BundlePolicy,
|
|
20
|
+
BundleAllocation,
|
|
21
|
+
Context,
|
|
22
|
+
ParameterValue,
|
|
23
|
+
DecisionResult,
|
|
24
|
+
LayerResolution,
|
|
25
|
+
Id,
|
|
26
|
+
} from "../types/index.js";
|
|
27
|
+
import { computeBucket, findMatchingAllocation } from "../hashing/bucket.js";
|
|
28
|
+
import { evaluateConditions } from "./conditions.js";
|
|
29
|
+
import { generateDecisionId } from "../ids/index.js";
|
|
30
|
+
import { fnv1a } from "../hashing/fnv1a.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Filters context to only include fields allowed by matched policies.
|
|
34
|
+
* Collects the union of all allowed fields from policies with contextLogging config.
|
|
35
|
+
*
|
|
36
|
+
* @param context - The full evaluation context
|
|
37
|
+
* @param policies - Matched policies from resolution
|
|
38
|
+
* @returns Filtered context with only allowed fields, or undefined if no fields allowed
|
|
39
|
+
*/
|
|
40
|
+
function filterContext(
|
|
41
|
+
context: Context,
|
|
42
|
+
policies: BundlePolicy[]
|
|
43
|
+
): Context | undefined {
|
|
44
|
+
// Collect union of all allowed fields from matched policies
|
|
45
|
+
const allowedFields = new Set<string>();
|
|
46
|
+
for (const policy of policies) {
|
|
47
|
+
if (policy.contextLogging?.allowedFields) {
|
|
48
|
+
for (const field of policy.contextLogging.allowedFields) {
|
|
49
|
+
allowedFields.add(field);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If no fields are allowed, return undefined
|
|
55
|
+
if (allowedFields.size === 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Filter context to only include allowed fields
|
|
60
|
+
const filtered: Context = {};
|
|
61
|
+
for (const field of allowedFields) {
|
|
62
|
+
if (field in context) {
|
|
63
|
+
filtered[field] = context[field];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return undefined if no fields matched
|
|
68
|
+
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Per-Entity Resolution Helpers
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds an entity ID from context using the policy's entityKeys.
|
|
77
|
+
*
|
|
78
|
+
* @param entityKeys - Array of context keys that identify the entity
|
|
79
|
+
* @param context - The evaluation context
|
|
80
|
+
* @returns Entity ID string, or null if any key is missing
|
|
81
|
+
*/
|
|
82
|
+
function buildEntityId(entityKeys: string[], context: Context): string | null {
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
for (const key of entityKeys) {
|
|
85
|
+
const value = context[key];
|
|
86
|
+
if (value === undefined || value === null) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
parts.push(String(value));
|
|
90
|
+
}
|
|
91
|
+
return parts.join("_");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Performs deterministic weighted selection using a hash.
|
|
96
|
+
*
|
|
97
|
+
* Uses the entity ID + unit key + policy ID to deterministically select
|
|
98
|
+
* an allocation based on weights. This ensures the same entity always
|
|
99
|
+
* gets the same allocation for a given weight distribution.
|
|
100
|
+
*
|
|
101
|
+
* @param weights - Array of weights (should sum to 1.0)
|
|
102
|
+
* @param seed - Seed string for deterministic hashing
|
|
103
|
+
* @returns Index of selected allocation
|
|
104
|
+
*/
|
|
105
|
+
function weightedSelection(weights: number[], seed: string): number {
|
|
106
|
+
if (weights.length === 0) return 0;
|
|
107
|
+
if (weights.length === 1) return 0;
|
|
108
|
+
|
|
109
|
+
// Compute a deterministic random value in [0, 1) using hash
|
|
110
|
+
const hash = fnv1a(seed);
|
|
111
|
+
const random = (hash % 10000) / 10000;
|
|
112
|
+
|
|
113
|
+
// Select based on cumulative weights
|
|
114
|
+
let cumulative = 0;
|
|
115
|
+
for (let i = 0; i < weights.length; i++) {
|
|
116
|
+
cumulative += weights[i];
|
|
117
|
+
if (random < cumulative) {
|
|
118
|
+
return i;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fallback to last allocation (handles floating point edge cases)
|
|
123
|
+
return weights.length - 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates uniform weights for dynamic allocations.
|
|
128
|
+
*
|
|
129
|
+
* @param count - Number of allocations
|
|
130
|
+
* @returns Array of equal weights summing to 1.0
|
|
131
|
+
*/
|
|
132
|
+
function createUniformWeights(count: number): number[] {
|
|
133
|
+
if (count <= 0) return [];
|
|
134
|
+
const weight = 1 / count;
|
|
135
|
+
return Array(count).fill(weight);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gets entity weights from the bundle's entityState.
|
|
140
|
+
*
|
|
141
|
+
* @param bundle - The config bundle
|
|
142
|
+
* @param policyId - The policy ID
|
|
143
|
+
* @param entityId - The entity ID
|
|
144
|
+
* @param allocationCount - Number of allocations (for dynamic allocations)
|
|
145
|
+
* @returns Entity weights or uniform weights for cold start
|
|
146
|
+
*/
|
|
147
|
+
function getEntityWeights(
|
|
148
|
+
bundle: ConfigBundle,
|
|
149
|
+
policyId: Id,
|
|
150
|
+
entityId: string,
|
|
151
|
+
allocationCount: number
|
|
152
|
+
): number[] {
|
|
153
|
+
const policyState = bundle.entityState?.[policyId];
|
|
154
|
+
|
|
155
|
+
if (!policyState) {
|
|
156
|
+
// No state for this policy - use uniform weights
|
|
157
|
+
return createUniformWeights(allocationCount);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Try entity-specific weights first
|
|
161
|
+
const entityWeights = policyState.entities[entityId];
|
|
162
|
+
if (entityWeights && entityWeights.weights.length === allocationCount) {
|
|
163
|
+
return entityWeights.weights;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fall back to global prior
|
|
167
|
+
const globalWeights = policyState._global;
|
|
168
|
+
if (globalWeights && globalWeights.weights.length === allocationCount) {
|
|
169
|
+
return globalWeights.weights;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Last resort: uniform weights
|
|
173
|
+
return createUniformWeights(allocationCount);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves a per-entity policy using weighted selection.
|
|
178
|
+
*
|
|
179
|
+
* @param bundle - The config bundle
|
|
180
|
+
* @param policy - The policy with entityConfig
|
|
181
|
+
* @param context - The evaluation context
|
|
182
|
+
* @param unitKeyValue - The unit key value for hashing
|
|
183
|
+
* @returns The selected allocation and entity ID, or null if cannot resolve
|
|
184
|
+
*/
|
|
185
|
+
function resolvePerEntityPolicy(
|
|
186
|
+
bundle: ConfigBundle,
|
|
187
|
+
policy: BundlePolicy,
|
|
188
|
+
context: Context,
|
|
189
|
+
unitKeyValue: string
|
|
190
|
+
): { allocation: BundleAllocation; entityId: string } | null {
|
|
191
|
+
const entityConfig = policy.entityConfig;
|
|
192
|
+
if (!entityConfig) return null;
|
|
193
|
+
|
|
194
|
+
// Build entity ID from context
|
|
195
|
+
const entityId = buildEntityId(entityConfig.entityKeys, context);
|
|
196
|
+
if (!entityId) {
|
|
197
|
+
// Missing entity keys - cannot resolve
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Determine allocations
|
|
202
|
+
let allocations: BundleAllocation[];
|
|
203
|
+
let allocationCount: number;
|
|
204
|
+
|
|
205
|
+
if (entityConfig.dynamicAllocations) {
|
|
206
|
+
// Dynamic allocations from context
|
|
207
|
+
const countKey = entityConfig.dynamicAllocations.countKey;
|
|
208
|
+
const count = context[countKey];
|
|
209
|
+
if (typeof count !== "number" || count <= 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
allocationCount = Math.floor(count);
|
|
213
|
+
|
|
214
|
+
// Create synthetic allocations for dynamic mode
|
|
215
|
+
// Each allocation is an index (0, 1, 2, ..., count-1)
|
|
216
|
+
allocations = Array.from({ length: allocationCount }, (_, i) => ({
|
|
217
|
+
id: `${policy.id}_dynamic_${i}`,
|
|
218
|
+
name: String(i),
|
|
219
|
+
bucketRange: [0, 0] as [number, number], // Not used for per-entity
|
|
220
|
+
overrides: {}, // Overrides are applied differently for dynamic
|
|
221
|
+
}));
|
|
222
|
+
} else {
|
|
223
|
+
// Fixed allocations from policy
|
|
224
|
+
allocations = policy.allocations;
|
|
225
|
+
allocationCount = allocations.length;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (allocationCount === 0) return null;
|
|
229
|
+
|
|
230
|
+
// Get weights for this entity
|
|
231
|
+
const weights = getEntityWeights(bundle, policy.id, entityId, allocationCount);
|
|
232
|
+
|
|
233
|
+
// Deterministic weighted selection
|
|
234
|
+
const seed = `${entityId}:${unitKeyValue}:${policy.id}`;
|
|
235
|
+
const selectedIndex = weightedSelection(weights, seed);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
allocation: allocations[selectedIndex],
|
|
239
|
+
entityId,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Extracts the unit key value from context using the bundle's hashing config.
|
|
245
|
+
*
|
|
246
|
+
* @param bundle - The config bundle
|
|
247
|
+
* @param context - The evaluation context
|
|
248
|
+
* @returns The unit key value as a string, or null if not found
|
|
249
|
+
*/
|
|
250
|
+
export function getUnitKeyValue(
|
|
251
|
+
bundle: ConfigBundle,
|
|
252
|
+
context: Context
|
|
253
|
+
): string | null {
|
|
254
|
+
const value = context[bundle.hashing.unitKey];
|
|
255
|
+
|
|
256
|
+
if (value === undefined || value === null) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return String(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Internal resolution result with metadata.
|
|
265
|
+
*/
|
|
266
|
+
interface ResolutionResult<T> {
|
|
267
|
+
assignments: T;
|
|
268
|
+
unitKeyValue: string;
|
|
269
|
+
layers: LayerResolution[];
|
|
270
|
+
/** Matched policies with context logging config */
|
|
271
|
+
matchedPolicies: BundlePolicy[];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Internal function that performs parameter resolution with metadata tracking.
|
|
276
|
+
* This is the single source of truth for resolution logic.
|
|
277
|
+
*
|
|
278
|
+
* @param bundle - The config bundle (can be null if unavailable)
|
|
279
|
+
* @param context - The evaluation context
|
|
280
|
+
* @param defaults - Default values for parameters (required, used as fallback)
|
|
281
|
+
* @returns Resolution result with assignments and metadata
|
|
282
|
+
*/
|
|
283
|
+
function resolveInternal<T extends Record<string, ParameterValue>>(
|
|
284
|
+
bundle: ConfigBundle | null,
|
|
285
|
+
context: Context,
|
|
286
|
+
defaults: T
|
|
287
|
+
): ResolutionResult<T> {
|
|
288
|
+
// Start with caller defaults (always safe)
|
|
289
|
+
const assignments = { ...defaults } as Record<string, ParameterValue>;
|
|
290
|
+
const layers: LayerResolution[] = [];
|
|
291
|
+
const matchedPolicies: BundlePolicy[] = [];
|
|
292
|
+
|
|
293
|
+
// If no bundle, return defaults with empty metadata
|
|
294
|
+
if (!bundle) {
|
|
295
|
+
return { assignments: assignments as T, unitKeyValue: "", layers, matchedPolicies };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Try to get unit key
|
|
299
|
+
const unitKeyValue = getUnitKeyValue(bundle, context);
|
|
300
|
+
if (!unitKeyValue) {
|
|
301
|
+
// Missing unit key - return defaults
|
|
302
|
+
return { assignments: assignments as T, unitKeyValue: "", layers, matchedPolicies };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Get requested parameter keys from defaults
|
|
306
|
+
const requestedKeys = new Set(Object.keys(defaults));
|
|
307
|
+
|
|
308
|
+
// Filter bundle parameters to only those requested
|
|
309
|
+
const params = bundle.parameters.filter((p) => requestedKeys.has(p.key));
|
|
310
|
+
|
|
311
|
+
// Apply bundle defaults (overrides caller defaults)
|
|
312
|
+
for (const param of params) {
|
|
313
|
+
if (param.key in assignments) {
|
|
314
|
+
assignments[param.key] = param.default;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Group by layer
|
|
319
|
+
const paramsByLayer = new Map<Id, BundleParameter[]>();
|
|
320
|
+
for (const param of params) {
|
|
321
|
+
const existing = paramsByLayer.get(param.layerId) || [];
|
|
322
|
+
existing.push(param);
|
|
323
|
+
paramsByLayer.set(param.layerId, existing);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Process each layer (order doesn't matter since params are partitioned)
|
|
327
|
+
for (const layer of bundle.layers) {
|
|
328
|
+
const layerParams = paramsByLayer.get(layer.id);
|
|
329
|
+
if (!layerParams || layerParams.length === 0) continue;
|
|
330
|
+
|
|
331
|
+
// Compute bucket
|
|
332
|
+
const bucket = computeBucket(
|
|
333
|
+
unitKeyValue,
|
|
334
|
+
layer.id,
|
|
335
|
+
bundle.hashing.bucketCount
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
let matchedPolicy: BundlePolicy | undefined;
|
|
339
|
+
let matchedAllocation: BundleAllocation | undefined;
|
|
340
|
+
|
|
341
|
+
// Find matching policy
|
|
342
|
+
for (const policy of layer.policies) {
|
|
343
|
+
if (policy.state !== "running") continue;
|
|
344
|
+
|
|
345
|
+
// Check bucket eligibility BEFORE conditions (performance optimization)
|
|
346
|
+
// This enables non-overlapping experiments within a layer
|
|
347
|
+
if (policy.eligibleBucketRange) {
|
|
348
|
+
const { start, end } = policy.eligibleBucketRange;
|
|
349
|
+
if (bucket < start || bucket > end) {
|
|
350
|
+
continue; // User's bucket not eligible for this policy
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!evaluateConditions(policy.conditions, context)) continue;
|
|
355
|
+
|
|
356
|
+
// Check if this is a per-entity policy
|
|
357
|
+
if (policy.entityConfig && policy.entityConfig.resolutionMode === "bundle") {
|
|
358
|
+
const result = resolvePerEntityPolicy(bundle, policy, context, unitKeyValue);
|
|
359
|
+
if (result) {
|
|
360
|
+
matchedPolicy = policy;
|
|
361
|
+
matchedAllocation = result.allocation;
|
|
362
|
+
|
|
363
|
+
// Track matched policy for context filtering
|
|
364
|
+
matchedPolicies.push(policy);
|
|
365
|
+
|
|
366
|
+
// Apply overrides
|
|
367
|
+
// For dynamic allocations, the allocation name IS the value
|
|
368
|
+
if (policy.entityConfig.dynamicAllocations) {
|
|
369
|
+
// For per-entity dynamic policies, we return the selected index
|
|
370
|
+
// The SDK caller should use metadata.allocationName to get the index
|
|
371
|
+
// No parameter overrides to apply in this mode
|
|
372
|
+
} else {
|
|
373
|
+
// For fixed allocations, apply normal overrides
|
|
374
|
+
for (const [key, value] of Object.entries(result.allocation.overrides)) {
|
|
375
|
+
if (key in assignments) {
|
|
376
|
+
assignments[key] = value;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
break; // Only one policy per layer
|
|
381
|
+
}
|
|
382
|
+
} else if (policy.entityConfig && policy.entityConfig.resolutionMode === "edge") {
|
|
383
|
+
// Edge mode: skip for now, will be handled by SDK's async resolution
|
|
384
|
+
// In synchronous resolution, we fall through to bucket-based resolution
|
|
385
|
+
// The SDK should use async decide() for edge mode policies
|
|
386
|
+
continue;
|
|
387
|
+
} else {
|
|
388
|
+
// Standard bucket-based resolution
|
|
389
|
+
const allocation = findMatchingAllocation(bucket, policy.allocations);
|
|
390
|
+
if (allocation) {
|
|
391
|
+
matchedPolicy = policy;
|
|
392
|
+
matchedAllocation = allocation;
|
|
393
|
+
|
|
394
|
+
// Track matched policy for context filtering
|
|
395
|
+
matchedPolicies.push(policy);
|
|
396
|
+
|
|
397
|
+
// Apply overrides
|
|
398
|
+
for (const [key, value] of Object.entries(allocation.overrides)) {
|
|
399
|
+
if (key in assignments) {
|
|
400
|
+
assignments[key] = value;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
break; // Only one policy per layer
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
layers.push({
|
|
409
|
+
layerId: layer.id,
|
|
410
|
+
bucket,
|
|
411
|
+
policyId: matchedPolicy?.id,
|
|
412
|
+
allocationId: matchedAllocation?.id,
|
|
413
|
+
allocationName: matchedAllocation?.name,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { assignments: assignments as T, unitKeyValue, layers, matchedPolicies };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolves parameters with required defaults as fallback.
|
|
422
|
+
* This is the primary SDK function that guarantees safe defaults.
|
|
423
|
+
*
|
|
424
|
+
* Resolution priority (highest wins):
|
|
425
|
+
* 1. Policy overrides (from bundle)
|
|
426
|
+
* 2. Parameter defaults (from bundle)
|
|
427
|
+
* 3. Caller defaults (always safe fallback)
|
|
428
|
+
*
|
|
429
|
+
* @param bundle - The config bundle (can be null if unavailable)
|
|
430
|
+
* @param context - The evaluation context
|
|
431
|
+
* @param defaults - Default values for parameters (required, used as fallback)
|
|
432
|
+
* @returns Resolved parameter assignments (always returns safe values with inferred types)
|
|
433
|
+
*/
|
|
434
|
+
export function resolveParameters<T extends Record<string, ParameterValue>>(
|
|
435
|
+
bundle: ConfigBundle | null,
|
|
436
|
+
context: Context,
|
|
437
|
+
defaults: T
|
|
438
|
+
): T {
|
|
439
|
+
return resolveInternal(bundle, context, defaults).assignments;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Makes a decision with full metadata for tracking.
|
|
444
|
+
* Requires defaults for graceful degradation.
|
|
445
|
+
*
|
|
446
|
+
* Resolution priority (highest wins):
|
|
447
|
+
* 1. Policy overrides (from bundle)
|
|
448
|
+
* 2. Parameter defaults (from bundle)
|
|
449
|
+
* 3. Caller defaults (always safe fallback)
|
|
450
|
+
*
|
|
451
|
+
* @param bundle - The config bundle (can be null if unavailable)
|
|
452
|
+
* @param context - The evaluation context
|
|
453
|
+
* @param defaults - Default values for parameters (required, used as fallback)
|
|
454
|
+
* @returns Decision result with metadata (always returns safe values)
|
|
455
|
+
*/
|
|
456
|
+
export function decide<T extends Record<string, ParameterValue>>(
|
|
457
|
+
bundle: ConfigBundle | null,
|
|
458
|
+
context: Context,
|
|
459
|
+
defaults: T
|
|
460
|
+
): DecisionResult {
|
|
461
|
+
const { assignments, unitKeyValue, layers, matchedPolicies } = resolveInternal(
|
|
462
|
+
bundle,
|
|
463
|
+
context,
|
|
464
|
+
defaults
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Filter context based on matched policies' contextLogging config
|
|
468
|
+
const filteredContext = filterContext(context, matchedPolicies);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
decisionId: generateDecisionId(),
|
|
472
|
+
assignments,
|
|
473
|
+
metadata: {
|
|
474
|
+
timestamp: new Date().toISOString(),
|
|
475
|
+
unitKeyValue,
|
|
476
|
+
layers,
|
|
477
|
+
filteredContext,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution Module
|
|
3
|
+
*
|
|
4
|
+
* Exports all resolution-related functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
resolveParameters,
|
|
9
|
+
decide,
|
|
10
|
+
getUnitKeyValue,
|
|
11
|
+
} from "./engine.js";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
evaluateCondition,
|
|
15
|
+
evaluateConditions,
|
|
16
|
+
// Condition builders
|
|
17
|
+
eq,
|
|
18
|
+
neq,
|
|
19
|
+
inValues,
|
|
20
|
+
notIn,
|
|
21
|
+
gt,
|
|
22
|
+
gte,
|
|
23
|
+
lt,
|
|
24
|
+
lte,
|
|
25
|
+
contains,
|
|
26
|
+
startsWith,
|
|
27
|
+
endsWith,
|
|
28
|
+
regex,
|
|
29
|
+
exists,
|
|
30
|
+
notExists,
|
|
31
|
+
} from "./conditions.js";
|
|
32
|
+
|