@sovr/engine 3.2.0 → 3.4.0

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/index.mjs CHANGED
@@ -1,3 +1,2004 @@
1
+ // src/expressionTree.ts
2
+ function compileFromJSON(rule) {
3
+ const ast = parseNode(rule);
4
+ const variables = extractVariables(ast);
5
+ return {
6
+ ast,
7
+ evaluate: (ctx) => evaluateNode(ast, ctx),
8
+ toString: () => nodeToString(ast),
9
+ variables
10
+ };
11
+ }
12
+ function parseNode(rule) {
13
+ if (rule === null || rule === void 0) {
14
+ return { type: "LITERAL", value: false };
15
+ }
16
+ if (rule.and && Array.isArray(rule.and)) {
17
+ return {
18
+ type: "AND",
19
+ children: rule.and.map((child) => parseNode(child))
20
+ };
21
+ }
22
+ if (rule.or && Array.isArray(rule.or)) {
23
+ return {
24
+ type: "OR",
25
+ children: rule.or.map((child) => parseNode(child))
26
+ };
27
+ }
28
+ if (rule.not) {
29
+ return {
30
+ type: "NOT",
31
+ child: parseNode(rule.not)
32
+ };
33
+ }
34
+ if (rule.fn) {
35
+ return {
36
+ type: "COMPARE",
37
+ operator: rule.op || "==",
38
+ left: {
39
+ type: "FUNCTION",
40
+ name: rule.fn,
41
+ args: (rule.args || []).map(
42
+ (a) => typeof a === "string" ? { type: "VARIABLE", path: a } : { type: "LITERAL", value: a }
43
+ )
44
+ },
45
+ right: { type: "LITERAL", value: rule.value }
46
+ };
47
+ }
48
+ if (rule.field && rule.op !== void 0) {
49
+ return {
50
+ type: "COMPARE",
51
+ operator: rule.op,
52
+ left: { type: "VARIABLE", path: rule.field },
53
+ right: { type: "LITERAL", value: rule.value }
54
+ };
55
+ }
56
+ if (typeof rule === "boolean") {
57
+ return { type: "LITERAL", value: rule };
58
+ }
59
+ throw new Error(`[ExpressionTree] Invalid rule node: ${JSON.stringify(rule)}`);
60
+ }
61
+ function evaluateNode(node, ctx) {
62
+ switch (node.type) {
63
+ case "AND":
64
+ return node.children.every((child) => evaluateNode(child, ctx));
65
+ case "OR":
66
+ return node.children.some((child) => evaluateNode(child, ctx));
67
+ case "NOT":
68
+ return !evaluateNode(node.child, ctx);
69
+ case "COMPARE":
70
+ return evaluateComparison(node, ctx);
71
+ case "LITERAL":
72
+ return Boolean(node.value);
73
+ case "VARIABLE":
74
+ return Boolean(resolveVariable(node.path, ctx));
75
+ case "FUNCTION":
76
+ return Boolean(evaluateFunction(node, ctx));
77
+ default:
78
+ return false;
79
+ }
80
+ }
81
+ function evaluateComparison(node, ctx) {
82
+ const leftVal = resolveNode(node.left, ctx);
83
+ const rightVal = resolveNode(node.right, ctx);
84
+ switch (node.operator) {
85
+ case "==":
86
+ return leftVal == rightVal;
87
+ case "!=":
88
+ return leftVal != rightVal;
89
+ case ">":
90
+ return Number(leftVal) > Number(rightVal);
91
+ case "<":
92
+ return Number(leftVal) < Number(rightVal);
93
+ case ">=":
94
+ return Number(leftVal) >= Number(rightVal);
95
+ case "<=":
96
+ return Number(leftVal) <= Number(rightVal);
97
+ case "in":
98
+ return Array.isArray(rightVal) ? rightVal.includes(leftVal) : false;
99
+ case "not_in":
100
+ return Array.isArray(rightVal) ? !rightVal.includes(leftVal) : true;
101
+ case "contains":
102
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.includes(rightVal) : false;
103
+ case "starts_with":
104
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.startsWith(rightVal) : false;
105
+ case "ends_with":
106
+ return typeof leftVal === "string" && typeof rightVal === "string" ? leftVal.endsWith(rightVal) : false;
107
+ case "matches":
108
+ try {
109
+ return typeof leftVal === "string" && typeof rightVal === "string" ? new RegExp(rightVal).test(leftVal) : false;
110
+ } catch {
111
+ return false;
112
+ }
113
+ default:
114
+ return false;
115
+ }
116
+ }
117
+ function resolveNode(node, ctx) {
118
+ switch (node.type) {
119
+ case "LITERAL":
120
+ return node.value;
121
+ case "VARIABLE":
122
+ return resolveVariable(node.path, ctx);
123
+ case "FUNCTION":
124
+ return evaluateFunction(node, ctx);
125
+ default:
126
+ return evaluateNode(node, ctx);
127
+ }
128
+ }
129
+ function resolveVariable(path, ctx) {
130
+ const parts = path.split(".");
131
+ let current = ctx;
132
+ for (const part of parts) {
133
+ if (current === null || current === void 0) return void 0;
134
+ current = current[part];
135
+ }
136
+ return current;
137
+ }
138
+ var BUILT_IN_FUNCTIONS = {
139
+ now_hour: () => (/* @__PURE__ */ new Date()).getHours(),
140
+ now_day: () => (/* @__PURE__ */ new Date()).getDay(),
141
+ // 0=Sunday
142
+ now_timestamp: () => Date.now(),
143
+ len: (args) => {
144
+ const val = args[0];
145
+ return typeof val === "string" || Array.isArray(val) ? val.length : 0;
146
+ },
147
+ lower: (args) => String(args[0] || "").toLowerCase(),
148
+ upper: (args) => String(args[0] || "").toUpperCase(),
149
+ abs: (args) => Math.abs(Number(args[0] || 0)),
150
+ min: (args) => Math.min(...args.map(Number)),
151
+ max: (args) => Math.max(...args.map(Number)),
152
+ coalesce: (args) => args.find((a) => a !== null && a !== void 0),
153
+ risk_level: (args) => {
154
+ const score = Number(args[0] || 0);
155
+ if (score >= 0.9) return "critical";
156
+ if (score >= 0.7) return "high";
157
+ if (score >= 0.4) return "medium";
158
+ return "low";
159
+ },
160
+ is_business_hours: () => {
161
+ const hour = (/* @__PURE__ */ new Date()).getHours();
162
+ const day = (/* @__PURE__ */ new Date()).getDay();
163
+ return day >= 1 && day <= 5 && hour >= 9 && hour < 18;
164
+ }
165
+ };
166
+ function registerFunction(name, fn) {
167
+ BUILT_IN_FUNCTIONS[name] = fn;
168
+ }
169
+ function evaluateFunction(node, ctx) {
170
+ const fn = BUILT_IN_FUNCTIONS[node.name];
171
+ if (!fn) {
172
+ console.warn(`[ExpressionTree] Unknown function: ${node.name}`);
173
+ return null;
174
+ }
175
+ const resolvedArgs = node.args.map((arg) => resolveNode(arg, ctx));
176
+ return fn(resolvedArgs, ctx);
177
+ }
178
+ function extractVariables(node) {
179
+ const vars = /* @__PURE__ */ new Set();
180
+ function walk(n) {
181
+ switch (n.type) {
182
+ case "VARIABLE":
183
+ vars.add(n.path);
184
+ break;
185
+ case "AND":
186
+ case "OR":
187
+ n.children.forEach(walk);
188
+ break;
189
+ case "NOT":
190
+ walk(n.child);
191
+ break;
192
+ case "COMPARE":
193
+ walk(n.left);
194
+ walk(n.right);
195
+ break;
196
+ case "FUNCTION":
197
+ n.args.forEach(walk);
198
+ break;
199
+ }
200
+ }
201
+ walk(node);
202
+ return Array.from(vars);
203
+ }
204
+ function nodeToString(node, depth = 0) {
205
+ const indent = " ".repeat(depth);
206
+ switch (node.type) {
207
+ case "AND":
208
+ return `${indent}AND(
209
+ ${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
210
+ ${indent})`;
211
+ case "OR":
212
+ return `${indent}OR(
213
+ ${node.children.map((c) => nodeToString(c, depth + 1)).join(",\n")}
214
+ ${indent})`;
215
+ case "NOT":
216
+ return `${indent}NOT(
217
+ ${nodeToString(node.child, depth + 1)}
218
+ ${indent})`;
219
+ case "COMPARE":
220
+ return `${indent}${nodeToString(node.left, 0)} ${node.operator} ${nodeToString(node.right, 0)}`;
221
+ case "LITERAL":
222
+ return `${indent}${JSON.stringify(node.value)}`;
223
+ case "VARIABLE":
224
+ return `${indent}\${${node.path}}`;
225
+ case "FUNCTION":
226
+ return `${indent}${node.name}(${node.args.map((a) => nodeToString(a, 0)).join(", ")})`;
227
+ default:
228
+ return `${indent}???`;
229
+ }
230
+ }
231
+ var ruleCache = /* @__PURE__ */ new Map();
232
+ function compileRuleSet(rules) {
233
+ ruleCache.clear();
234
+ for (const rule of rules) {
235
+ try {
236
+ ruleCache.set(rule.id, compileFromJSON(rule.condition));
237
+ } catch (err) {
238
+ console.error(`[ExpressionTree] Failed to compile rule ${rule.id}: ${rule.name}`, err);
239
+ }
240
+ }
241
+ }
242
+ function evaluateRules(rules, ctx) {
243
+ const sorted = [...rules].sort((a, b) => b.priority - a.priority);
244
+ const results = [];
245
+ let matched = null;
246
+ for (const rule of sorted) {
247
+ const start = performance.now();
248
+ const compiled = ruleCache.get(rule.id) || compileFromJSON(rule.condition);
249
+ let isMatch = false;
250
+ try {
251
+ isMatch = compiled.evaluate(ctx);
252
+ } catch (err) {
253
+ console.warn(`[ExpressionTree] Rule ${rule.id} evaluation error`, err);
254
+ }
255
+ const elapsed = performance.now() - start;
256
+ results.push({
257
+ ruleId: rule.id,
258
+ ruleName: rule.name,
259
+ matched: isMatch,
260
+ action: rule.action,
261
+ evaluationTimeMs: elapsed
262
+ });
263
+ if (isMatch && !matched) {
264
+ matched = rule;
265
+ }
266
+ }
267
+ return { matched, results };
268
+ }
269
+
270
+ // src/adaptiveThreshold.ts
271
+ var DEFAULT_THRESHOLDS = [
272
+ {
273
+ name: "risk_score",
274
+ currentValue: 0.7,
275
+ minValue: 0.3,
276
+ maxValue: 0.95,
277
+ alpha: 0.1,
278
+ maxStepPercent: 5,
279
+ lastAdjustedAt: 0,
280
+ directionLock: null,
281
+ consecutiveAdjustments: 0
282
+ },
283
+ {
284
+ name: "cost_limit_usd",
285
+ currentValue: 100,
286
+ minValue: 10,
287
+ maxValue: 1e4,
288
+ alpha: 0.05,
289
+ maxStepPercent: 10,
290
+ lastAdjustedAt: 0,
291
+ directionLock: null,
292
+ consecutiveAdjustments: 0
293
+ },
294
+ {
295
+ name: "latency_p99_ms",
296
+ currentValue: 5e3,
297
+ minValue: 1e3,
298
+ maxValue: 3e4,
299
+ alpha: 0.15,
300
+ maxStepPercent: 15,
301
+ lastAdjustedAt: 0,
302
+ directionLock: null,
303
+ consecutiveAdjustments: 0
304
+ },
305
+ {
306
+ name: "hallucination_score",
307
+ currentValue: 0.5,
308
+ minValue: 0.1,
309
+ maxValue: 0.9,
310
+ alpha: 0.08,
311
+ maxStepPercent: 5,
312
+ lastAdjustedAt: 0,
313
+ directionLock: null,
314
+ consecutiveAdjustments: 0
315
+ }
316
+ ];
317
+ var AdaptiveThresholdManager = class {
318
+ thresholds = /* @__PURE__ */ new Map();
319
+ feedbackBuffer = [];
320
+ maxBufferSize;
321
+ cooldownMs;
322
+ onAdjustment;
323
+ constructor(options = {}) {
324
+ this.maxBufferSize = options.maxBufferSize ?? 1e4;
325
+ this.cooldownMs = options.adjustmentCooldownMs ?? 5 * 60 * 1e3;
326
+ this.onAdjustment = options.onAdjustment;
327
+ const configs = options.thresholds ?? DEFAULT_THRESHOLDS;
328
+ for (const config of configs) {
329
+ this.thresholds.set(config.name, { ...config });
330
+ }
331
+ }
332
+ // ========== Core API ==========
333
+ /** Get a threshold by name */
334
+ getThreshold(name) {
335
+ const t = this.thresholds.get(name);
336
+ return t ? { ...t } : void 0;
337
+ }
338
+ /** Get all thresholds */
339
+ getAllThresholds() {
340
+ return Array.from(this.thresholds.values()).map((t) => ({ ...t }));
341
+ }
342
+ /** Record a decision feedback sample */
343
+ recordFeedback(feedback) {
344
+ this.feedbackBuffer.push(feedback);
345
+ if (this.feedbackBuffer.length > this.maxBufferSize) {
346
+ this.feedbackBuffer.splice(0, this.feedbackBuffer.length - this.maxBufferSize);
347
+ }
348
+ }
349
+ /**
350
+ * Adjust a single threshold based on feedback (EWMA algorithm).
351
+ * Returns null if no adjustment is needed (cooldown, insufficient samples, etc.).
352
+ */
353
+ async adjustThreshold(thresholdName, windowHours = 24) {
354
+ const config = this.thresholds.get(thresholdName);
355
+ if (!config) return null;
356
+ if (Date.now() - config.lastAdjustedAt < this.cooldownMs) return null;
357
+ const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
358
+ const windowFeedback = this.feedbackBuffer.filter(
359
+ (f) => f.thresholdName === thresholdName && f.timestamp >= windowStart
360
+ );
361
+ if (windowFeedback.length < 10) return null;
362
+ const fp = windowFeedback.filter((f) => f.outcome === "false_positive").length;
363
+ const fn = windowFeedback.filter((f) => f.outcome === "false_negative").length;
364
+ const total = windowFeedback.length;
365
+ const fpRate = fp / total;
366
+ const fnRate = fn / total;
367
+ let direction = "none";
368
+ let reason = "";
369
+ if (fpRate > 0.15 && fpRate > fnRate * 2) {
370
+ direction = "up";
371
+ reason = `False positive rate ${(fpRate * 100).toFixed(1)}% exceeds 15% threshold`;
372
+ } else if (fnRate > 0.1 && fnRate > fpRate * 2) {
373
+ direction = "down";
374
+ reason = `False negative rate ${(fnRate * 100).toFixed(1)}% exceeds 10% threshold`;
375
+ }
376
+ if (direction === "none") return null;
377
+ if (config.directionLock && config.directionLock !== direction && config.consecutiveAdjustments < 3) {
378
+ return null;
379
+ }
380
+ const errorRate = direction === "up" ? fpRate : fnRate;
381
+ const ewmaAdjustment = config.alpha * errorRate;
382
+ const maxStep = config.currentValue * (config.maxStepPercent / 100);
383
+ const actualStep = Math.min(ewmaAdjustment * config.currentValue, maxStep);
384
+ const previousValue = config.currentValue;
385
+ let newValue = direction === "up" ? config.currentValue + actualStep : config.currentValue - actualStep;
386
+ newValue = Math.max(config.minValue, Math.min(config.maxValue, newValue));
387
+ config.currentValue = newValue;
388
+ config.lastAdjustedAt = Date.now();
389
+ config.consecutiveAdjustments = config.directionLock === direction ? config.consecutiveAdjustments + 1 : 1;
390
+ config.directionLock = direction;
391
+ const result = {
392
+ thresholdName,
393
+ previousValue,
394
+ newValue,
395
+ reason,
396
+ metrics: { falsePositiveRate: fpRate, falseNegativeRate: fnRate, totalSamples: total, windowHours }
397
+ };
398
+ if (this.onAdjustment) {
399
+ try {
400
+ await this.onAdjustment(result);
401
+ } catch (err) {
402
+ console.warn("[AdaptiveThreshold] Audit callback failed", err);
403
+ }
404
+ }
405
+ return result;
406
+ }
407
+ /** Adjust all thresholds */
408
+ async adjustAll(windowHours = 24) {
409
+ const results = [];
410
+ for (const [name] of this.thresholds) {
411
+ const result = await this.adjustThreshold(name, windowHours);
412
+ if (result) results.push(result);
413
+ }
414
+ return results;
415
+ }
416
+ /** Manually override a threshold value */
417
+ setThresholdManual(name, value) {
418
+ const config = this.thresholds.get(name);
419
+ if (!config) return false;
420
+ if (value < config.minValue || value > config.maxValue) return false;
421
+ config.currentValue = value;
422
+ config.lastAdjustedAt = Date.now();
423
+ config.directionLock = null;
424
+ config.consecutiveAdjustments = 0;
425
+ return true;
426
+ }
427
+ /** Get feedback statistics for a threshold (or all) */
428
+ getFeedbackStats(thresholdName, windowHours = 24) {
429
+ const windowStart = Date.now() - windowHours * 60 * 60 * 1e3;
430
+ const filtered = this.feedbackBuffer.filter(
431
+ (f) => f.timestamp >= windowStart && (!thresholdName || f.thresholdName === thresholdName)
432
+ );
433
+ const tp = filtered.filter((f) => f.outcome === "true_positive").length;
434
+ const tn = filtered.filter((f) => f.outcome === "true_negative").length;
435
+ const fp = filtered.filter((f) => f.outcome === "false_positive").length;
436
+ const fn = filtered.filter((f) => f.outcome === "false_negative").length;
437
+ const total = filtered.length;
438
+ return {
439
+ total,
440
+ truePositive: tp,
441
+ trueNegative: tn,
442
+ falsePositive: fp,
443
+ falseNegative: fn,
444
+ accuracy: total > 0 ? (tp + tn) / total : 0,
445
+ precision: tp + fp > 0 ? tp / (tp + fp) : 0,
446
+ recall: tp + fn > 0 ? tp / (tp + fn) : 0
447
+ };
448
+ }
449
+ /** Clear the feedback buffer */
450
+ clearFeedback() {
451
+ this.feedbackBuffer.length = 0;
452
+ }
453
+ };
454
+
455
+ // src/featureSwitches.ts
456
+ var SOVR_FEATURE_SWITCHES = {
457
+ USE_NEW_RISK_ENGINE: {
458
+ key: "USE_NEW_RISK_ENGINE",
459
+ description: "Enable new risk engine (expression tree + adaptive threshold), replacing legacy static rule matching",
460
+ defaultValue: false,
461
+ category: "risk",
462
+ impactLevel: "high"
463
+ },
464
+ ENFORCE_COST_CAP: {
465
+ key: "ENFORCE_COST_CAP",
466
+ description: "Enforce cost cap \u2014 hard-reject execution when agent cost exceeds budget (not just warn)",
467
+ defaultValue: false,
468
+ category: "cost",
469
+ impactLevel: "high"
470
+ },
471
+ ENABLE_PROMPT_GUARD: {
472
+ key: "ENABLE_PROMPT_GUARD",
473
+ description: "Enable prompt injection guard \u2014 scan all LLM call prompts for injection attacks",
474
+ defaultValue: true,
475
+ category: "security",
476
+ impactLevel: "medium"
477
+ },
478
+ DISABLE_EVIDENCE_PAUSE: {
479
+ key: "DISABLE_EVIDENCE_PAUSE",
480
+ description: "Disable evidence pause \u2014 keep audit chain writing continuously even under high system load",
481
+ defaultValue: false,
482
+ category: "evidence",
483
+ impactLevel: "medium"
484
+ }
485
+ };
486
+ var FeatureSwitchesManager = class {
487
+ cache = /* @__PURE__ */ new Map();
488
+ cacheTtlMs;
489
+ onLoad;
490
+ onSave;
491
+ constructor(options = {}) {
492
+ this.cacheTtlMs = options.cacheTtlMs ?? 3e4;
493
+ this.onLoad = options.onLoad;
494
+ this.onSave = options.onSave;
495
+ }
496
+ /**
497
+ * Get a switch value (with cache + optional external load).
498
+ */
499
+ async getValue(key, tenantId = "default") {
500
+ const cacheKey = `${tenantId}:${key}`;
501
+ const cached = this.cache.get(cacheKey);
502
+ if (cached && Date.now() - cached.updatedAt < this.cacheTtlMs) {
503
+ return cached.value;
504
+ }
505
+ if (this.onLoad) {
506
+ try {
507
+ const loaded = await this.onLoad(key, tenantId);
508
+ if (loaded !== null) {
509
+ this.cache.set(cacheKey, { value: loaded, updatedAt: Date.now() });
510
+ return loaded;
511
+ }
512
+ } catch (err) {
513
+ console.warn(`[FeatureSwitch] Failed to load ${key}, using default`, err);
514
+ }
515
+ }
516
+ const def = SOVR_FEATURE_SWITCHES[key];
517
+ const defaultVal = def ? def.defaultValue : false;
518
+ this.cache.set(cacheKey, { value: defaultVal, updatedAt: Date.now() });
519
+ return defaultVal;
520
+ }
521
+ /**
522
+ * Set a switch value (updates cache + optional external save).
523
+ */
524
+ async setValue(key, value, tenantId = "default") {
525
+ const def = SOVR_FEATURE_SWITCHES[key];
526
+ if (!def) {
527
+ throw new Error(`Unknown feature switch: ${key}`);
528
+ }
529
+ const cacheKey = `${tenantId}:${key}`;
530
+ if (this.onSave) {
531
+ await this.onSave(key, value, tenantId);
532
+ }
533
+ this.cache.set(cacheKey, { value, updatedAt: Date.now() });
534
+ }
535
+ /**
536
+ * Get all switch states.
537
+ */
538
+ async getAll(tenantId = "default") {
539
+ const result = {};
540
+ for (const [key, def] of Object.entries(SOVR_FEATURE_SWITCHES)) {
541
+ const value = await this.getValue(key, tenantId);
542
+ result[key] = {
543
+ key,
544
+ value,
545
+ description: def.description,
546
+ category: def.category,
547
+ impactLevel: def.impactLevel,
548
+ isDefault: value === def.defaultValue
549
+ };
550
+ }
551
+ return result;
552
+ }
553
+ /** Clear the cache (for testing or forced refresh) */
554
+ clearCache() {
555
+ this.cache.clear();
556
+ }
557
+ // ========== Convenience Checkers ==========
558
+ async isNewRiskEngineEnabled(tenantId) {
559
+ return this.getValue("USE_NEW_RISK_ENGINE", tenantId);
560
+ }
561
+ async isCostCapEnforced(tenantId) {
562
+ return this.getValue("ENFORCE_COST_CAP", tenantId);
563
+ }
564
+ async isPromptGuardEnabled(tenantId) {
565
+ return this.getValue("ENABLE_PROMPT_GUARD", tenantId);
566
+ }
567
+ async isEvidencePauseDisabled(tenantId) {
568
+ return this.getValue("DISABLE_EVIDENCE_PAUSE", tenantId);
569
+ }
570
+ };
571
+
572
+ // src/pricingRules.ts
573
+ var PricingRulesEngine = class {
574
+ rules = [];
575
+ constructor(rules) {
576
+ if (rules) {
577
+ this.loadRules(rules);
578
+ }
579
+ }
580
+ /** Load rules (sorted by priority descending) */
581
+ loadRules(rules) {
582
+ this.rules = [...rules].filter((r) => r.isActive).sort((a, b) => b.priority - a.priority);
583
+ }
584
+ /** Add a single rule */
585
+ addRule(rule) {
586
+ this.rules.push(rule);
587
+ this.rules.sort((a, b) => b.priority - a.priority);
588
+ }
589
+ /** Get all loaded rules */
590
+ getRules() {
591
+ return this.rules;
592
+ }
593
+ /**
594
+ * Evaluate pricing for an action.
595
+ * Returns the computed cost and breakdown.
596
+ */
597
+ evaluate(request) {
598
+ const currentHour = request.currentHour ?? (/* @__PURE__ */ new Date()).getHours();
599
+ for (const rule of this.rules) {
600
+ if (rule.tier && rule.tier !== request.tier) continue;
601
+ if (!this.globMatch(rule.actionPattern, request.action)) continue;
602
+ let timeWindowActive = false;
603
+ if (rule.timeWindowStart >= 0 && rule.timeWindowEnd >= 0) {
604
+ if (rule.timeWindowStart <= rule.timeWindowEnd) {
605
+ timeWindowActive = currentHour >= rule.timeWindowStart && currentHour < rule.timeWindowEnd;
606
+ } else {
607
+ timeWindowActive = currentHour >= rule.timeWindowStart || currentHour < rule.timeWindowEnd;
608
+ }
609
+ if (!timeWindowActive) continue;
610
+ } else {
611
+ timeWindowActive = false;
612
+ }
613
+ let cost = rule.baseCostUsd * rule.multiplier;
614
+ let discountPercent = 0;
615
+ if (rule.volumeDiscountThreshold > 0 && request.currentVolume >= rule.volumeDiscountThreshold) {
616
+ discountPercent = rule.volumeDiscountPercent;
617
+ cost = cost * (1 - discountPercent / 100);
618
+ }
619
+ const parts = [`base=$${rule.baseCostUsd.toFixed(4)}`];
620
+ if (rule.multiplier !== 1) parts.push(`\xD7${rule.multiplier}`);
621
+ if (discountPercent > 0) parts.push(`-${discountPercent}% vol`);
622
+ if (timeWindowActive) parts.push(`[time:${rule.timeWindowStart}-${rule.timeWindowEnd}]`);
623
+ return {
624
+ costUsd: Math.max(0, cost),
625
+ baseCostUsd: rule.baseCostUsd,
626
+ multiplier: rule.multiplier,
627
+ volumeDiscountPercent: discountPercent,
628
+ timeWindowActive,
629
+ matchedRuleId: rule.id,
630
+ breakdown: parts.join(" ")
631
+ };
632
+ }
633
+ return {
634
+ costUsd: 0,
635
+ baseCostUsd: 0,
636
+ multiplier: 1,
637
+ volumeDiscountPercent: 0,
638
+ timeWindowActive: false,
639
+ matchedRuleId: null,
640
+ breakdown: "no matching pricing rule"
641
+ };
642
+ }
643
+ globMatch(pattern, value) {
644
+ if (pattern === "*") return true;
645
+ const regex = new RegExp(
646
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
647
+ );
648
+ return regex.test(value);
649
+ }
650
+ };
651
+
652
+ // src/recalculationEngine.ts
653
+ var idCounter = 0;
654
+ function genId(prefix) {
655
+ return `${prefix}_${Date.now()}_${++idCounter}`;
656
+ }
657
+ function simpleHash(data) {
658
+ let hash = 0;
659
+ for (let i = 0; i < data.length; i++) {
660
+ const char = data.charCodeAt(i);
661
+ hash = (hash << 5) - hash + char;
662
+ hash = hash & hash;
663
+ }
664
+ return "h_" + Math.abs(hash).toString(16).padStart(8, "0");
665
+ }
666
+ var RecalculationEngine = class {
667
+ tasks = /* @__PURE__ */ new Map();
668
+ consistencyResults = /* @__PURE__ */ new Map();
669
+ snapshots = /* @__PURE__ */ new Map();
670
+ handlers = /* @__PURE__ */ new Map();
671
+ config;
672
+ constructor(config = {}) {
673
+ this.config = { maxRangeDays: 30, ...config };
674
+ }
675
+ /** G3: Trigger recalculation */
676
+ triggerRecalculation(input) {
677
+ if (input.fromTs >= input.toTs) throw new Error("fromTs must be before toTs");
678
+ const maxMs = this.config.maxRangeDays * 864e5;
679
+ if (input.toTs - input.fromTs > maxMs) throw new Error(`Range exceeds ${this.config.maxRangeDays} days`);
680
+ const task = {
681
+ id: genId("recalc"),
682
+ tenantId: input.tenantId,
683
+ triggerType: input.triggerType,
684
+ triggeredBy: input.triggeredBy,
685
+ reason: input.reason,
686
+ fromTs: input.fromTs,
687
+ toTs: input.toTs,
688
+ granularity: input.granularity || "all",
689
+ incremental: input.incremental || false,
690
+ incrementalCheckpoint: input.incrementalCheckpoint,
691
+ status: "pending",
692
+ progress: 0,
693
+ bucketsProcessed: 0,
694
+ rowsUpserted: 0,
695
+ createdAt: Date.now(),
696
+ traceId: genId("trace")
697
+ };
698
+ this.tasks.set(task.id, task);
699
+ this.audit("recalc.triggered", { taskId: task.id, triggerType: task.triggerType, reason: task.reason });
700
+ return task;
701
+ }
702
+ /** Execute recalculation task */
703
+ async executeTask(taskId) {
704
+ const task = this.tasks.get(taskId);
705
+ if (!task) throw new Error(`Task not found: ${taskId}`);
706
+ if (task.status !== "pending") throw new Error(`Task not pending: ${task.status}`);
707
+ task.status = "running";
708
+ task.startedAt = Date.now();
709
+ this.audit("recalc.started", { taskId: task.id });
710
+ try {
711
+ const preSnapshot = this.createSnapshot(task.tenantId, "pre_recalc", task.id);
712
+ const handler = this.handlers.get(task.granularity);
713
+ if (handler) {
714
+ const result = await handler(task);
715
+ task.bucketsProcessed = result.bucketsProcessed;
716
+ task.rowsUpserted = result.rowsUpserted;
717
+ const postSnapshot = this.createSnapshot(task.tenantId, "post_recalc", task.id, result.metrics);
718
+ task.consistencyResult = this.verifyConsistency(task.id, preSnapshot, postSnapshot, result.metrics);
719
+ } else {
720
+ const segmentMs = task.granularity === "5m" ? 3e5 : task.granularity === "1h" ? 36e5 : task.granularity === "1d" ? 864e5 : 36e5;
721
+ const totalSegments = Math.ceil((task.toTs - task.fromTs) / segmentMs);
722
+ task.bucketsProcessed = totalSegments;
723
+ task.progress = 100;
724
+ }
725
+ task.status = "completed";
726
+ task.completedAt = Date.now();
727
+ task.progress = 100;
728
+ this.audit("recalc.completed", {
729
+ taskId: task.id,
730
+ bucketsProcessed: task.bucketsProcessed,
731
+ durationMs: task.completedAt - (task.startedAt || 0),
732
+ consistencyRate: task.consistencyResult?.consistencyRate
733
+ });
734
+ return task;
735
+ } catch (error) {
736
+ task.status = "failed";
737
+ task.error = error instanceof Error ? error.message : "Unknown error";
738
+ task.completedAt = Date.now();
739
+ this.audit("recalc.failed", { taskId: task.id, error: task.error });
740
+ return task;
741
+ }
742
+ }
743
+ /** G4: Verify consistency */
744
+ verifyConsistency(taskId, preSnapshot, postSnapshot, recalculatedMetrics) {
745
+ const mismatches = [];
746
+ const metrics = recalculatedMetrics || postSnapshot.metrics;
747
+ let totalComparisons = 0;
748
+ let matchCount = 0;
749
+ for (const [metric, recalcValue] of Object.entries(metrics)) {
750
+ const currentValue = preSnapshot.metrics[metric];
751
+ if (currentValue === void 0) continue;
752
+ totalComparisons++;
753
+ const deviation = Math.abs(recalcValue - currentValue);
754
+ const deviationPercent = currentValue !== 0 ? deviation / Math.abs(currentValue) * 100 : recalcValue !== 0 ? 100 : 0;
755
+ if (deviationPercent < 0.01) {
756
+ matchCount++;
757
+ } else {
758
+ mismatches.push({
759
+ metric,
760
+ bucket: `${preSnapshot.id} \u2192 ${postSnapshot.id}`,
761
+ currentValue,
762
+ recalculatedValue: recalcValue,
763
+ deviation,
764
+ deviationPercent: Math.round(deviationPercent * 100) / 100,
765
+ severity: deviationPercent > 10 ? "critical" : deviationPercent > 5 ? "high" : deviationPercent > 1 ? "medium" : "low"
766
+ });
767
+ }
768
+ }
769
+ const consistencyRate = totalComparisons > 0 ? Math.round(matchCount / totalComparisons * 1e4) / 100 : 100;
770
+ const verificationHash = simpleHash(JSON.stringify({ taskId, pre: preSnapshot.hash, post: postSnapshot.hash, mismatches }));
771
+ const result = {
772
+ id: genId("cr"),
773
+ taskId,
774
+ status: mismatches.length === 0 ? "consistent" : consistencyRate > 95 ? "partial" : "inconsistent",
775
+ totalComparisons,
776
+ matchCount,
777
+ mismatchCount: mismatches.length,
778
+ consistencyRate,
779
+ mismatches,
780
+ verifiedAt: Date.now(),
781
+ verificationHash
782
+ };
783
+ this.consistencyResults.set(result.id, result);
784
+ this.audit("recalc.consistency_check", { taskId, consistencyRate, mismatchCount: mismatches.length, status: result.status });
785
+ return result;
786
+ }
787
+ /** G5: Incremental recalculation */
788
+ triggerIncremental(input) {
789
+ const completed = Array.from(this.tasks.values()).filter((t) => t.tenantId === input.tenantId && t.status === "completed").sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0));
790
+ const last = completed[0];
791
+ const fromTs = last ? last.toTs : Date.now() - 864e5;
792
+ return this.triggerRecalculation({
793
+ tenantId: input.tenantId,
794
+ triggerType: "manual",
795
+ triggeredBy: input.triggeredBy,
796
+ reason: input.reason,
797
+ fromTs,
798
+ toTs: Date.now(),
799
+ granularity: input.granularity,
800
+ incremental: true,
801
+ incrementalCheckpoint: last?.id
802
+ });
803
+ }
804
+ /** Create aggregate snapshot */
805
+ createSnapshot(tenantId, snapshotType, taskId, metrics) {
806
+ const m = metrics || {};
807
+ const snapshot = {
808
+ id: genId("snap"),
809
+ tenantId,
810
+ snapshotType,
811
+ taskId,
812
+ metrics: m,
813
+ hash: simpleHash(JSON.stringify(m)),
814
+ createdAt: Date.now()
815
+ };
816
+ this.snapshots.set(snapshot.id, snapshot);
817
+ return snapshot;
818
+ }
819
+ /** Register recalculation handler */
820
+ registerHandler(granularity, handler) {
821
+ this.handlers.set(granularity, handler);
822
+ }
823
+ /** Query functions */
824
+ getTask(taskId) {
825
+ return this.tasks.get(taskId);
826
+ }
827
+ getTasks(tenantId, limit = 20) {
828
+ return Array.from(this.tasks.values()).filter((t) => t.tenantId === tenantId).sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
829
+ }
830
+ getConsistencyResult(taskId) {
831
+ return Array.from(this.consistencyResults.values()).find((r) => r.taskId === taskId);
832
+ }
833
+ getSnapshot(snapshotId) {
834
+ return this.snapshots.get(snapshotId);
835
+ }
836
+ cancelTask(taskId) {
837
+ const task = this.tasks.get(taskId);
838
+ if (!task || task.status !== "pending") return false;
839
+ task.status = "cancelled";
840
+ return true;
841
+ }
842
+ audit(type, details) {
843
+ if (this.config.onAudit) {
844
+ this.config.onAudit({ type, details, timestamp: Date.now() });
845
+ }
846
+ }
847
+ };
848
+ function createRecalculationEngine(config) {
849
+ return new RecalculationEngine(config);
850
+ }
851
+
852
+ // src/autoHarden.ts
853
+ var LEVEL_PRIORITY = {
854
+ normal: 0,
855
+ elevated: 1,
856
+ hardened: 2,
857
+ lockdown: 3
858
+ };
859
+ var DEFAULT_CONFIG = {
860
+ enabled: true,
861
+ levelMeasures: {
862
+ normal: [],
863
+ elevated: ["enhanced_audit", "alert_admin"],
864
+ hardened: ["gate_level_upgrade", "rate_limit_reduce", "enhanced_audit", "alert_admin"],
865
+ lockdown: ["gate_level_upgrade", "rate_limit_reduce", "enhanced_audit", "ip_block", "session_invalidate", "feature_disable", "alert_admin"]
866
+ },
867
+ autoRollbackMs: 30 * 60 * 1e3,
868
+ rateLimitReductionFactor: 0.25,
869
+ notifyAdmin: true
870
+ };
871
+ var idCounter2 = 0;
872
+ function genId2(prefix) {
873
+ return `${prefix}_${Date.now()}_${++idCounter2}`;
874
+ }
875
+ var AutoHardenEngine = class {
876
+ states = /* @__PURE__ */ new Map();
877
+ history = [];
878
+ config;
879
+ rollbackTimers = /* @__PURE__ */ new Map();
880
+ maxHistory = 1e3;
881
+ constructor(config = {}) {
882
+ this.config = { ...DEFAULT_CONFIG, ...config };
883
+ }
884
+ /** Get tenant harden state */
885
+ getState(tenantId) {
886
+ return this.states.get(tenantId) || { currentLevel: "normal", activeMeasures: [] };
887
+ }
888
+ /** Get current harden level */
889
+ getLevel(tenantId) {
890
+ return this.getState(tenantId).currentLevel;
891
+ }
892
+ /** Auto-harden (main entry) */
893
+ async harden(input) {
894
+ if (!this.config.enabled) throw new Error("AutoHarden is disabled");
895
+ const state = this.getState(input.tenantId);
896
+ const traceId = genId2("trace");
897
+ const targetLevel = input.targetLevel || this.determineLevel(
898
+ input.manipulationScore || 0,
899
+ input.reasonCodes || []
900
+ );
901
+ if (LEVEL_PRIORITY[targetLevel] <= LEVEL_PRIORITY[state.currentLevel]) {
902
+ const event2 = {
903
+ id: genId2("harden"),
904
+ tenantId: input.tenantId,
905
+ triggeredBy: input.triggeredBy,
906
+ reason: `Already at ${state.currentLevel}, target ${targetLevel} skipped`,
907
+ previousLevel: state.currentLevel,
908
+ newLevel: state.currentLevel,
909
+ measures: [],
910
+ createdAt: Date.now(),
911
+ rolledBack: false,
912
+ traceId
913
+ };
914
+ this.history.push(event2);
915
+ return event2;
916
+ }
917
+ const measures = this.applyMeasures(input.tenantId, targetLevel);
918
+ const previousLevel = state.currentLevel;
919
+ state.currentLevel = targetLevel;
920
+ state.activeMeasures = measures;
921
+ state.hardenedSince = Date.now();
922
+ if (this.config.autoRollbackMs > 0) {
923
+ state.expectedRollbackAt = Date.now() + this.config.autoRollbackMs;
924
+ this.scheduleAutoRollback(input.tenantId, this.config.autoRollbackMs);
925
+ }
926
+ this.states.set(input.tenantId, state);
927
+ const event = {
928
+ id: genId2("harden"),
929
+ tenantId: input.tenantId,
930
+ triggeredBy: input.triggeredBy,
931
+ reason: input.reason,
932
+ previousLevel,
933
+ newLevel: targetLevel,
934
+ measures,
935
+ createdAt: Date.now(),
936
+ rolledBack: false,
937
+ traceId
938
+ };
939
+ this.history.push(event);
940
+ if (this.history.length > this.maxHistory) this.history.shift();
941
+ state.lastEvent = event;
942
+ this.audit(event.rolledBack ? "HARDEN_ROLLBACK" : "HARDEN_ACTIVATED", {
943
+ tenantId: event.tenantId,
944
+ previousLevel,
945
+ newLevel: targetLevel,
946
+ measuresCount: measures.length,
947
+ measures: measures.map((m) => m.type)
948
+ });
949
+ if (this.config.notifyAdmin && this.config.onNotifyAdmin) {
950
+ try {
951
+ await this.config.onNotifyAdmin(event);
952
+ } catch {
953
+ }
954
+ }
955
+ return event;
956
+ }
957
+ /** Manual rollback */
958
+ async rollback(tenantId, rolledBackBy, reason) {
959
+ const state = this.getState(tenantId);
960
+ if (state.currentLevel === "normal") throw new Error("Already at normal level");
961
+ const timer = this.rollbackTimers.get(tenantId);
962
+ if (timer) {
963
+ clearTimeout(timer);
964
+ this.rollbackTimers.delete(tenantId);
965
+ }
966
+ const previousLevel = state.currentLevel;
967
+ const event = {
968
+ id: genId2("harden"),
969
+ tenantId,
970
+ triggeredBy: rolledBackBy,
971
+ reason: reason || `Manual rollback from ${previousLevel}`,
972
+ previousLevel,
973
+ newLevel: "normal",
974
+ measures: state.activeMeasures.map((m) => ({ ...m, rolledBackAt: Date.now() })),
975
+ createdAt: Date.now(),
976
+ rolledBack: true,
977
+ rolledBackAt: Date.now(),
978
+ traceId: genId2("trace")
979
+ };
980
+ state.currentLevel = "normal";
981
+ state.activeMeasures = [];
982
+ state.hardenedSince = void 0;
983
+ state.expectedRollbackAt = void 0;
984
+ state.lastEvent = event;
985
+ this.states.set(tenantId, state);
986
+ this.history.push(event);
987
+ this.audit("HARDEN_ROLLBACK", { tenantId, previousLevel, rolledBackBy });
988
+ return event;
989
+ }
990
+ /** Update config */
991
+ updateConfig(config) {
992
+ this.config = { ...this.config, ...config };
993
+ return this.config;
994
+ }
995
+ /** Get config */
996
+ getConfig() {
997
+ return { ...this.config };
998
+ }
999
+ /** Get history */
1000
+ getHistory(tenantId, limit = 50) {
1001
+ const filtered = tenantId ? this.history.filter((e) => e.tenantId === tenantId) : this.history;
1002
+ return filtered.slice(-limit);
1003
+ }
1004
+ /** Get all active hardened states */
1005
+ getActiveStates() {
1006
+ const result = [];
1007
+ this.states.forEach((state, tenantId) => {
1008
+ if (state.currentLevel !== "normal") result.push({ tenantId, state });
1009
+ });
1010
+ return result;
1011
+ }
1012
+ /** Cleanup timers */
1013
+ destroy() {
1014
+ this.rollbackTimers.forEach((t) => clearTimeout(t));
1015
+ this.rollbackTimers.clear();
1016
+ }
1017
+ // Internal
1018
+ determineLevel(score, reasonCodes) {
1019
+ if (reasonCodes.some((c) => c.startsWith("CRIT_"))) return "lockdown";
1020
+ if (score >= 0.8) return "lockdown";
1021
+ if (score >= 0.5) return "hardened";
1022
+ if (score >= 0.3) return "elevated";
1023
+ return "normal";
1024
+ }
1025
+ applyMeasures(tenantId, level) {
1026
+ const types = this.config.levelMeasures[level];
1027
+ return types.map((type) => ({
1028
+ id: genId2("measure"),
1029
+ type,
1030
+ description: this.getMeasureDescription(type, level),
1031
+ applied: true,
1032
+ appliedAt: Date.now()
1033
+ }));
1034
+ }
1035
+ getMeasureDescription(type, level) {
1036
+ const d = {
1037
+ gate_level_upgrade: `Gate approval level upgraded to ${level}`,
1038
+ rate_limit_reduce: `Rate limit reduced to ${this.config.rateLimitReductionFactor * 100}%`,
1039
+ enhanced_audit: "Enhanced audit mode enabled (full recording)",
1040
+ ip_block: "Suspicious IPs blocked",
1041
+ session_invalidate: "All active sessions invalidated",
1042
+ feature_disable: "Non-essential features disabled",
1043
+ alert_admin: "Admin security alert sent"
1044
+ };
1045
+ return d[type] || type;
1046
+ }
1047
+ scheduleAutoRollback(tenantId, delayMs) {
1048
+ const existing = this.rollbackTimers.get(tenantId);
1049
+ if (existing) clearTimeout(existing);
1050
+ const timer = setTimeout(async () => {
1051
+ try {
1052
+ await this.rollback(tenantId, "system:auto_rollback", "Auto rollback after timeout");
1053
+ } catch {
1054
+ }
1055
+ this.rollbackTimers.delete(tenantId);
1056
+ }, delayMs);
1057
+ this.rollbackTimers.set(tenantId, timer);
1058
+ }
1059
+ audit(type, details) {
1060
+ if (this.config.onAudit) {
1061
+ this.config.onAudit({ type, details, timestamp: Date.now() });
1062
+ }
1063
+ }
1064
+ };
1065
+ function createAutoHardenEngine(config) {
1066
+ return new AutoHardenEngine(config);
1067
+ }
1068
+
1069
+ // src/costEstimator.ts
1070
+ var DEFAULT_MODEL_PRICING = [
1071
+ {
1072
+ modelId: "gpt-4o",
1073
+ modelName: "GPT-4o",
1074
+ inputPricePerKToken: 25e-4,
1075
+ outputPricePerKToken: 0.01,
1076
+ cachedInputPricePerKToken: 125e-5,
1077
+ maxContextLength: 128e3,
1078
+ updatedAt: /* @__PURE__ */ new Date("2025-01-01")
1079
+ },
1080
+ {
1081
+ modelId: "gpt-4o-mini",
1082
+ modelName: "GPT-4o Mini",
1083
+ inputPricePerKToken: 15e-5,
1084
+ outputPricePerKToken: 6e-4,
1085
+ cachedInputPricePerKToken: 75e-6,
1086
+ maxContextLength: 128e3,
1087
+ updatedAt: /* @__PURE__ */ new Date("2025-01-01")
1088
+ },
1089
+ {
1090
+ modelId: "claude-3.5-sonnet",
1091
+ modelName: "Claude 3.5 Sonnet",
1092
+ inputPricePerKToken: 3e-3,
1093
+ outputPricePerKToken: 0.015,
1094
+ cachedInputPricePerKToken: 3e-4,
1095
+ maxContextLength: 2e5,
1096
+ updatedAt: /* @__PURE__ */ new Date("2025-01-01")
1097
+ },
1098
+ {
1099
+ modelId: "claude-3.5-haiku",
1100
+ modelName: "Claude 3.5 Haiku",
1101
+ inputPricePerKToken: 8e-4,
1102
+ outputPricePerKToken: 4e-3,
1103
+ cachedInputPricePerKToken: 8e-5,
1104
+ maxContextLength: 2e5,
1105
+ updatedAt: /* @__PURE__ */ new Date("2025-01-01")
1106
+ },
1107
+ {
1108
+ modelId: "gemini-2.0-flash",
1109
+ modelName: "Gemini 2.0 Flash",
1110
+ inputPricePerKToken: 1e-4,
1111
+ outputPricePerKToken: 4e-4,
1112
+ maxContextLength: 1e6,
1113
+ updatedAt: /* @__PURE__ */ new Date("2025-01-01")
1114
+ }
1115
+ ];
1116
+ var DEFAULT_TOOL_PRICING = [
1117
+ { toolId: "web_search", toolName: "Web Search", costPerCall: 5e-3, multiplier: 1, updatedAt: /* @__PURE__ */ new Date() },
1118
+ { toolId: "code_exec", toolName: "Code Execution", costPerCall: 2e-3, multiplier: 1, updatedAt: /* @__PURE__ */ new Date() },
1119
+ { toolId: "file_read", toolName: "File Read", costPerCall: 1e-3, multiplier: 1, updatedAt: /* @__PURE__ */ new Date() },
1120
+ { toolId: "db_query", toolName: "Database Query", costPerCall: 3e-3, multiplier: 1, updatedAt: /* @__PURE__ */ new Date() },
1121
+ { toolId: "api_call", toolName: "External API Call", costPerCall: 0.01, multiplier: 1.5, updatedAt: /* @__PURE__ */ new Date() }
1122
+ ];
1123
+ var DEFAULT_HUMAN_RATES = [
1124
+ { level: "standard", hourlyRate: 50, avgReviewMinutes: 5 },
1125
+ { level: "senior", hourlyRate: 100, avgReviewMinutes: 10 },
1126
+ { level: "executive", hourlyRate: 200, avgReviewMinutes: 15 }
1127
+ ];
1128
+ var modelPricing = [...DEFAULT_MODEL_PRICING];
1129
+ var toolPricing = [...DEFAULT_TOOL_PRICING];
1130
+ var humanRates = [...DEFAULT_HUMAN_RATES];
1131
+ var accuracyRecords = [];
1132
+ var MAX_ACCURACY_RECORDS = 1e4;
1133
+ function estimateCost(request) {
1134
+ const id = `est_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1135
+ const model = modelPricing.find((m) => m.modelId === request.modelId) || modelPricing[0];
1136
+ const inputCost = request.estimatedInputTokens / 1e3 * model.inputPricePerKToken;
1137
+ const outputCost = request.estimatedOutputTokens / 1e3 * model.outputPricePerKToken;
1138
+ const modelCostTotal = inputCost + outputCost;
1139
+ const toolItems = [];
1140
+ let toolCostTotal = 0;
1141
+ if (request.toolCalls) {
1142
+ for (const tc of request.toolCalls) {
1143
+ const tool = toolPricing.find((t) => t.toolId === tc.toolId);
1144
+ if (tool) {
1145
+ const total = tc.estimatedCalls * tool.costPerCall * tool.multiplier;
1146
+ toolItems.push({
1147
+ toolId: tc.toolId,
1148
+ calls: tc.estimatedCalls,
1149
+ costPerCall: tool.costPerCall,
1150
+ total
1151
+ });
1152
+ toolCostTotal += total;
1153
+ }
1154
+ }
1155
+ }
1156
+ let humanCost = null;
1157
+ if (request.requiresHumanReview) {
1158
+ const rate = humanRates.find((r) => r.level === (request.humanReviewLevel || "standard")) || humanRates[0];
1159
+ const humanTotal = rate.avgReviewMinutes / 60 * rate.hourlyRate;
1160
+ humanCost = {
1161
+ level: rate.level,
1162
+ hourlyRate: rate.hourlyRate,
1163
+ estimatedMinutes: rate.avgReviewMinutes,
1164
+ total: humanTotal
1165
+ };
1166
+ }
1167
+ const totalCost = modelCostTotal + toolCostTotal + (humanCost?.total || 0);
1168
+ const avgDeviation = getAverageDeviation();
1169
+ const confidence = {
1170
+ low: totalCost * (1 - avgDeviation),
1171
+ high: totalCost * (1 + avgDeviation),
1172
+ level: Math.max(0, 1 - avgDeviation)
1173
+ };
1174
+ return {
1175
+ id,
1176
+ modelCost: {
1177
+ inputCost: Math.round(inputCost * 1e6) / 1e6,
1178
+ outputCost: Math.round(outputCost * 1e6) / 1e6,
1179
+ total: Math.round(modelCostTotal * 1e6) / 1e6
1180
+ },
1181
+ toolCost: {
1182
+ items: toolItems,
1183
+ total: Math.round(toolCostTotal * 1e6) / 1e6
1184
+ },
1185
+ humanCost,
1186
+ totalCost: Math.round(totalCost * 1e6) / 1e6,
1187
+ confidence,
1188
+ estimatedAt: /* @__PURE__ */ new Date()
1189
+ };
1190
+ }
1191
+ function recordActualCost(estimateId, actualCost) {
1192
+ const record = {
1193
+ estimateId,
1194
+ estimatedCost: 0,
1195
+ // 需要外部传入
1196
+ actualCost,
1197
+ deviation: 0,
1198
+ deviationPercent: 0,
1199
+ recordedAt: /* @__PURE__ */ new Date()
1200
+ };
1201
+ accuracyRecords.push(record);
1202
+ if (accuracyRecords.length > MAX_ACCURACY_RECORDS) accuracyRecords.shift();
1203
+ return record;
1204
+ }
1205
+ function getAverageDeviation() {
1206
+ if (accuracyRecords.length === 0) return 0.3;
1207
+ const deviations = accuracyRecords.filter((r) => r.estimatedCost > 0).map((r) => Math.abs(r.deviationPercent));
1208
+ if (deviations.length === 0) return 0.3;
1209
+ return deviations.reduce((sum, d) => sum + d, 0) / deviations.length / 100;
1210
+ }
1211
+ function updateModelPricing(pricing) {
1212
+ const index = modelPricing.findIndex((m) => m.modelId === pricing.modelId);
1213
+ if (index >= 0) {
1214
+ modelPricing[index] = pricing;
1215
+ } else {
1216
+ modelPricing.push(pricing);
1217
+ }
1218
+ }
1219
+ function updateToolPricing(pricing) {
1220
+ const index = toolPricing.findIndex((t) => t.toolId === pricing.toolId);
1221
+ if (index >= 0) {
1222
+ toolPricing[index] = pricing;
1223
+ } else {
1224
+ toolPricing.push(pricing);
1225
+ }
1226
+ }
1227
+ function getModelPricingTable() {
1228
+ return [...modelPricing];
1229
+ }
1230
+ function getToolPricingTable() {
1231
+ return [...toolPricing];
1232
+ }
1233
+ function getAccuracyStats() {
1234
+ const avgDev = getAverageDeviation();
1235
+ return {
1236
+ totalRecords: accuracyRecords.length,
1237
+ avgDeviation: avgDev,
1238
+ avgDeviationPercent: avgDev * 100,
1239
+ confidenceLevel: Math.max(0, 1 - avgDev)
1240
+ };
1241
+ }
1242
+
1243
+ // src/semanticDriftDetector.ts
1244
+ var DEFAULT_CONFIG2 = {
1245
+ lowThreshold: 0.1,
1246
+ mediumThreshold: 0.3,
1247
+ highThreshold: 0.5,
1248
+ criticalThreshold: 0.7,
1249
+ onDriftDetected: void 0,
1250
+ onAudit: void 0
1251
+ };
1252
+ var SemanticDriftDetectorEngine = class {
1253
+ fingerprints = /* @__PURE__ */ new Map();
1254
+ history = [];
1255
+ config;
1256
+ constructor(config = {}) {
1257
+ this.config = { ...DEFAULT_CONFIG2, ...config };
1258
+ }
1259
+ /**
1260
+ * 计算策略语义指纹
1261
+ */
1262
+ computeFingerprint(snapshot) {
1263
+ const rules = snapshot.rules;
1264
+ const resourceSet = [...new Set(rules.map((r) => r.resource))].sort();
1265
+ const actionSet = [...new Set(rules.map((r) => r.action))].sort();
1266
+ const fingerprint = {
1267
+ version: snapshot.version,
1268
+ hash: this.hashRules(rules),
1269
+ ruleCount: rules.length,
1270
+ allowCount: rules.filter((r) => r.decision === "ALLOW").length,
1271
+ denyCount: rules.filter((r) => r.decision === "DENY").length,
1272
+ reviewCount: rules.filter((r) => r.decision === "REVIEW").length,
1273
+ resourceSet,
1274
+ actionSet,
1275
+ conditionDepth: this.maxConditionDepth(rules),
1276
+ timestamp: Date.now()
1277
+ };
1278
+ this.fingerprints.set(snapshot.version, fingerprint);
1279
+ return fingerprint;
1280
+ }
1281
+ /**
1282
+ * 检测两个版本间的语义漂移
1283
+ */
1284
+ detectDrift(from, to) {
1285
+ const fromRuleMap = new Map(from.rules.map((r) => [r.id, r]));
1286
+ const toRuleMap = new Map(to.rules.map((r) => [r.id, r]));
1287
+ const addedRules = [];
1288
+ const removedRules = [];
1289
+ const modifiedRules = [];
1290
+ const decisionChanges = [];
1291
+ for (const [id, toRule] of toRuleMap) {
1292
+ const fromRule = fromRuleMap.get(id);
1293
+ if (!fromRule) {
1294
+ addedRules.push(id);
1295
+ } else if (this.ruleChanged(fromRule, toRule)) {
1296
+ modifiedRules.push(id);
1297
+ if (fromRule.decision !== toRule.decision) {
1298
+ decisionChanges.push({ ruleId: id, from: fromRule.decision, to: toRule.decision });
1299
+ }
1300
+ }
1301
+ }
1302
+ for (const id of fromRuleMap.keys()) {
1303
+ if (!toRuleMap.has(id)) {
1304
+ removedRules.push(id);
1305
+ }
1306
+ }
1307
+ const fromResources = new Set(from.rules.map((r) => r.resource));
1308
+ const toResources = new Set(to.rules.map((r) => r.resource));
1309
+ const newResources = [...toResources].filter((r) => !fromResources.has(r));
1310
+ const removedResources = [...fromResources].filter((r) => !toResources.has(r));
1311
+ const totalRules = Math.max(from.rules.length, to.rules.length, 1);
1312
+ const changeCount = addedRules.length + removedRules.length + modifiedRules.length;
1313
+ const decisionWeight = decisionChanges.length * 2;
1314
+ const driftScore = Math.min(1, (changeCount + decisionWeight) / totalRules);
1315
+ let severity = "none";
1316
+ if (driftScore >= this.config.criticalThreshold) severity = "critical";
1317
+ else if (driftScore >= this.config.highThreshold) severity = "high";
1318
+ else if (driftScore >= this.config.mediumThreshold) severity = "medium";
1319
+ else if (driftScore >= this.config.lowThreshold) severity = "low";
1320
+ const result = {
1321
+ fromVersion: from.version,
1322
+ toVersion: to.version,
1323
+ driftScore,
1324
+ ruleCountDelta: to.rules.length - from.rules.length,
1325
+ addedRules,
1326
+ removedRules,
1327
+ modifiedRules,
1328
+ decisionChanges,
1329
+ newResources,
1330
+ removedResources,
1331
+ severity,
1332
+ timestamp: Date.now()
1333
+ };
1334
+ this.history.push(result);
1335
+ if (severity !== "none" && this.config.onDriftDetected) {
1336
+ this.config.onDriftDetected(result);
1337
+ }
1338
+ if (this.config.onAudit) {
1339
+ this.config.onAudit({
1340
+ type: "drift_detected",
1341
+ details: { fromVersion: from.version, toVersion: to.version, driftScore, severity },
1342
+ timestamp: Date.now()
1343
+ });
1344
+ }
1345
+ return result;
1346
+ }
1347
+ /**
1348
+ * 获取漂移历史
1349
+ */
1350
+ getDriftHistory() {
1351
+ return [...this.history];
1352
+ }
1353
+ /**
1354
+ * 获取指纹
1355
+ */
1356
+ getFingerprint(version) {
1357
+ return this.fingerprints.get(version) ?? null;
1358
+ }
1359
+ /**
1360
+ * 获取漂移趋势
1361
+ */
1362
+ getDriftTrend() {
1363
+ return this.history.map((h) => ({
1364
+ version: h.toVersion,
1365
+ driftScore: h.driftScore,
1366
+ timestamp: h.timestamp
1367
+ }));
1368
+ }
1369
+ // ============================================
1370
+ // 私有方法
1371
+ // ============================================
1372
+ hashRules(rules) {
1373
+ const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
1374
+ const str = sorted.map((r) => `${r.id}:${r.action}:${r.resource}:${r.decision}:${r.priority}`).join("|");
1375
+ let hash = 0;
1376
+ for (let i = 0; i < str.length; i++) {
1377
+ const char = str.charCodeAt(i);
1378
+ hash = (hash << 5) - hash + char;
1379
+ hash = hash & hash;
1380
+ }
1381
+ return "fp_" + Math.abs(hash).toString(16).padStart(8, "0");
1382
+ }
1383
+ ruleChanged(a, b) {
1384
+ return a.action !== b.action || a.resource !== b.resource || a.decision !== b.decision || a.priority !== b.priority || JSON.stringify(a.conditions) !== JSON.stringify(b.conditions);
1385
+ }
1386
+ maxConditionDepth(rules) {
1387
+ let max = 0;
1388
+ for (const r of rules) {
1389
+ if (r.conditions) {
1390
+ max = Math.max(max, this.objectDepth(r.conditions));
1391
+ }
1392
+ }
1393
+ return max;
1394
+ }
1395
+ objectDepth(obj, depth = 0) {
1396
+ let max = depth;
1397
+ for (const val of Object.values(obj)) {
1398
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1399
+ max = Math.max(max, this.objectDepth(val, depth + 1));
1400
+ }
1401
+ }
1402
+ return max;
1403
+ }
1404
+ };
1405
+ function createSemanticDriftDetector(config) {
1406
+ return new SemanticDriftDetectorEngine(config);
1407
+ }
1408
+
1409
+ // src/costGateEnhanced.ts
1410
+ var DEFAULT_CONFIG3 = {
1411
+ defaultBudgetUsd: 100,
1412
+ warningThreshold: 0.8,
1413
+ criticalThreshold: 0.95,
1414
+ humanReviewHourlyRateUsd: 50,
1415
+ toolCosts: [],
1416
+ onAudit: void 0,
1417
+ onBudgetExceeded: void 0
1418
+ };
1419
+ var CostGateEnhancedEngine = class {
1420
+ records = [];
1421
+ budgets = /* @__PURE__ */ new Map();
1422
+ // tenantId → budgetUsd
1423
+ config;
1424
+ toolCostMap = /* @__PURE__ */ new Map();
1425
+ constructor(config = {}) {
1426
+ this.config = { ...DEFAULT_CONFIG3, ...config };
1427
+ for (const tc of this.config.toolCosts) {
1428
+ this.toolCostMap.set(tc.toolName, tc);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * E2: 记录成本并生成汇总
1433
+ */
1434
+ recordCost(record) {
1435
+ this.records.push(record);
1436
+ this.audit("cost_recorded", record.tenantId, { record });
1437
+ return this.checkBudget(record.tenantId, record.agentId, record.action);
1438
+ }
1439
+ /**
1440
+ * E2: 获取成本汇总(滑动窗口)
1441
+ */
1442
+ getCostSummary(tenantId, period) {
1443
+ const now = Date.now();
1444
+ const periodMs = {
1445
+ "1h": 36e5,
1446
+ "24h": 864e5,
1447
+ "7d": 6048e5,
1448
+ "30d": 2592e6
1449
+ };
1450
+ const startTime = now - (periodMs[period] || 864e5);
1451
+ const filtered = this.records.filter((r) => r.tenantId === tenantId && r.timestamp >= startTime);
1452
+ const byCategory = {
1453
+ llm_inference: 0,
1454
+ tool_call: 0,
1455
+ human_review: 0,
1456
+ storage: 0,
1457
+ network: 0,
1458
+ compute: 0,
1459
+ other: 0
1460
+ };
1461
+ const byAgent = {};
1462
+ const byTool = {};
1463
+ let totalCostUsd = 0;
1464
+ for (const r of filtered) {
1465
+ totalCostUsd += r.costUsd;
1466
+ byCategory[r.category] = (byCategory[r.category] || 0) + r.costUsd;
1467
+ byAgent[r.agentId] = (byAgent[r.agentId] || 0) + r.costUsd;
1468
+ if (r.toolName) {
1469
+ byTool[r.toolName] = (byTool[r.toolName] || 0) + r.costUsd;
1470
+ }
1471
+ }
1472
+ this.audit("summary_generated", tenantId, { period, totalCostUsd, recordCount: filtered.length });
1473
+ return {
1474
+ period,
1475
+ totalCostUsd,
1476
+ byCategory,
1477
+ byAgent,
1478
+ byTool,
1479
+ recordCount: filtered.length,
1480
+ avgCostPerAction: filtered.length > 0 ? totalCostUsd / filtered.length : 0,
1481
+ startTime,
1482
+ endTime: now
1483
+ };
1484
+ }
1485
+ /**
1486
+ * E7: getDailyCostStats — 按天统计
1487
+ */
1488
+ getDailyCostStats(tenantId, days = 30) {
1489
+ const now = Date.now();
1490
+ const startTime = now - days * 864e5;
1491
+ const filtered = this.records.filter((r) => r.tenantId === tenantId && r.timestamp >= startTime);
1492
+ const dailyMap = /* @__PURE__ */ new Map();
1493
+ for (const r of filtered) {
1494
+ const date = new Date(r.timestamp).toISOString().split("T")[0];
1495
+ if (!dailyMap.has(date)) {
1496
+ dailyMap.set(date, { records: [], agents: /* @__PURE__ */ new Set() });
1497
+ }
1498
+ const day = dailyMap.get(date);
1499
+ day.records.push(r);
1500
+ day.agents.add(r.agentId);
1501
+ }
1502
+ const budget = this.budgets.get(tenantId) ?? this.config.defaultBudgetUsd;
1503
+ const dailyBudget = budget / 30;
1504
+ return Array.from(dailyMap.entries()).map(([date, { records, agents }]) => {
1505
+ const byCategory = {
1506
+ llm_inference: 0,
1507
+ tool_call: 0,
1508
+ human_review: 0,
1509
+ storage: 0,
1510
+ network: 0,
1511
+ compute: 0,
1512
+ other: 0
1513
+ };
1514
+ let totalCostUsd = 0;
1515
+ for (const r of records) {
1516
+ totalCostUsd += r.costUsd;
1517
+ byCategory[r.category] = (byCategory[r.category] || 0) + r.costUsd;
1518
+ }
1519
+ return {
1520
+ date,
1521
+ totalCostUsd,
1522
+ byCategory,
1523
+ actionCount: records.length,
1524
+ uniqueAgents: agents.size,
1525
+ budgetUtilization: dailyBudget > 0 ? totalCostUsd / dailyBudget : 0
1526
+ };
1527
+ }).sort((a, b) => a.date.localeCompare(b.date));
1528
+ }
1529
+ /**
1530
+ * E9: 计算工具调用成本
1531
+ */
1532
+ calculateToolCost(toolName, tokenCount) {
1533
+ const config = this.toolCostMap.get(toolName);
1534
+ if (!config) return 1e-3;
1535
+ let cost = config.baseCostUsd + config.perCallCostUsd;
1536
+ if (tokenCount && config.perTokenCostUsd) {
1537
+ cost += tokenCount * config.perTokenCostUsd;
1538
+ }
1539
+ if (config.maxCostPerCallUsd) {
1540
+ cost = Math.min(cost, config.maxCostPerCallUsd);
1541
+ }
1542
+ return cost;
1543
+ }
1544
+ /**
1545
+ * E10: 计算人工审批成本
1546
+ */
1547
+ calculateHumanReviewCost(waitTimeMs, reviewTimeMs) {
1548
+ const totalTimeMs = waitTimeMs + reviewTimeMs;
1549
+ const hours = totalTimeMs / 36e5;
1550
+ const costUsd = hours * this.config.humanReviewHourlyRateUsd;
1551
+ return {
1552
+ reviewId: `review_${Date.now()}`,
1553
+ reviewerType: "human",
1554
+ waitTimeMs,
1555
+ reviewTimeMs,
1556
+ costUsd: Math.round(costUsd * 1e4) / 1e4,
1557
+ hourlyRateUsd: this.config.humanReviewHourlyRateUsd
1558
+ };
1559
+ }
1560
+ /**
1561
+ * 设置租户预算
1562
+ */
1563
+ setBudget(tenantId, budgetUsd) {
1564
+ this.budgets.set(tenantId, budgetUsd);
1565
+ }
1566
+ /**
1567
+ * 获取租户预算使用率
1568
+ */
1569
+ getBudgetUtilization(tenantId) {
1570
+ const budget = this.budgets.get(tenantId) ?? this.config.defaultBudgetUsd;
1571
+ const summary = this.getCostSummary(tenantId, "30d");
1572
+ return {
1573
+ used: summary.totalCostUsd,
1574
+ limit: budget,
1575
+ utilization: budget > 0 ? summary.totalCostUsd / budget : 0
1576
+ };
1577
+ }
1578
+ // ============================================
1579
+ // 私有方法
1580
+ // ============================================
1581
+ checkBudget(tenantId, agentId, action) {
1582
+ const budget = this.budgets.get(tenantId) ?? this.config.defaultBudgetUsd;
1583
+ const summary = this.getCostSummary(tenantId, "30d");
1584
+ const utilization = budget > 0 ? summary.totalCostUsd / budget : 0;
1585
+ if (utilization >= 1) {
1586
+ const event = {
1587
+ code: "BUDGET_EXCEEDED",
1588
+ tenantId,
1589
+ agentId,
1590
+ action,
1591
+ currentCostUsd: summary.totalCostUsd,
1592
+ budgetLimitUsd: budget,
1593
+ overage: summary.totalCostUsd - budget,
1594
+ period: "30d",
1595
+ timestamp: Date.now(),
1596
+ severity: "blocked"
1597
+ };
1598
+ this.audit("budget_exceeded", tenantId, { event });
1599
+ if (this.config.onBudgetExceeded) this.config.onBudgetExceeded(event);
1600
+ return event;
1601
+ }
1602
+ if (utilization >= this.config.criticalThreshold) {
1603
+ this.audit("budget_critical", tenantId, { utilization, budget });
1604
+ } else if (utilization >= this.config.warningThreshold) {
1605
+ this.audit("budget_warning", tenantId, { utilization, budget });
1606
+ }
1607
+ return null;
1608
+ }
1609
+ audit(type, tenantId, details) {
1610
+ if (this.config.onAudit) {
1611
+ this.config.onAudit({ type, tenantId, details, timestamp: Date.now() });
1612
+ }
1613
+ }
1614
+ };
1615
+ function createCostGateEnhanced(config) {
1616
+ return new CostGateEnhancedEngine(config);
1617
+ }
1618
+
1619
+ // src/budgetMultiLevel.ts
1620
+ var DEFAULT_ALERT_RULES = [
1621
+ { id: "warn_80", threshold: 0.8, severity: "warning", action: "notify", cooldownMs: 36e5 },
1622
+ { id: "crit_95", threshold: 0.95, severity: "critical", action: "throttle", cooldownMs: 18e5 },
1623
+ { id: "block_100", threshold: 1, severity: "critical", action: "block", cooldownMs: 3e5 }
1624
+ ];
1625
+ var DEFAULT_CONFIG4 = {
1626
+ defaultAlertRules: DEFAULT_ALERT_RULES,
1627
+ trendWindowDays: 30,
1628
+ onAlert: void 0,
1629
+ onAudit: void 0
1630
+ };
1631
+ var MultiLevelBudgetEngine = class {
1632
+ nodes = /* @__PURE__ */ new Map();
1633
+ spendHistory = [];
1634
+ config;
1635
+ constructor(config = {}) {
1636
+ this.config = { ...DEFAULT_CONFIG4, ...config };
1637
+ }
1638
+ /**
1639
+ * F6: 创建预算节点
1640
+ */
1641
+ createNode(node) {
1642
+ const budgetNode = {
1643
+ ...node,
1644
+ spentUsd: 0,
1645
+ children: [],
1646
+ alertRules: node.alertRules ?? [...this.config.defaultAlertRules]
1647
+ };
1648
+ this.nodes.set(budgetNode.id, budgetNode);
1649
+ if (node.parentId) {
1650
+ const parent = this.nodes.get(node.parentId);
1651
+ if (parent) {
1652
+ parent.children.push(budgetNode.id);
1653
+ }
1654
+ }
1655
+ return budgetNode;
1656
+ }
1657
+ /**
1658
+ * F6: 记录支出(向上冒泡到所有父级)
1659
+ */
1660
+ recordSpend(nodeId, amount) {
1661
+ const alerts = [];
1662
+ let current = this.nodes.get(nodeId);
1663
+ while (current) {
1664
+ current.spentUsd += amount;
1665
+ this.spendHistory.push({ nodeId: current.id, amount, timestamp: Date.now() });
1666
+ const nodeAlerts = this.checkAlerts(current);
1667
+ alerts.push(...nodeAlerts);
1668
+ current = current.parentId ? this.nodes.get(current.parentId) : void 0;
1669
+ }
1670
+ return alerts;
1671
+ }
1672
+ /**
1673
+ * F6: 获取节点预算状态(含子节点汇总)
1674
+ */
1675
+ getNodeStatus(nodeId) {
1676
+ const node = this.nodes.get(nodeId);
1677
+ if (!node) return null;
1678
+ const childrenSpent = this.getChildrenSpent(nodeId);
1679
+ const utilization = node.budgetUsd > 0 ? node.spentUsd / node.budgetUsd : 0;
1680
+ return {
1681
+ node,
1682
+ utilization,
1683
+ childrenSpent,
1684
+ remaining: Math.max(0, node.budgetUsd - node.spentUsd)
1685
+ };
1686
+ }
1687
+ /**
1688
+ * F7: 预测预算耗尽日期
1689
+ */
1690
+ predictExhaustion(nodeId) {
1691
+ const node = this.nodes.get(nodeId);
1692
+ if (!node || node.spentUsd === 0) return null;
1693
+ const remaining = node.budgetUsd - node.spentUsd;
1694
+ if (remaining <= 0) return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1695
+ const sevenDaysAgo = Date.now() - 7 * 864e5;
1696
+ const recentSpends = this.spendHistory.filter(
1697
+ (s) => s.nodeId === nodeId && s.timestamp >= sevenDaysAgo
1698
+ );
1699
+ const recentTotal = recentSpends.reduce((sum, s) => sum + s.amount, 0);
1700
+ const dailyRate = recentTotal / 7;
1701
+ if (dailyRate <= 0) return null;
1702
+ const daysRemaining = remaining / dailyRate;
1703
+ const exhaustionDate = new Date(Date.now() + daysRemaining * 864e5);
1704
+ return exhaustionDate.toISOString().split("T")[0];
1705
+ }
1706
+ /**
1707
+ * F8: 生成成本报告
1708
+ */
1709
+ generateReport(rootNodeId) {
1710
+ const nodes = rootNodeId ? this.getSubtree(rootNodeId) : Array.from(this.nodes.values());
1711
+ const byLevel = {
1712
+ organization: { budget: 0, spent: 0, count: 0 },
1713
+ team: { budget: 0, spent: 0, count: 0 },
1714
+ project: { budget: 0, spent: 0, count: 0 },
1715
+ agent: { budget: 0, spent: 0, count: 0 }
1716
+ };
1717
+ let totalBudget = 0;
1718
+ let totalSpent = 0;
1719
+ for (const n of nodes) {
1720
+ byLevel[n.level].budget += n.budgetUsd;
1721
+ byLevel[n.level].spent += n.spentUsd;
1722
+ byLevel[n.level].count++;
1723
+ if (n.children.length === 0) {
1724
+ totalBudget += n.budgetUsd;
1725
+ totalSpent += n.spentUsd;
1726
+ }
1727
+ }
1728
+ const topSpenders = nodes.sort((a, b) => b.spentUsd - a.spentUsd).slice(0, 10).map((n) => ({ id: n.id, name: n.name, level: n.level, spent: n.spentUsd }));
1729
+ const trend = this.generateTrend(rootNodeId);
1730
+ return {
1731
+ reportId: `report_${Date.now()}`,
1732
+ generatedAt: Date.now(),
1733
+ period: { start: Date.now() - this.config.trendWindowDays * 864e5, end: Date.now() },
1734
+ totalBudgetUsd: totalBudget,
1735
+ totalSpentUsd: totalSpent,
1736
+ utilization: totalBudget > 0 ? totalSpent / totalBudget : 0,
1737
+ byLevel,
1738
+ topSpenders,
1739
+ alerts: [],
1740
+ trend
1741
+ };
1742
+ }
1743
+ /**
1744
+ * 获取所有节点
1745
+ */
1746
+ listNodes(level) {
1747
+ const all = Array.from(this.nodes.values());
1748
+ return level ? all.filter((n) => n.level === level) : all;
1749
+ }
1750
+ // ============================================
1751
+ // 私有方法
1752
+ // ============================================
1753
+ checkAlerts(node) {
1754
+ const alerts = [];
1755
+ const utilization = node.budgetUsd > 0 ? node.spentUsd / node.budgetUsd : 0;
1756
+ for (const rule of node.alertRules) {
1757
+ if (utilization >= rule.threshold) {
1758
+ if (rule.lastTriggeredAt && Date.now() - rule.lastTriggeredAt < rule.cooldownMs) {
1759
+ continue;
1760
+ }
1761
+ rule.lastTriggeredAt = Date.now();
1762
+ const alert = {
1763
+ ruleId: rule.id,
1764
+ nodeId: node.id,
1765
+ nodeName: node.name,
1766
+ level: node.level,
1767
+ severity: rule.severity,
1768
+ action: rule.action,
1769
+ utilization,
1770
+ budgetUsd: node.budgetUsd,
1771
+ spentUsd: node.spentUsd,
1772
+ remainingUsd: Math.max(0, node.budgetUsd - node.spentUsd),
1773
+ predictedExhaustionDate: this.predictExhaustion(node.id) ?? void 0,
1774
+ timestamp: Date.now()
1775
+ };
1776
+ alerts.push(alert);
1777
+ if (this.config.onAlert) this.config.onAlert(alert);
1778
+ }
1779
+ }
1780
+ return alerts;
1781
+ }
1782
+ getChildrenSpent(nodeId) {
1783
+ const node = this.nodes.get(nodeId);
1784
+ if (!node) return 0;
1785
+ let total = 0;
1786
+ for (const childId of node.children) {
1787
+ const child = this.nodes.get(childId);
1788
+ if (child) {
1789
+ total += child.spentUsd;
1790
+ }
1791
+ }
1792
+ return total;
1793
+ }
1794
+ getSubtree(nodeId) {
1795
+ const result = [];
1796
+ const queue = [nodeId];
1797
+ while (queue.length > 0) {
1798
+ const id = queue.shift();
1799
+ const node = this.nodes.get(id);
1800
+ if (node) {
1801
+ result.push(node);
1802
+ queue.push(...node.children);
1803
+ }
1804
+ }
1805
+ return result;
1806
+ }
1807
+ generateTrend(rootNodeId) {
1808
+ const nodeIds = rootNodeId ? new Set(this.getSubtree(rootNodeId).map((n) => n.id)) : null;
1809
+ const startTime = Date.now() - this.config.trendWindowDays * 864e5;
1810
+ const filtered = this.spendHistory.filter((s) => {
1811
+ if (s.timestamp < startTime) return false;
1812
+ if (nodeIds && !nodeIds.has(s.nodeId)) return false;
1813
+ return true;
1814
+ });
1815
+ const dailyMap = /* @__PURE__ */ new Map();
1816
+ for (const s of filtered) {
1817
+ const date = new Date(s.timestamp).toISOString().split("T")[0];
1818
+ dailyMap.set(date, (dailyMap.get(date) ?? 0) + s.amount);
1819
+ }
1820
+ return Array.from(dailyMap.entries()).map(([date, spent]) => ({ date, spent })).sort((a, b) => a.date.localeCompare(b.date));
1821
+ }
1822
+ };
1823
+ function createMultiLevelBudget(config) {
1824
+ return new MultiLevelBudgetEngine(config);
1825
+ }
1826
+
1827
+ // src/evolutionChannel.ts
1828
+ var DEFAULT_CONFIG5 = {
1829
+ maxVersions: 50,
1830
+ canaryMinDurationMs: 36e5,
1831
+ // 1 小时
1832
+ autoPromoteThreshold: 0.95,
1833
+ onAudit: void 0
1834
+ };
1835
+ var EvolutionChannelEngine = class {
1836
+ versions = /* @__PURE__ */ new Map();
1837
+ activeVersionId = null;
1838
+ canaryVersionId = null;
1839
+ abTests = /* @__PURE__ */ new Map();
1840
+ config;
1841
+ constructor(config = {}) {
1842
+ this.config = { ...DEFAULT_CONFIG5, ...config };
1843
+ }
1844
+ /**
1845
+ * 发布新策略版本
1846
+ */
1847
+ publish(version) {
1848
+ if (this.versions.size >= this.config.maxVersions) {
1849
+ this.cleanupOldVersions();
1850
+ }
1851
+ version.status = "draft";
1852
+ version.createdAt = Date.now();
1853
+ this.versions.set(version.id, version);
1854
+ this.audit("publish", version.id, version.version, version.createdBy);
1855
+ return version;
1856
+ }
1857
+ /**
1858
+ * 启动灰度发布
1859
+ */
1860
+ startCanary(versionId, percent, actor) {
1861
+ const version = this.versions.get(versionId);
1862
+ if (!version || version.status !== "draft") return false;
1863
+ if (percent < 1 || percent > 99) return false;
1864
+ version.status = "canary";
1865
+ version.canaryPercent = percent;
1866
+ this.canaryVersionId = versionId;
1867
+ this.audit("canary_start", versionId, version.version, actor, { percent });
1868
+ return true;
1869
+ }
1870
+ /**
1871
+ * 灰度晋升为正式版本
1872
+ */
1873
+ promoteCanary(actor) {
1874
+ if (!this.canaryVersionId) return false;
1875
+ const canary = this.versions.get(this.canaryVersionId);
1876
+ if (!canary || canary.status !== "canary") return false;
1877
+ const elapsed = Date.now() - canary.createdAt;
1878
+ if (elapsed < this.config.canaryMinDurationMs) return false;
1879
+ if (this.activeVersionId) {
1880
+ const old = this.versions.get(this.activeVersionId);
1881
+ if (old) old.status = "deprecated";
1882
+ }
1883
+ canary.status = "active";
1884
+ canary.canaryPercent = 100;
1885
+ this.activeVersionId = this.canaryVersionId;
1886
+ this.canaryVersionId = null;
1887
+ this.audit("canary_promote", canary.id, canary.version, actor);
1888
+ return true;
1889
+ }
1890
+ /**
1891
+ * 灰度回滚
1892
+ */
1893
+ rollbackCanary(actor) {
1894
+ if (!this.canaryVersionId) return false;
1895
+ const canary = this.versions.get(this.canaryVersionId);
1896
+ if (!canary) return false;
1897
+ canary.status = "rollback";
1898
+ canary.canaryPercent = 0;
1899
+ this.canaryVersionId = null;
1900
+ this.audit("canary_rollback", canary.id, canary.version, actor);
1901
+ return true;
1902
+ }
1903
+ /**
1904
+ * 路由决策:根据灰度百分比选择策略版本
1905
+ */
1906
+ resolveVersion(requestHash) {
1907
+ if (this.canaryVersionId) {
1908
+ const canary = this.versions.get(this.canaryVersionId);
1909
+ if (canary && canary.status === "canary") {
1910
+ const hash = requestHash ?? Math.random() * 100;
1911
+ if (hash % 100 < canary.canaryPercent) {
1912
+ return canary;
1913
+ }
1914
+ }
1915
+ }
1916
+ if (this.activeVersionId) {
1917
+ return this.versions.get(this.activeVersionId) ?? null;
1918
+ }
1919
+ return null;
1920
+ }
1921
+ /**
1922
+ * 启动 A/B 测试
1923
+ */
1924
+ startABTest(testId, controlId, treatmentId, trafficSplit, actor) {
1925
+ const control = this.versions.get(controlId);
1926
+ const treatment = this.versions.get(treatmentId);
1927
+ if (!control || !treatment) return null;
1928
+ const test = {
1929
+ id: testId,
1930
+ controlVersionId: controlId,
1931
+ treatmentVersionId: treatmentId,
1932
+ trafficSplit: Math.max(1, Math.min(99, trafficSplit)),
1933
+ startedAt: Date.now(),
1934
+ status: "running",
1935
+ metrics: {
1936
+ controlDecisions: 0,
1937
+ treatmentDecisions: 0,
1938
+ controlAllowRate: 0,
1939
+ treatmentAllowRate: 0,
1940
+ controlAvgLatencyMs: 0,
1941
+ treatmentAvgLatencyMs: 0
1942
+ }
1943
+ };
1944
+ this.abTests.set(testId, test);
1945
+ this.audit("ab_test_start", testId, `${control.version} vs ${treatment.version}`, actor, { trafficSplit });
1946
+ return test;
1947
+ }
1948
+ /**
1949
+ * A/B 测试路由
1950
+ */
1951
+ resolveABTest(testId, requestHash) {
1952
+ const test = this.abTests.get(testId);
1953
+ if (!test || test.status !== "running") return null;
1954
+ const hash = requestHash ?? Math.random() * 100;
1955
+ const isTreatment = hash % 100 < test.trafficSplit;
1956
+ const versionId = isTreatment ? test.treatmentVersionId : test.controlVersionId;
1957
+ const version = this.versions.get(versionId);
1958
+ if (!version) return null;
1959
+ return { version, group: isTreatment ? "treatment" : "control" };
1960
+ }
1961
+ /**
1962
+ * 结束 A/B 测试
1963
+ */
1964
+ endABTest(testId, actor) {
1965
+ const test = this.abTests.get(testId);
1966
+ if (!test) return null;
1967
+ test.status = "completed";
1968
+ this.audit("ab_test_end", testId, "", actor, { metrics: test.metrics });
1969
+ return test;
1970
+ }
1971
+ /**
1972
+ * 获取所有版本
1973
+ */
1974
+ listVersions() {
1975
+ return Array.from(this.versions.values()).sort((a, b) => b.createdAt - a.createdAt);
1976
+ }
1977
+ /**
1978
+ * 获取当前 active 版本
1979
+ */
1980
+ getActiveVersion() {
1981
+ return this.activeVersionId ? this.versions.get(this.activeVersionId) ?? null : null;
1982
+ }
1983
+ // ============================================
1984
+ // 私有方法
1985
+ // ============================================
1986
+ cleanupOldVersions() {
1987
+ const deprecated = Array.from(this.versions.values()).filter((v) => v.status === "deprecated" || v.status === "rollback").sort((a, b) => a.createdAt - b.createdAt);
1988
+ if (deprecated.length > 0) {
1989
+ this.versions.delete(deprecated[0].id);
1990
+ }
1991
+ }
1992
+ audit(type, policyId, version, actor, details) {
1993
+ if (this.config.onAudit) {
1994
+ this.config.onAudit({ type, policyId, version, timestamp: Date.now(), actor, details });
1995
+ }
1996
+ }
1997
+ };
1998
+ function createEvolutionChannel(config) {
1999
+ return new EvolutionChannelEngine(config);
2000
+ }
2001
+
1
2002
  // src/index.ts
2
2003
  var DEFAULT_RULES = [
3
2004
  // --- HTTP Proxy: Dangerous outbound calls ---
@@ -232,7 +2233,7 @@ var RISK_SCORES = {
232
2233
  high: 70,
233
2234
  critical: 95
234
2235
  };
235
- var ENGINE_VERSION = "3.2.0";
2236
+ var ENGINE_VERSION = "3.4.0";
236
2237
  var ENGINE_VERSION_CHECK_URL = "https://api.sovr.inc/api/sovr/v1/version/check";
237
2238
  var ENGINE_TIER_LIMITS = {
238
2239
  free: { evaluationsPerMonth: 50, irreversibleAllowsPerMonth: 0 },
@@ -486,7 +2487,34 @@ var PolicyEngine = class {
486
2487
  };
487
2488
  var index_default = PolicyEngine;
488
2489
  export {
2490
+ AdaptiveThresholdManager,
2491
+ AutoHardenEngine,
2492
+ CostGateEnhancedEngine,
489
2493
  DEFAULT_RULES,
2494
+ EvolutionChannelEngine,
2495
+ FeatureSwitchesManager,
2496
+ MultiLevelBudgetEngine,
490
2497
  PolicyEngine,
491
- index_default as default
2498
+ PricingRulesEngine,
2499
+ RecalculationEngine,
2500
+ SOVR_FEATURE_SWITCHES,
2501
+ SemanticDriftDetectorEngine,
2502
+ compileFromJSON,
2503
+ compileRuleSet,
2504
+ createAutoHardenEngine,
2505
+ createCostGateEnhanced,
2506
+ createEvolutionChannel,
2507
+ createMultiLevelBudget,
2508
+ createRecalculationEngine,
2509
+ createSemanticDriftDetector,
2510
+ index_default as default,
2511
+ estimateCost,
2512
+ evaluateRules,
2513
+ getAccuracyStats,
2514
+ getModelPricingTable,
2515
+ getToolPricingTable,
2516
+ recordActualCost,
2517
+ registerFunction,
2518
+ updateModelPricing,
2519
+ updateToolPricing
492
2520
  };