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