@sovr/engine 3.4.0 → 3.5.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.d.mts +678 -7
- package/dist/index.d.ts +678 -7
- package/dist/index.js +1220 -0
- package/dist/index.mjs +1210 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,16 +22,26 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AdaptiveThresholdManager: () => AdaptiveThresholdManager,
|
|
24
24
|
AutoHardenEngine: () => AutoHardenEngine,
|
|
25
|
+
ContextAccelerator: () => ContextAccelerator,
|
|
25
26
|
CostGateEnhancedEngine: () => CostGateEnhancedEngine,
|
|
27
|
+
DEFAULT_CA_CONFIG: () => DEFAULT_CA_CONFIG,
|
|
28
|
+
DEFAULT_GE_CONFIG: () => DEFAULT_GE_CONFIG,
|
|
29
|
+
DEFAULT_HARD_RULES: () => DEFAULT_HARD_RULES,
|
|
26
30
|
DEFAULT_RULES: () => DEFAULT_RULES,
|
|
31
|
+
DEFAULT_SCORING_WEIGHTS: () => DEFAULT_SCORING_WEIGHTS,
|
|
32
|
+
DEFAULT_TSA_CONFIG: () => DEFAULT_TSA_CONFIG,
|
|
27
33
|
EvolutionChannelEngine: () => EvolutionChannelEngine,
|
|
28
34
|
FeatureSwitchesManager: () => FeatureSwitchesManager,
|
|
35
|
+
GovernanceEnhancer: () => GovernanceEnhancer,
|
|
29
36
|
MultiLevelBudgetEngine: () => MultiLevelBudgetEngine,
|
|
30
37
|
PolicyEngine: () => PolicyEngine,
|
|
31
38
|
PricingRulesEngine: () => PricingRulesEngine,
|
|
32
39
|
RecalculationEngine: () => RecalculationEngine,
|
|
33
40
|
SOVR_FEATURE_SWITCHES: () => SOVR_FEATURE_SWITCHES,
|
|
34
41
|
SemanticDriftDetectorEngine: () => SemanticDriftDetectorEngine,
|
|
42
|
+
TimeSeriesAggregator: () => TimeSeriesAggregator,
|
|
43
|
+
TwoPhaseRouter: () => TwoPhaseRouter,
|
|
44
|
+
ValuationModel: () => ValuationModel,
|
|
35
45
|
compileFromJSON: () => compileFromJSON,
|
|
36
46
|
compileRuleSet: () => compileRuleSet,
|
|
37
47
|
createAutoHardenEngine: () => createAutoHardenEngine,
|
|
@@ -2054,6 +2064,1206 @@ function createEvolutionChannel(config) {
|
|
|
2054
2064
|
return new EvolutionChannelEngine(config);
|
|
2055
2065
|
}
|
|
2056
2066
|
|
|
2067
|
+
// src/twoPhaseRouter.ts
|
|
2068
|
+
var DEFAULT_HARD_RULES = [
|
|
2069
|
+
{
|
|
2070
|
+
id: "HR-001",
|
|
2071
|
+
name: "high_risk_requires_powerful",
|
|
2072
|
+
condition: (req, candidate) => (req.riskLevel === "critical" || req.riskLevel === "high") && candidate.tier === "cheap",
|
|
2073
|
+
reasonCode: "RISK_TIER_MISMATCH",
|
|
2074
|
+
reasonTemplate: "High/critical risk tasks cannot use cheap tier models",
|
|
2075
|
+
enabled: true
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
id: "HR-002",
|
|
2079
|
+
name: "critical_requires_powerful_only",
|
|
2080
|
+
condition: (req, candidate) => req.riskLevel === "critical" && candidate.tier !== "powerful",
|
|
2081
|
+
reasonCode: "RISK_TIER_MISMATCH",
|
|
2082
|
+
reasonTemplate: "Critical risk tasks require powerful tier models only",
|
|
2083
|
+
enabled: true
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
id: "HR-003",
|
|
2087
|
+
name: "budget_hard_cap",
|
|
2088
|
+
condition: (req, candidate) => {
|
|
2089
|
+
if (!req.maxCostUsd) return false;
|
|
2090
|
+
const estimatedCost = req.estimatedTokens / 1e3 * candidate.costPer1kTokens;
|
|
2091
|
+
return estimatedCost > req.maxCostUsd;
|
|
2092
|
+
},
|
|
2093
|
+
reasonCode: "COST_EXCEEDS_BUDGET",
|
|
2094
|
+
reasonTemplate: "Estimated cost exceeds hard budget cap",
|
|
2095
|
+
enabled: true
|
|
2096
|
+
}
|
|
2097
|
+
];
|
|
2098
|
+
var DEFAULT_SCORING_WEIGHTS = {
|
|
2099
|
+
cost: 0.25,
|
|
2100
|
+
latency: 0.2,
|
|
2101
|
+
capability: 0.25,
|
|
2102
|
+
weight: 0.15,
|
|
2103
|
+
tierMatch: 0.15
|
|
2104
|
+
};
|
|
2105
|
+
var TwoPhaseRouter = class {
|
|
2106
|
+
config;
|
|
2107
|
+
constructor(config) {
|
|
2108
|
+
this.config = {
|
|
2109
|
+
...config,
|
|
2110
|
+
hardRules: config.hardRules.length > 0 ? config.hardRules : DEFAULT_HARD_RULES,
|
|
2111
|
+
scoringWeights: config.scoringWeights ?? DEFAULT_SCORING_WEIGHTS
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Phase 1: Eligibility — 硬性规则过滤
|
|
2116
|
+
*/
|
|
2117
|
+
evaluateEligibility(request) {
|
|
2118
|
+
return this.config.models.map((candidate) => {
|
|
2119
|
+
if (!candidate.enabled) {
|
|
2120
|
+
return {
|
|
2121
|
+
candidateId: candidate.id,
|
|
2122
|
+
eligible: false,
|
|
2123
|
+
reasonCode: "DISABLED",
|
|
2124
|
+
reasonDetail: `Model ${candidate.name} is disabled`
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
const missingCaps = request.requiredCapabilities.filter(
|
|
2128
|
+
(cap) => !candidate.capabilities.includes(cap)
|
|
2129
|
+
);
|
|
2130
|
+
if (missingCaps.length > 0) {
|
|
2131
|
+
return {
|
|
2132
|
+
candidateId: candidate.id,
|
|
2133
|
+
eligible: false,
|
|
2134
|
+
reasonCode: "CAPABILITY_MISSING",
|
|
2135
|
+
reasonDetail: `Missing capabilities: ${missingCaps.join(", ")}`
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
if (request.estimatedTokens > candidate.maxContextTokens) {
|
|
2139
|
+
return {
|
|
2140
|
+
candidateId: candidate.id,
|
|
2141
|
+
eligible: false,
|
|
2142
|
+
reasonCode: "CONTEXT_TOO_LARGE",
|
|
2143
|
+
reasonDetail: `Estimated ${request.estimatedTokens} tokens exceeds max ${candidate.maxContextTokens}`
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
if (request.maxLatencyMs && candidate.avgLatencyMs > request.maxLatencyMs) {
|
|
2147
|
+
return {
|
|
2148
|
+
candidateId: candidate.id,
|
|
2149
|
+
eligible: false,
|
|
2150
|
+
reasonCode: "LATENCY_EXCEEDS_LIMIT",
|
|
2151
|
+
reasonDetail: `Avg latency ${candidate.avgLatencyMs}ms exceeds limit ${request.maxLatencyMs}ms`
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
for (const rule of this.config.hardRules) {
|
|
2155
|
+
if (!rule.enabled) continue;
|
|
2156
|
+
if (rule.condition(request, candidate)) {
|
|
2157
|
+
return {
|
|
2158
|
+
candidateId: candidate.id,
|
|
2159
|
+
eligible: false,
|
|
2160
|
+
reasonCode: rule.reasonCode,
|
|
2161
|
+
reasonDetail: rule.reasonTemplate
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return {
|
|
2166
|
+
candidateId: candidate.id,
|
|
2167
|
+
eligible: true,
|
|
2168
|
+
reasonCode: "ELIGIBLE",
|
|
2169
|
+
reasonDetail: "Passed all eligibility checks"
|
|
2170
|
+
};
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Phase 2: Scoring — 软性指标加权评分
|
|
2175
|
+
*/
|
|
2176
|
+
evaluateScoring(request, eligibleIds) {
|
|
2177
|
+
const eligible = this.config.models.filter((m) => eligibleIds.has(m.id));
|
|
2178
|
+
if (eligible.length === 0) return [];
|
|
2179
|
+
const w = this.config.scoringWeights;
|
|
2180
|
+
const maxCost = Math.max(...eligible.map((m) => m.costPer1kTokens), 1e-3);
|
|
2181
|
+
const maxLatency = Math.max(...eligible.map((m) => m.avgLatencyMs), 1);
|
|
2182
|
+
return eligible.map((candidate) => {
|
|
2183
|
+
const costScore = 100 * (1 - candidate.costPer1kTokens / maxCost);
|
|
2184
|
+
const latencyScore = 100 * (1 - candidate.avgLatencyMs / maxLatency);
|
|
2185
|
+
const matchedCaps = request.requiredCapabilities.filter(
|
|
2186
|
+
(cap) => candidate.capabilities.includes(cap)
|
|
2187
|
+
).length;
|
|
2188
|
+
const capabilityScore = request.requiredCapabilities.length > 0 ? 100 * (matchedCaps / request.requiredCapabilities.length) : 50;
|
|
2189
|
+
const weightScore = candidate.weight;
|
|
2190
|
+
let tierMatchScore = 50;
|
|
2191
|
+
if (request.preferredTier) {
|
|
2192
|
+
if (candidate.tier === request.preferredTier) tierMatchScore = 100;
|
|
2193
|
+
else {
|
|
2194
|
+
const tierOrder = ["cheap", "balanced", "powerful"];
|
|
2195
|
+
const diff = Math.abs(
|
|
2196
|
+
tierOrder.indexOf(candidate.tier) - tierOrder.indexOf(request.preferredTier)
|
|
2197
|
+
);
|
|
2198
|
+
tierMatchScore = Math.max(0, 100 - diff * 40);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
const score = costScore * w.cost + latencyScore * w.latency + capabilityScore * w.capability + weightScore * w.weight + tierMatchScore * w.tierMatch;
|
|
2202
|
+
return {
|
|
2203
|
+
candidateId: candidate.id,
|
|
2204
|
+
score: Math.round(score * 100) / 100,
|
|
2205
|
+
breakdown: {
|
|
2206
|
+
costScore: Math.round(costScore * 100) / 100,
|
|
2207
|
+
latencyScore: Math.round(latencyScore * 100) / 100,
|
|
2208
|
+
capabilityScore: Math.round(capabilityScore * 100) / 100,
|
|
2209
|
+
weightScore: Math.round(weightScore * 100) / 100,
|
|
2210
|
+
tierMatchScore: Math.round(tierMatchScore * 100) / 100
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Main routing decision
|
|
2217
|
+
*/
|
|
2218
|
+
async route(request) {
|
|
2219
|
+
const startTime = Date.now();
|
|
2220
|
+
const phase1Results = this.evaluateEligibility(request);
|
|
2221
|
+
const eligibleIds = new Set(
|
|
2222
|
+
phase1Results.filter((r) => r.eligible).map((r) => r.candidateId)
|
|
2223
|
+
);
|
|
2224
|
+
const phase2Results = this.evaluateScoring(request, eligibleIds);
|
|
2225
|
+
phase2Results.sort((a, b) => b.score - a.score);
|
|
2226
|
+
let selectedModelId;
|
|
2227
|
+
let fallbackUsed = false;
|
|
2228
|
+
const reasonChain = [];
|
|
2229
|
+
if (phase2Results.length > 0) {
|
|
2230
|
+
selectedModelId = phase2Results[0].candidateId;
|
|
2231
|
+
reasonChain.push(
|
|
2232
|
+
`Phase 1: ${eligibleIds.size}/${this.config.models.length} candidates eligible`
|
|
2233
|
+
);
|
|
2234
|
+
reasonChain.push(
|
|
2235
|
+
`Phase 2: Top scorer = ${selectedModelId} (score: ${phase2Results[0].score})`
|
|
2236
|
+
);
|
|
2237
|
+
const rejected = phase1Results.filter((r) => !r.eligible);
|
|
2238
|
+
if (rejected.length > 0) {
|
|
2239
|
+
reasonChain.push(
|
|
2240
|
+
`Rejected: ${rejected.map((r) => `${r.candidateId}(${r.reasonCode})`).join(", ")}`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
} else if (this.config.fallbackModelId) {
|
|
2244
|
+
selectedModelId = this.config.fallbackModelId;
|
|
2245
|
+
fallbackUsed = true;
|
|
2246
|
+
reasonChain.push("Phase 1: No eligible candidates");
|
|
2247
|
+
reasonChain.push(`Fallback: Using ${selectedModelId}`);
|
|
2248
|
+
} else {
|
|
2249
|
+
const leastBad = this.config.models.find((m) => m.enabled);
|
|
2250
|
+
selectedModelId = leastBad?.id ?? this.config.models[0]?.id ?? "unknown";
|
|
2251
|
+
fallbackUsed = true;
|
|
2252
|
+
reasonChain.push("Phase 1: No eligible candidates, no fallback configured");
|
|
2253
|
+
reasonChain.push(`Emergency: Using ${selectedModelId}`);
|
|
2254
|
+
}
|
|
2255
|
+
const selectedModel = this.config.models.find((m) => m.id === selectedModelId);
|
|
2256
|
+
const decisionTimeMs = Date.now() - startTime;
|
|
2257
|
+
const decision = {
|
|
2258
|
+
selectedModelId,
|
|
2259
|
+
selectedModelName: selectedModel?.name ?? "unknown",
|
|
2260
|
+
tier: selectedModel?.tier ?? this.config.defaultTier,
|
|
2261
|
+
phase1Results,
|
|
2262
|
+
phase2Results,
|
|
2263
|
+
reasonChain,
|
|
2264
|
+
totalCandidates: this.config.models.length,
|
|
2265
|
+
eligibleCandidates: eligibleIds.size,
|
|
2266
|
+
decisionTimeMs,
|
|
2267
|
+
fallbackUsed
|
|
2268
|
+
};
|
|
2269
|
+
if (this.config.onDecision) {
|
|
2270
|
+
try {
|
|
2271
|
+
await this.config.onDecision(decision);
|
|
2272
|
+
} catch {
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (this.config.onAudit) {
|
|
2276
|
+
try {
|
|
2277
|
+
await this.config.onAudit({
|
|
2278
|
+
type: "routing_decision",
|
|
2279
|
+
data: {
|
|
2280
|
+
selectedModelId,
|
|
2281
|
+
tier: decision.tier,
|
|
2282
|
+
eligibleCount: eligibleIds.size,
|
|
2283
|
+
totalCount: this.config.models.length,
|
|
2284
|
+
fallbackUsed,
|
|
2285
|
+
decisionTimeMs,
|
|
2286
|
+
taskType: request.taskType,
|
|
2287
|
+
riskLevel: request.riskLevel
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
} catch {
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return decision;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Get models by tier
|
|
2297
|
+
*/
|
|
2298
|
+
getModelsByTier(tier) {
|
|
2299
|
+
return this.config.models.filter((m) => m.tier === tier && m.enabled);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Update model status
|
|
2303
|
+
*/
|
|
2304
|
+
updateModel(modelId, updates) {
|
|
2305
|
+
const idx = this.config.models.findIndex((m) => m.id === modelId);
|
|
2306
|
+
if (idx === -1) return false;
|
|
2307
|
+
this.config.models[idx] = { ...this.config.models[idx], ...updates };
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Add a new hard rule
|
|
2312
|
+
*/
|
|
2313
|
+
addHardRule(rule) {
|
|
2314
|
+
this.config.hardRules.push(rule);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Get routing explanation for a specific request (dry run)
|
|
2318
|
+
*/
|
|
2319
|
+
async explain(request) {
|
|
2320
|
+
const decision = await this.route(request);
|
|
2321
|
+
const lines = [
|
|
2322
|
+
`=== SOVR Routing Explanation ===`,
|
|
2323
|
+
`Task: ${request.taskType} | Risk: ${request.riskLevel}`,
|
|
2324
|
+
`Required Capabilities: ${request.requiredCapabilities.join(", ") || "none"}`,
|
|
2325
|
+
`Estimated Tokens: ${request.estimatedTokens}`,
|
|
2326
|
+
``,
|
|
2327
|
+
`--- Phase 1: Eligibility ---`
|
|
2328
|
+
];
|
|
2329
|
+
for (const r of decision.phase1Results) {
|
|
2330
|
+
const model = this.config.models.find((m) => m.id === r.candidateId);
|
|
2331
|
+
lines.push(
|
|
2332
|
+
` ${r.eligible ? "\u2705" : "\u274C"} ${model?.name ?? r.candidateId} [${r.reasonCode}] ${r.reasonDetail}`
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
lines.push("", `--- Phase 2: Scoring ---`);
|
|
2336
|
+
for (const r of decision.phase2Results) {
|
|
2337
|
+
const model = this.config.models.find((m) => m.id === r.candidateId);
|
|
2338
|
+
lines.push(
|
|
2339
|
+
` ${model?.name ?? r.candidateId}: ${r.score} (cost=${r.breakdown.costScore} lat=${r.breakdown.latencyScore} cap=${r.breakdown.capabilityScore})`
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
lines.push(
|
|
2343
|
+
"",
|
|
2344
|
+
`--- Decision ---`,
|
|
2345
|
+
`Selected: ${decision.selectedModelName} (${decision.tier})`,
|
|
2346
|
+
`Fallback: ${decision.fallbackUsed ? "YES" : "NO"}`,
|
|
2347
|
+
`Time: ${decision.decisionTimeMs}ms`
|
|
2348
|
+
);
|
|
2349
|
+
return { decision, explanation: lines.join("\n") };
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
// src/timeSeriesAggregator.ts
|
|
2354
|
+
var DEFAULT_TSA_CONFIG = {
|
|
2355
|
+
windowSizeMs: 6e4,
|
|
2356
|
+
// 1 minute
|
|
2357
|
+
windowType: "tumbling",
|
|
2358
|
+
maxWindows: 60,
|
|
2359
|
+
// Keep 1 hour of 1-min windows
|
|
2360
|
+
maxValuesPerWindow: 1e4
|
|
2361
|
+
};
|
|
2362
|
+
function percentile(sorted, p) {
|
|
2363
|
+
if (sorted.length === 0) return 0;
|
|
2364
|
+
if (sorted.length === 1) return sorted[0];
|
|
2365
|
+
const idx = p / 100 * (sorted.length - 1);
|
|
2366
|
+
const lower = Math.floor(idx);
|
|
2367
|
+
const upper = Math.ceil(idx);
|
|
2368
|
+
if (lower === upper) return sorted[lower];
|
|
2369
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
2370
|
+
}
|
|
2371
|
+
var TimeSeriesAggregator = class {
|
|
2372
|
+
windows = /* @__PURE__ */ new Map();
|
|
2373
|
+
closedResults = [];
|
|
2374
|
+
config;
|
|
2375
|
+
totalDedups = 0;
|
|
2376
|
+
constructor(config) {
|
|
2377
|
+
this.config = { ...DEFAULT_TSA_CONFIG, ...config };
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Ingest a data point (idempotent — duplicates are rejected)
|
|
2381
|
+
*/
|
|
2382
|
+
async ingest(point) {
|
|
2383
|
+
const windowId = this.getWindowId(point.timestamp);
|
|
2384
|
+
let window = this.windows.get(windowId);
|
|
2385
|
+
if (!window) {
|
|
2386
|
+
window = this.createWindow(windowId, point.timestamp);
|
|
2387
|
+
this.windows.set(windowId, window);
|
|
2388
|
+
this.evictOldWindows();
|
|
2389
|
+
}
|
|
2390
|
+
if (window.seenIds.has(point.id)) {
|
|
2391
|
+
this.totalDedups++;
|
|
2392
|
+
return { accepted: false, windowId, duplicate: true };
|
|
2393
|
+
}
|
|
2394
|
+
window.seenIds.add(point.id);
|
|
2395
|
+
window.count++;
|
|
2396
|
+
window.sum += point.value;
|
|
2397
|
+
window.min = Math.min(window.min, point.value);
|
|
2398
|
+
window.max = Math.max(window.max, point.value);
|
|
2399
|
+
if (window.values.length < this.config.maxValuesPerWindow) {
|
|
2400
|
+
window.values.push(point.value);
|
|
2401
|
+
}
|
|
2402
|
+
return { accepted: true, windowId, duplicate: false };
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Ingest multiple points (batch)
|
|
2406
|
+
*/
|
|
2407
|
+
async ingestBatch(points) {
|
|
2408
|
+
let accepted = 0;
|
|
2409
|
+
let duplicates = 0;
|
|
2410
|
+
let errors = 0;
|
|
2411
|
+
for (const point of points) {
|
|
2412
|
+
try {
|
|
2413
|
+
const result = await this.ingest(point);
|
|
2414
|
+
if (result.accepted) accepted++;
|
|
2415
|
+
if (result.duplicate) duplicates++;
|
|
2416
|
+
} catch {
|
|
2417
|
+
errors++;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return { accepted, duplicates, errors };
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Get aggregation result for a specific window
|
|
2424
|
+
*/
|
|
2425
|
+
getWindowResult(windowId) {
|
|
2426
|
+
const window = this.windows.get(windowId);
|
|
2427
|
+
if (!window) {
|
|
2428
|
+
return this.closedResults.find((r) => r.windowId === windowId) ?? null;
|
|
2429
|
+
}
|
|
2430
|
+
return this.computeResult(window);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Get current (latest) window result
|
|
2434
|
+
*/
|
|
2435
|
+
getCurrentResult() {
|
|
2436
|
+
const now = Date.now();
|
|
2437
|
+
const windowId = this.getWindowId(now);
|
|
2438
|
+
return this.getWindowResult(windowId);
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Get results for a time range
|
|
2442
|
+
*/
|
|
2443
|
+
getResultsInRange(startMs, endMs) {
|
|
2444
|
+
const results = [];
|
|
2445
|
+
for (const window of this.windows.values()) {
|
|
2446
|
+
if (window.startMs >= startMs && window.endMs <= endMs) {
|
|
2447
|
+
results.push(this.computeResult(window));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
for (const result of this.closedResults) {
|
|
2451
|
+
if (result.startMs >= startMs && result.endMs <= endMs) {
|
|
2452
|
+
if (!results.find((r) => r.windowId === result.windowId)) {
|
|
2453
|
+
results.push(result);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return results.sort((a, b) => a.startMs - b.startMs);
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Close current window and emit result
|
|
2461
|
+
*/
|
|
2462
|
+
async closeWindow(windowId) {
|
|
2463
|
+
const window = this.windows.get(windowId);
|
|
2464
|
+
if (!window) return null;
|
|
2465
|
+
const result = this.computeResult(window);
|
|
2466
|
+
this.closedResults.push(result);
|
|
2467
|
+
this.windows.delete(windowId);
|
|
2468
|
+
if (this.closedResults.length > this.config.maxWindows * 2) {
|
|
2469
|
+
this.closedResults = this.closedResults.slice(-this.config.maxWindows);
|
|
2470
|
+
}
|
|
2471
|
+
if (this.config.onWindowClosed) {
|
|
2472
|
+
try {
|
|
2473
|
+
await this.config.onWindowClosed(result);
|
|
2474
|
+
} catch {
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (this.config.onAudit) {
|
|
2478
|
+
try {
|
|
2479
|
+
await this.config.onAudit({
|
|
2480
|
+
type: "window_closed",
|
|
2481
|
+
data: {
|
|
2482
|
+
windowId,
|
|
2483
|
+
count: result.metrics.count,
|
|
2484
|
+
avg: result.metrics.avg,
|
|
2485
|
+
p95: result.metrics.p95,
|
|
2486
|
+
dedupCount: result.dedupCount
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return result;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Get overall stats
|
|
2496
|
+
*/
|
|
2497
|
+
getStats() {
|
|
2498
|
+
let totalDataPoints = 0;
|
|
2499
|
+
for (const w of this.windows.values()) totalDataPoints += w.count;
|
|
2500
|
+
for (const r of this.closedResults) totalDataPoints += r.metrics.count;
|
|
2501
|
+
return {
|
|
2502
|
+
activeWindows: this.windows.size,
|
|
2503
|
+
closedWindows: this.closedResults.length,
|
|
2504
|
+
totalDedups: this.totalDedups,
|
|
2505
|
+
totalDataPoints
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Reset all windows
|
|
2510
|
+
*/
|
|
2511
|
+
reset() {
|
|
2512
|
+
this.windows.clear();
|
|
2513
|
+
this.closedResults = [];
|
|
2514
|
+
this.totalDedups = 0;
|
|
2515
|
+
}
|
|
2516
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
2517
|
+
getWindowId(timestamp) {
|
|
2518
|
+
const windowStart = Math.floor(timestamp / this.config.windowSizeMs) * this.config.windowSizeMs;
|
|
2519
|
+
return `w_${windowStart}`;
|
|
2520
|
+
}
|
|
2521
|
+
createWindow(windowId, timestamp) {
|
|
2522
|
+
const windowStart = Math.floor(timestamp / this.config.windowSizeMs) * this.config.windowSizeMs;
|
|
2523
|
+
return {
|
|
2524
|
+
windowId,
|
|
2525
|
+
windowType: this.config.windowType,
|
|
2526
|
+
startMs: windowStart,
|
|
2527
|
+
endMs: windowStart + this.config.windowSizeMs,
|
|
2528
|
+
count: 0,
|
|
2529
|
+
sum: 0,
|
|
2530
|
+
min: Infinity,
|
|
2531
|
+
max: -Infinity,
|
|
2532
|
+
values: [],
|
|
2533
|
+
seenIds: /* @__PURE__ */ new Set()
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
computeResult(window) {
|
|
2537
|
+
const sorted = [...window.values].sort((a, b) => a - b);
|
|
2538
|
+
const durationSec = (window.endMs - window.startMs) / 1e3;
|
|
2539
|
+
return {
|
|
2540
|
+
windowId: window.windowId,
|
|
2541
|
+
windowType: window.windowType,
|
|
2542
|
+
startMs: window.startMs,
|
|
2543
|
+
endMs: window.endMs,
|
|
2544
|
+
metrics: {
|
|
2545
|
+
count: window.count,
|
|
2546
|
+
sum: window.sum,
|
|
2547
|
+
avg: window.count > 0 ? window.sum / window.count : 0,
|
|
2548
|
+
min: window.min === Infinity ? 0 : window.min,
|
|
2549
|
+
max: window.max === -Infinity ? 0 : window.max,
|
|
2550
|
+
p50: percentile(sorted, 50),
|
|
2551
|
+
p95: percentile(sorted, 95),
|
|
2552
|
+
p99: percentile(sorted, 99),
|
|
2553
|
+
rate: durationSec > 0 ? window.count / durationSec : 0
|
|
2554
|
+
},
|
|
2555
|
+
dedupCount: window.seenIds.size - window.count > 0 ? 0 : this.totalDedups
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
evictOldWindows() {
|
|
2559
|
+
if (this.windows.size <= this.config.maxWindows) return;
|
|
2560
|
+
const sorted = Array.from(this.windows.entries()).sort((a, b) => a[1].startMs - b[1].startMs);
|
|
2561
|
+
while (sorted.length > this.config.maxWindows) {
|
|
2562
|
+
const [id, window] = sorted.shift();
|
|
2563
|
+
this.closedResults.push(this.computeResult(window));
|
|
2564
|
+
this.windows.delete(id);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// src/valuationModel.ts
|
|
2570
|
+
var ValuationModel = class {
|
|
2571
|
+
config;
|
|
2572
|
+
constructor(config) {
|
|
2573
|
+
this.config = config ?? {};
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Calculate TAM/SAM/SOM
|
|
2577
|
+
*/
|
|
2578
|
+
calculateMarketSize(input) {
|
|
2579
|
+
return {
|
|
2580
|
+
tam: input.totalMarketUsd,
|
|
2581
|
+
sam: input.totalMarketUsd * input.serviceablePercent,
|
|
2582
|
+
som: input.totalMarketUsd * input.serviceablePercent * input.obtainablePercent,
|
|
2583
|
+
year: input.year,
|
|
2584
|
+
growthRate: input.growthRate,
|
|
2585
|
+
assumptions: input.assumptions
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Calculate Unit Economics
|
|
2590
|
+
*/
|
|
2591
|
+
calculateUnitEconomics(input) {
|
|
2592
|
+
const netChurn = input.churnRate - input.expansionRate;
|
|
2593
|
+
const effectiveChurn = Math.max(netChurn, 1e-3);
|
|
2594
|
+
const ltv = input.arpu * input.grossMargin / effectiveChurn;
|
|
2595
|
+
const paybackMonths = input.cac / (input.arpu * input.grossMargin);
|
|
2596
|
+
return {
|
|
2597
|
+
arpu: input.arpu,
|
|
2598
|
+
cac: input.cac,
|
|
2599
|
+
ltv: Math.round(ltv * 100) / 100,
|
|
2600
|
+
ltvCacRatio: Math.round(ltv / input.cac * 100) / 100,
|
|
2601
|
+
paybackMonths: Math.round(paybackMonths * 10) / 10,
|
|
2602
|
+
grossMargin: input.grossMargin,
|
|
2603
|
+
churnRate: input.churnRate,
|
|
2604
|
+
expansionRate: input.expansionRate
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* DCF Valuation
|
|
2609
|
+
*/
|
|
2610
|
+
calculateDCF(input) {
|
|
2611
|
+
const fcfs = [];
|
|
2612
|
+
const discountedFcfs = [];
|
|
2613
|
+
for (let i = 0; i < input.projectedRevenues.length; i++) {
|
|
2614
|
+
const revenue = input.projectedRevenues[i];
|
|
2615
|
+
const operatingIncome = revenue * input.operatingMargin;
|
|
2616
|
+
const afterTax = operatingIncome * (1 - input.taxRate);
|
|
2617
|
+
const capex = revenue * input.capexRatio;
|
|
2618
|
+
const fcf = afterTax - capex;
|
|
2619
|
+
fcfs.push(fcf);
|
|
2620
|
+
const discountFactor = Math.pow(1 + input.discountRate, i + 1);
|
|
2621
|
+
discountedFcfs.push(fcf / discountFactor);
|
|
2622
|
+
}
|
|
2623
|
+
const presentValue = discountedFcfs.reduce((sum, v) => sum + v, 0);
|
|
2624
|
+
const lastFcf = fcfs[fcfs.length - 1] ?? 0;
|
|
2625
|
+
const terminalFcf = lastFcf * (1 + input.terminalGrowthRate);
|
|
2626
|
+
const terminalValue = terminalFcf / (input.discountRate - input.terminalGrowthRate);
|
|
2627
|
+
const discountedTerminal = terminalValue / Math.pow(1 + input.discountRate, input.projectedRevenues.length);
|
|
2628
|
+
const enterpriseValue = presentValue + discountedTerminal;
|
|
2629
|
+
const lastRevenue = input.projectedRevenues[input.projectedRevenues.length - 1] ?? 1;
|
|
2630
|
+
const impliedMultiple = enterpriseValue / lastRevenue;
|
|
2631
|
+
return {
|
|
2632
|
+
presentValue: Math.round(presentValue),
|
|
2633
|
+
terminalValue: Math.round(discountedTerminal),
|
|
2634
|
+
enterpriseValue: Math.round(enterpriseValue),
|
|
2635
|
+
freeCashFlows: fcfs.map((v) => Math.round(v)),
|
|
2636
|
+
discountedCashFlows: discountedFcfs.map((v) => Math.round(v)),
|
|
2637
|
+
impliedMultiple: Math.round(impliedMultiple * 10) / 10
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Comparable Company Valuation
|
|
2642
|
+
*/
|
|
2643
|
+
calculateComparable(input) {
|
|
2644
|
+
const revMultiples = input.comparables.map((c) => c.evRevenue);
|
|
2645
|
+
const ebitdaMultiples = input.comparables.map((c) => c.evEbitda);
|
|
2646
|
+
const userValues = input.comparables.map((c) => c.evUser);
|
|
2647
|
+
const medianRevMultiple = this.median(revMultiples);
|
|
2648
|
+
const medianEbitdaMultiple = this.median(ebitdaMultiples);
|
|
2649
|
+
const medianUserValue = this.median(userValues);
|
|
2650
|
+
const revenueVal = input.revenue * medianRevMultiple;
|
|
2651
|
+
const ebitdaVal = input.ebitda * medianEbitdaMultiple;
|
|
2652
|
+
const userVal = input.users * medianUserValue;
|
|
2653
|
+
const avgCompGrowth = input.comparables.reduce((s, c) => s + c.growthRate, 0) / input.comparables.length;
|
|
2654
|
+
const growthPremium = input.growthRate / (avgCompGrowth || 0.1);
|
|
2655
|
+
const growthAdjusted = revenueVal * Math.min(growthPremium, 3);
|
|
2656
|
+
return {
|
|
2657
|
+
revenueMultipleValuation: Math.round(revenueVal),
|
|
2658
|
+
ebitdaMultipleValuation: Math.round(ebitdaVal),
|
|
2659
|
+
perUserValuation: Math.round(userVal),
|
|
2660
|
+
averageValuation: Math.round((revenueVal + ebitdaVal + userVal) / 3),
|
|
2661
|
+
medianMultiple: medianRevMultiple,
|
|
2662
|
+
growthAdjustedValuation: Math.round(growthAdjusted)
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Network Effect Valuation (Metcalfe's Law)
|
|
2667
|
+
*/
|
|
2668
|
+
calculateNetworkEffect(input) {
|
|
2669
|
+
const currentValue = Math.pow(input.currentUsers, input.metcalfeExponent) * input.valuePerConnection * input.networkDensity;
|
|
2670
|
+
const projectedValues = input.projectedUsers.map(
|
|
2671
|
+
(users) => Math.pow(users, input.metcalfeExponent) * input.valuePerConnection * input.networkDensity
|
|
2672
|
+
);
|
|
2673
|
+
const metcalfeValue = Math.pow(input.currentUsers, 2) * input.valuePerConnection;
|
|
2674
|
+
const adjustedValue = metcalfeValue * input.networkDensity;
|
|
2675
|
+
return {
|
|
2676
|
+
currentNetworkValue: Math.round(currentValue),
|
|
2677
|
+
projectedValues: projectedValues.map((v) => Math.round(v)),
|
|
2678
|
+
metcalfeValue: Math.round(metcalfeValue),
|
|
2679
|
+
adjustedValue: Math.round(adjustedValue)
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Sensitivity Analysis
|
|
2684
|
+
*/
|
|
2685
|
+
runSensitivity(baseInput, variables) {
|
|
2686
|
+
const baseResult = this.calculateDCF(baseInput);
|
|
2687
|
+
const baseCase = baseResult.enterpriseValue;
|
|
2688
|
+
const scenarios = [];
|
|
2689
|
+
const tornado = [];
|
|
2690
|
+
for (const v of variables) {
|
|
2691
|
+
const lowInput = { ...baseInput, [v.field]: v.low };
|
|
2692
|
+
const lowResult = this.calculateDCF(lowInput);
|
|
2693
|
+
const highInput = { ...baseInput, [v.field]: v.high };
|
|
2694
|
+
const highResult = this.calculateDCF(highInput);
|
|
2695
|
+
scenarios.push({
|
|
2696
|
+
name: `${v.name} Low`,
|
|
2697
|
+
value: lowResult.enterpriseValue,
|
|
2698
|
+
delta: (lowResult.enterpriseValue - baseCase) / baseCase * 100,
|
|
2699
|
+
variables: { [v.name]: v.low }
|
|
2700
|
+
});
|
|
2701
|
+
scenarios.push({
|
|
2702
|
+
name: `${v.name} High`,
|
|
2703
|
+
value: highResult.enterpriseValue,
|
|
2704
|
+
delta: (highResult.enterpriseValue - baseCase) / baseCase * 100,
|
|
2705
|
+
variables: { [v.name]: v.high }
|
|
2706
|
+
});
|
|
2707
|
+
tornado.push({
|
|
2708
|
+
variable: v.name,
|
|
2709
|
+
lowValue: lowResult.enterpriseValue,
|
|
2710
|
+
highValue: highResult.enterpriseValue,
|
|
2711
|
+
range: Math.abs(highResult.enterpriseValue - lowResult.enterpriseValue)
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
tornado.sort((a, b) => b.range - a.range);
|
|
2715
|
+
return { baseCase, scenarios, tornado };
|
|
2716
|
+
}
|
|
2717
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
2718
|
+
median(values) {
|
|
2719
|
+
if (values.length === 0) return 0;
|
|
2720
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2721
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2722
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// src/governanceEnhancer.ts
|
|
2727
|
+
var DEFAULT_GE_CONFIG = {
|
|
2728
|
+
circuitBreakerThreshold: 5,
|
|
2729
|
+
circuitBreakerTimeoutMs: 3e4,
|
|
2730
|
+
halfOpenSuccessThreshold: 3,
|
|
2731
|
+
degradationChain: [
|
|
2732
|
+
{
|
|
2733
|
+
id: "full",
|
|
2734
|
+
name: "Full Capability",
|
|
2735
|
+
order: 4,
|
|
2736
|
+
capabilities: ["all"],
|
|
2737
|
+
restrictions: []
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
id: "reduced",
|
|
2741
|
+
name: "Reduced Capability",
|
|
2742
|
+
order: 3,
|
|
2743
|
+
capabilities: ["read", "compute", "cache"],
|
|
2744
|
+
restrictions: ["write", "external_api"]
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
id: "minimal",
|
|
2748
|
+
name: "Minimal Capability",
|
|
2749
|
+
order: 2,
|
|
2750
|
+
capabilities: ["read", "cache"],
|
|
2751
|
+
restrictions: ["write", "compute", "external_api"]
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
id: "readonly",
|
|
2755
|
+
name: "Read-Only Mode",
|
|
2756
|
+
order: 1,
|
|
2757
|
+
capabilities: ["read"],
|
|
2758
|
+
restrictions: ["write", "compute", "external_api", "cache"]
|
|
2759
|
+
}
|
|
2760
|
+
],
|
|
2761
|
+
maxPolicyVersions: 10
|
|
2762
|
+
};
|
|
2763
|
+
function simpleHash2(input) {
|
|
2764
|
+
let hash = 0;
|
|
2765
|
+
for (let i = 0; i < input.length; i++) {
|
|
2766
|
+
hash = (hash << 5) - hash + input.charCodeAt(i);
|
|
2767
|
+
hash = hash & hash;
|
|
2768
|
+
}
|
|
2769
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
2770
|
+
}
|
|
2771
|
+
var GovernanceEnhancer = class {
|
|
2772
|
+
currentPolicy = null;
|
|
2773
|
+
policyHistory = [];
|
|
2774
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
2775
|
+
currentDegradationLevel = "full";
|
|
2776
|
+
config;
|
|
2777
|
+
constructor(config) {
|
|
2778
|
+
this.config = { ...DEFAULT_GE_CONFIG, ...config };
|
|
2779
|
+
}
|
|
2780
|
+
// ─── Policy Hot-Reload ───────────────────────────────────────────
|
|
2781
|
+
/**
|
|
2782
|
+
* Load a new policy version (hot-reload)
|
|
2783
|
+
*/
|
|
2784
|
+
async loadPolicy(version, rules) {
|
|
2785
|
+
const hash = simpleHash2(JSON.stringify(rules));
|
|
2786
|
+
const oldVersion = this.currentPolicy?.version;
|
|
2787
|
+
const newPolicy = {
|
|
2788
|
+
version,
|
|
2789
|
+
rules: rules.sort((a, b) => b.priority - a.priority),
|
|
2790
|
+
activatedAt: Date.now(),
|
|
2791
|
+
hash
|
|
2792
|
+
};
|
|
2793
|
+
if (this.currentPolicy) {
|
|
2794
|
+
this.policyHistory.push(this.currentPolicy);
|
|
2795
|
+
if (this.policyHistory.length > this.config.maxPolicyVersions) {
|
|
2796
|
+
this.policyHistory = this.policyHistory.slice(-this.config.maxPolicyVersions);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
this.currentPolicy = newPolicy;
|
|
2800
|
+
if (this.config.onPolicyChange && oldVersion) {
|
|
2801
|
+
try {
|
|
2802
|
+
await this.config.onPolicyChange(oldVersion, version);
|
|
2803
|
+
} catch {
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
if (this.config.onAudit) {
|
|
2807
|
+
try {
|
|
2808
|
+
await this.config.onAudit({
|
|
2809
|
+
type: "policy_loaded",
|
|
2810
|
+
data: { version, ruleCount: rules.length, hash, previousVersion: oldVersion }
|
|
2811
|
+
});
|
|
2812
|
+
} catch {
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
return { success: true, previousVersion: oldVersion, newVersion: version };
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Rollback to a previous policy version
|
|
2819
|
+
*/
|
|
2820
|
+
async rollbackPolicy(version) {
|
|
2821
|
+
const target = this.policyHistory.find((p) => p.version === version);
|
|
2822
|
+
if (!target) return false;
|
|
2823
|
+
if (this.currentPolicy) {
|
|
2824
|
+
this.policyHistory.push(this.currentPolicy);
|
|
2825
|
+
}
|
|
2826
|
+
this.currentPolicy = { ...target, activatedAt: Date.now() };
|
|
2827
|
+
return true;
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Evaluate a governance decision
|
|
2831
|
+
*/
|
|
2832
|
+
async evaluate(context) {
|
|
2833
|
+
if (!this.currentPolicy) {
|
|
2834
|
+
return {
|
|
2835
|
+
allowed: false,
|
|
2836
|
+
action: "deny",
|
|
2837
|
+
reason: "No policy loaded",
|
|
2838
|
+
policyVersion: "none"
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
const cbState = this.getCircuitBreakerState(context.resource);
|
|
2842
|
+
if (cbState === "OPEN") {
|
|
2843
|
+
return {
|
|
2844
|
+
allowed: false,
|
|
2845
|
+
action: "deny",
|
|
2846
|
+
circuitState: "OPEN",
|
|
2847
|
+
reason: `Circuit breaker OPEN for ${context.resource}`,
|
|
2848
|
+
policyVersion: this.currentPolicy.version
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
for (const rule of this.currentPolicy.rules) {
|
|
2852
|
+
if (!rule.enabled) continue;
|
|
2853
|
+
const matches = this.evaluateCondition(rule.condition, context);
|
|
2854
|
+
if (!matches) continue;
|
|
2855
|
+
const decision = {
|
|
2856
|
+
allowed: rule.action === "allow",
|
|
2857
|
+
action: rule.action,
|
|
2858
|
+
ruleId: rule.id,
|
|
2859
|
+
ruleName: rule.name,
|
|
2860
|
+
circuitState: cbState,
|
|
2861
|
+
policyVersion: this.currentPolicy.version,
|
|
2862
|
+
reason: `Rule ${rule.name} matched: ${rule.action}`
|
|
2863
|
+
};
|
|
2864
|
+
if (rule.action === "degrade" && rule.degradeTo) {
|
|
2865
|
+
decision.degradationLevel = rule.degradeTo;
|
|
2866
|
+
decision.allowed = true;
|
|
2867
|
+
this.currentDegradationLevel = rule.degradeTo;
|
|
2868
|
+
}
|
|
2869
|
+
if (this.config.onAudit) {
|
|
2870
|
+
try {
|
|
2871
|
+
await this.config.onAudit({
|
|
2872
|
+
type: "governance_decision",
|
|
2873
|
+
data: {
|
|
2874
|
+
action: context.action,
|
|
2875
|
+
resource: context.resource,
|
|
2876
|
+
decision: rule.action,
|
|
2877
|
+
ruleId: rule.id,
|
|
2878
|
+
policyVersion: this.currentPolicy.version
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
} catch {
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return decision;
|
|
2885
|
+
}
|
|
2886
|
+
return {
|
|
2887
|
+
allowed: true,
|
|
2888
|
+
action: "allow",
|
|
2889
|
+
reason: "No matching rule, default allow",
|
|
2890
|
+
policyVersion: this.currentPolicy.version,
|
|
2891
|
+
circuitState: cbState
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
// ─── Circuit Breaker ─────────────────────────────────────────────
|
|
2895
|
+
/**
|
|
2896
|
+
* Record a success for circuit breaker
|
|
2897
|
+
*/
|
|
2898
|
+
recordSuccess(resource) {
|
|
2899
|
+
const cb = this.getOrCreateCircuitBreaker(resource);
|
|
2900
|
+
cb.successCount++;
|
|
2901
|
+
cb.lastSuccessAt = Date.now();
|
|
2902
|
+
if (cb.state === "HALF_OPEN") {
|
|
2903
|
+
if (cb.successCount >= this.config.halfOpenSuccessThreshold) {
|
|
2904
|
+
cb.state = "CLOSED";
|
|
2905
|
+
cb.failureCount = 0;
|
|
2906
|
+
cb.halfOpenAttempts = 0;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Record a failure for circuit breaker
|
|
2912
|
+
*/
|
|
2913
|
+
recordFailure(resource) {
|
|
2914
|
+
const cb = this.getOrCreateCircuitBreaker(resource);
|
|
2915
|
+
cb.failureCount++;
|
|
2916
|
+
cb.lastFailureAt = Date.now();
|
|
2917
|
+
if (cb.state === "CLOSED" && cb.failureCount >= this.config.circuitBreakerThreshold) {
|
|
2918
|
+
cb.state = "OPEN";
|
|
2919
|
+
cb.openedAt = Date.now();
|
|
2920
|
+
} else if (cb.state === "HALF_OPEN") {
|
|
2921
|
+
cb.state = "OPEN";
|
|
2922
|
+
cb.openedAt = Date.now();
|
|
2923
|
+
cb.halfOpenAttempts++;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Get circuit breaker state for a resource
|
|
2928
|
+
*/
|
|
2929
|
+
getCircuitBreakerState(resource) {
|
|
2930
|
+
const cb = this.circuitBreakers.get(resource);
|
|
2931
|
+
if (!cb) return "CLOSED";
|
|
2932
|
+
if (cb.state === "OPEN") {
|
|
2933
|
+
const elapsed = Date.now() - cb.openedAt;
|
|
2934
|
+
if (elapsed >= this.config.circuitBreakerTimeoutMs) {
|
|
2935
|
+
cb.state = "HALF_OPEN";
|
|
2936
|
+
cb.successCount = 0;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
return cb.state;
|
|
2940
|
+
}
|
|
2941
|
+
// ─── Degradation Chain ───────────────────────────────────────────
|
|
2942
|
+
/**
|
|
2943
|
+
* Get current degradation level
|
|
2944
|
+
*/
|
|
2945
|
+
getCurrentDegradationLevel() {
|
|
2946
|
+
return this.config.degradationChain.find((d) => d.id === this.currentDegradationLevel);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Degrade to next level
|
|
2950
|
+
*/
|
|
2951
|
+
degradeOneLevel() {
|
|
2952
|
+
const current = this.config.degradationChain.find((d) => d.id === this.currentDegradationLevel);
|
|
2953
|
+
if (!current) return null;
|
|
2954
|
+
const nextLevel = this.config.degradationChain.filter((d) => d.order < current.order).sort((a, b) => b.order - a.order)[0];
|
|
2955
|
+
if (nextLevel) {
|
|
2956
|
+
this.currentDegradationLevel = nextLevel.id;
|
|
2957
|
+
return nextLevel;
|
|
2958
|
+
}
|
|
2959
|
+
return null;
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Restore to full capability
|
|
2963
|
+
*/
|
|
2964
|
+
restoreFullCapability() {
|
|
2965
|
+
this.currentDegradationLevel = "full";
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Check if a capability is available at current degradation level
|
|
2969
|
+
*/
|
|
2970
|
+
isCapabilityAvailable(capability) {
|
|
2971
|
+
const level = this.getCurrentDegradationLevel();
|
|
2972
|
+
if (!level) return false;
|
|
2973
|
+
return level.capabilities.includes("all") || level.capabilities.includes(capability);
|
|
2974
|
+
}
|
|
2975
|
+
// ─── Status ──────────────────────────────────────────────────────
|
|
2976
|
+
/**
|
|
2977
|
+
* Get full governance status
|
|
2978
|
+
*/
|
|
2979
|
+
getStatus() {
|
|
2980
|
+
return {
|
|
2981
|
+
policyVersion: this.currentPolicy?.version ?? null,
|
|
2982
|
+
ruleCount: this.currentPolicy?.rules.length ?? 0,
|
|
2983
|
+
degradationLevel: this.currentDegradationLevel,
|
|
2984
|
+
circuitBreakers: Array.from(this.circuitBreakers.values()),
|
|
2985
|
+
policyHistoryCount: this.policyHistory.length
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
2989
|
+
evaluateCondition(condition, context) {
|
|
2990
|
+
if (condition === "*") return true;
|
|
2991
|
+
if (condition.startsWith("action:")) {
|
|
2992
|
+
return context.action === condition.substring(7);
|
|
2993
|
+
}
|
|
2994
|
+
if (condition.startsWith("resource:")) {
|
|
2995
|
+
return context.resource.includes(condition.substring(9));
|
|
2996
|
+
}
|
|
2997
|
+
if (condition.startsWith("agent:")) {
|
|
2998
|
+
return context.agentId === condition.substring(6);
|
|
2999
|
+
}
|
|
3000
|
+
return false;
|
|
3001
|
+
}
|
|
3002
|
+
getOrCreateCircuitBreaker(resource) {
|
|
3003
|
+
let cb = this.circuitBreakers.get(resource);
|
|
3004
|
+
if (!cb) {
|
|
3005
|
+
cb = {
|
|
3006
|
+
name: resource,
|
|
3007
|
+
state: "CLOSED",
|
|
3008
|
+
failureCount: 0,
|
|
3009
|
+
successCount: 0,
|
|
3010
|
+
lastFailureAt: 0,
|
|
3011
|
+
lastSuccessAt: 0,
|
|
3012
|
+
openedAt: 0,
|
|
3013
|
+
halfOpenAttempts: 0
|
|
3014
|
+
};
|
|
3015
|
+
this.circuitBreakers.set(resource, cb);
|
|
3016
|
+
}
|
|
3017
|
+
return cb;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
// src/contextAccelerator.ts
|
|
3022
|
+
var DEFAULT_CA_CONFIG = {
|
|
3023
|
+
maxCacheEntries: 500,
|
|
3024
|
+
defaultTtlMs: 3e5,
|
|
3025
|
+
// 5 minutes
|
|
3026
|
+
tokenBudget: 8e3,
|
|
3027
|
+
compressionTarget: 0.5,
|
|
3028
|
+
priorityWeights: {
|
|
3029
|
+
critical: 100,
|
|
3030
|
+
high: 75,
|
|
3031
|
+
medium: 50,
|
|
3032
|
+
low: 25
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
function estimateTokens(text) {
|
|
3036
|
+
return Math.ceil(text.length / 3.5);
|
|
3037
|
+
}
|
|
3038
|
+
function defaultCompressor(content, targetTokens) {
|
|
3039
|
+
const currentTokens = estimateTokens(content);
|
|
3040
|
+
if (currentTokens <= targetTokens) return content;
|
|
3041
|
+
const ratio = targetTokens / currentTokens;
|
|
3042
|
+
const targetLength = Math.floor(content.length * ratio);
|
|
3043
|
+
if (targetLength < 50) return content.substring(0, 50) + "...";
|
|
3044
|
+
return content.substring(0, targetLength) + "...[compressed]";
|
|
3045
|
+
}
|
|
3046
|
+
var ContextAccelerator = class {
|
|
3047
|
+
cache = /* @__PURE__ */ new Map();
|
|
3048
|
+
prefetchRules = /* @__PURE__ */ new Map();
|
|
3049
|
+
config;
|
|
3050
|
+
stats = {
|
|
3051
|
+
totalHits: 0,
|
|
3052
|
+
totalMisses: 0,
|
|
3053
|
+
totalEvictions: 0,
|
|
3054
|
+
totalCompressions: 0,
|
|
3055
|
+
totalAssemblies: 0
|
|
3056
|
+
};
|
|
3057
|
+
constructor(config) {
|
|
3058
|
+
this.config = { ...DEFAULT_CA_CONFIG, ...config };
|
|
3059
|
+
}
|
|
3060
|
+
/**
|
|
3061
|
+
* Put a fragment into cache
|
|
3062
|
+
*/
|
|
3063
|
+
put(fragment) {
|
|
3064
|
+
this.evictExpired();
|
|
3065
|
+
if (this.cache.size >= this.config.maxCacheEntries) {
|
|
3066
|
+
this.evictLRU();
|
|
3067
|
+
}
|
|
3068
|
+
this.cache.set(fragment.id, {
|
|
3069
|
+
fragment,
|
|
3070
|
+
accessCount: 0,
|
|
3071
|
+
lastAccessedAt: Date.now(),
|
|
3072
|
+
createdAt: Date.now(),
|
|
3073
|
+
expiresAt: Date.now() + (fragment.ttlMs || this.config.defaultTtlMs)
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Get a fragment from cache
|
|
3078
|
+
*/
|
|
3079
|
+
get(id) {
|
|
3080
|
+
const entry = this.cache.get(id);
|
|
3081
|
+
if (!entry) {
|
|
3082
|
+
this.stats.totalMisses++;
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
if (Date.now() > entry.expiresAt) {
|
|
3086
|
+
this.cache.delete(id);
|
|
3087
|
+
this.stats.totalMisses++;
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
entry.accessCount++;
|
|
3091
|
+
entry.lastAccessedAt = Date.now();
|
|
3092
|
+
this.stats.totalHits++;
|
|
3093
|
+
return entry.fragment;
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Assemble context from fragments within token budget
|
|
3097
|
+
*/
|
|
3098
|
+
async assemble(fragmentIds, additionalFragments, budgetOverride) {
|
|
3099
|
+
const startTime = Date.now();
|
|
3100
|
+
const budget = budgetOverride ?? this.config.tokenBudget;
|
|
3101
|
+
let cacheHits = 0;
|
|
3102
|
+
let cacheMisses = 0;
|
|
3103
|
+
const allFragments = [];
|
|
3104
|
+
for (const id of fragmentIds) {
|
|
3105
|
+
const fragment = this.get(id);
|
|
3106
|
+
if (fragment) {
|
|
3107
|
+
allFragments.push(fragment);
|
|
3108
|
+
cacheHits++;
|
|
3109
|
+
} else {
|
|
3110
|
+
cacheMisses++;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
if (additionalFragments) {
|
|
3114
|
+
allFragments.push(...additionalFragments);
|
|
3115
|
+
}
|
|
3116
|
+
const weights = this.config.priorityWeights;
|
|
3117
|
+
allFragments.sort((a, b) => {
|
|
3118
|
+
const wDiff = (weights[b.priority] ?? 0) - (weights[a.priority] ?? 0);
|
|
3119
|
+
if (wDiff !== 0) return wDiff;
|
|
3120
|
+
return b.timestamp - a.timestamp;
|
|
3121
|
+
});
|
|
3122
|
+
const selected = [];
|
|
3123
|
+
let totalTokens = 0;
|
|
3124
|
+
let droppedCount = 0;
|
|
3125
|
+
let compressedCount = 0;
|
|
3126
|
+
for (const fragment of allFragments) {
|
|
3127
|
+
if (totalTokens >= budget) {
|
|
3128
|
+
droppedCount++;
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
const remaining = budget - totalTokens;
|
|
3132
|
+
if (fragment.tokenCount <= remaining) {
|
|
3133
|
+
selected.push(fragment);
|
|
3134
|
+
totalTokens += fragment.tokenCount;
|
|
3135
|
+
} else if (fragment.priority === "critical" || fragment.priority === "high") {
|
|
3136
|
+
const compressor = this.config.compressor ?? defaultCompressor;
|
|
3137
|
+
const compressed = compressor(fragment.content, remaining);
|
|
3138
|
+
const compressedTokens = estimateTokens(compressed);
|
|
3139
|
+
selected.push({
|
|
3140
|
+
...fragment,
|
|
3141
|
+
content: compressed,
|
|
3142
|
+
tokenCount: compressedTokens,
|
|
3143
|
+
compressed: true
|
|
3144
|
+
});
|
|
3145
|
+
totalTokens += compressedTokens;
|
|
3146
|
+
compressedCount++;
|
|
3147
|
+
this.stats.totalCompressions++;
|
|
3148
|
+
} else {
|
|
3149
|
+
droppedCount++;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
this.stats.totalAssemblies++;
|
|
3153
|
+
const result = {
|
|
3154
|
+
fragments: selected,
|
|
3155
|
+
totalTokens,
|
|
3156
|
+
budgetUsed: budget > 0 ? totalTokens / budget : 0,
|
|
3157
|
+
droppedCount,
|
|
3158
|
+
compressedCount,
|
|
3159
|
+
cacheHits,
|
|
3160
|
+
cacheMisses,
|
|
3161
|
+
assemblyTimeMs: Date.now() - startTime
|
|
3162
|
+
};
|
|
3163
|
+
if (this.config.onAudit) {
|
|
3164
|
+
try {
|
|
3165
|
+
await this.config.onAudit({
|
|
3166
|
+
type: "context_assembled",
|
|
3167
|
+
data: {
|
|
3168
|
+
totalTokens,
|
|
3169
|
+
budgetUsed: result.budgetUsed,
|
|
3170
|
+
fragmentCount: selected.length,
|
|
3171
|
+
droppedCount,
|
|
3172
|
+
compressedCount,
|
|
3173
|
+
cacheHits,
|
|
3174
|
+
cacheMisses,
|
|
3175
|
+
assemblyTimeMs: result.assemblyTimeMs
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
} catch {
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return result;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Register a prefetch rule
|
|
3185
|
+
*/
|
|
3186
|
+
addPrefetchRule(rule) {
|
|
3187
|
+
this.prefetchRules.set(rule.id, rule);
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Execute prefetch based on task type
|
|
3191
|
+
*/
|
|
3192
|
+
async prefetch(taskType, fragmentLoader) {
|
|
3193
|
+
const matchingRules = Array.from(this.prefetchRules.values()).filter(
|
|
3194
|
+
(r) => r.enabled && taskType.includes(r.trigger)
|
|
3195
|
+
);
|
|
3196
|
+
if (matchingRules.length === 0) return 0;
|
|
3197
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
3198
|
+
for (const rule of matchingRules) {
|
|
3199
|
+
for (const id of rule.fragmentIds) {
|
|
3200
|
+
if (!this.cache.has(id)) {
|
|
3201
|
+
idsToFetch.add(id);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (idsToFetch.size === 0) return 0;
|
|
3206
|
+
const fragments = await fragmentLoader(Array.from(idsToFetch));
|
|
3207
|
+
for (const f of fragments) {
|
|
3208
|
+
this.put(f);
|
|
3209
|
+
}
|
|
3210
|
+
return fragments.length;
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Invalidate cache entries
|
|
3214
|
+
*/
|
|
3215
|
+
invalidate(ids) {
|
|
3216
|
+
let count = 0;
|
|
3217
|
+
for (const id of ids) {
|
|
3218
|
+
if (this.cache.delete(id)) count++;
|
|
3219
|
+
}
|
|
3220
|
+
return count;
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* Get cache stats
|
|
3224
|
+
*/
|
|
3225
|
+
getStats() {
|
|
3226
|
+
const total = this.stats.totalHits + this.stats.totalMisses;
|
|
3227
|
+
return {
|
|
3228
|
+
cacheSize: this.cache.size,
|
|
3229
|
+
maxSize: this.config.maxCacheEntries,
|
|
3230
|
+
hitRate: total > 0 ? this.stats.totalHits / total : 0,
|
|
3231
|
+
...this.stats,
|
|
3232
|
+
prefetchRuleCount: this.prefetchRules.size
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Clear all cache
|
|
3237
|
+
*/
|
|
3238
|
+
clear() {
|
|
3239
|
+
this.cache.clear();
|
|
3240
|
+
}
|
|
3241
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
3242
|
+
evictExpired() {
|
|
3243
|
+
const now = Date.now();
|
|
3244
|
+
for (const [id, entry] of this.cache) {
|
|
3245
|
+
if (now > entry.expiresAt) {
|
|
3246
|
+
this.cache.delete(id);
|
|
3247
|
+
this.stats.totalEvictions++;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
evictLRU() {
|
|
3252
|
+
let oldestId = null;
|
|
3253
|
+
let oldestAccess = Infinity;
|
|
3254
|
+
for (const [id, entry] of this.cache) {
|
|
3255
|
+
if (entry.lastAccessedAt < oldestAccess) {
|
|
3256
|
+
oldestAccess = entry.lastAccessedAt;
|
|
3257
|
+
oldestId = id;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
if (oldestId) {
|
|
3261
|
+
this.cache.delete(oldestId);
|
|
3262
|
+
this.stats.totalEvictions++;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
|
|
2057
3267
|
// src/index.ts
|
|
2058
3268
|
var DEFAULT_RULES = [
|
|
2059
3269
|
// --- HTTP Proxy: Dangerous outbound calls ---
|
|
@@ -2545,16 +3755,26 @@ var index_default = PolicyEngine;
|
|
|
2545
3755
|
0 && (module.exports = {
|
|
2546
3756
|
AdaptiveThresholdManager,
|
|
2547
3757
|
AutoHardenEngine,
|
|
3758
|
+
ContextAccelerator,
|
|
2548
3759
|
CostGateEnhancedEngine,
|
|
3760
|
+
DEFAULT_CA_CONFIG,
|
|
3761
|
+
DEFAULT_GE_CONFIG,
|
|
3762
|
+
DEFAULT_HARD_RULES,
|
|
2549
3763
|
DEFAULT_RULES,
|
|
3764
|
+
DEFAULT_SCORING_WEIGHTS,
|
|
3765
|
+
DEFAULT_TSA_CONFIG,
|
|
2550
3766
|
EvolutionChannelEngine,
|
|
2551
3767
|
FeatureSwitchesManager,
|
|
3768
|
+
GovernanceEnhancer,
|
|
2552
3769
|
MultiLevelBudgetEngine,
|
|
2553
3770
|
PolicyEngine,
|
|
2554
3771
|
PricingRulesEngine,
|
|
2555
3772
|
RecalculationEngine,
|
|
2556
3773
|
SOVR_FEATURE_SWITCHES,
|
|
2557
3774
|
SemanticDriftDetectorEngine,
|
|
3775
|
+
TimeSeriesAggregator,
|
|
3776
|
+
TwoPhaseRouter,
|
|
3777
|
+
ValuationModel,
|
|
2558
3778
|
compileFromJSON,
|
|
2559
3779
|
compileRuleSet,
|
|
2560
3780
|
createAutoHardenEngine,
|