areev_loop/analyzers/
budget_pressure.rs1use crate::analyzer::{AnalyzeCtx, Analyzer};
15use crate::error::Result;
16use crate::manifest::*;
17use crate::model::{ActionKind, Severity};
18use crate::recommendation::{Proposal, RecDraft, Summary};
19use serde_json::{json, Map};
20
21pub struct BudgetPressure {
22 manifest: AnalyzerManifest,
23}
24
25impl BudgetPressure {
26 pub fn new() -> Self {
27 BudgetPressure {
28 manifest: AnalyzerManifest {
29 id: "loop.budget_pressure/1".into(),
30 title: "Budget pressure".into(),
31 description: "Flags context assembly that keeps overflowing its token budget."
32 .into(),
33 tier: Tier::T0,
34 cadence: CadenceClass::Slow,
35 requires: vec![Capability::Telemetry],
36 target_classes: vec![TargetClass::Memory],
37 auto_apply: AutoApplyClass::Never, trust_class: TrustClass::Builtin,
39 params: vec![
40 ParamSpec::Int {
41 name: "min_samples".into(),
42 default: 20,
43 min: 1,
44 max: 10_000_000,
45 description: "Minimum assembly samples before overflow rate is meaningful."
46 .into(),
47 },
48 ParamSpec::Float {
49 name: "min_overflow_ratio".into(),
50 default: 0.5,
51 min: 0.0,
52 max: 1.0,
53 description: "Overflow fraction at or above which pressure is flagged."
54 .into(),
55 },
56 ],
57 default_on: true,
58 },
59 }
60 }
61}
62
63impl Default for BudgetPressure {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl Analyzer for BudgetPressure {
70 fn analyze(&self, ctx: &AnalyzeCtx) -> Result<Vec<RecDraft>> {
71 let Some(tel) = ctx.telemetry()? else {
72 return Ok(Vec::new());
73 };
74 let min_samples = ctx.params().get_int("min_samples");
75 let min_overflow_ratio = ctx.params().get_float("min_overflow_ratio");
76
77 let b = &tel.budget;
78 if b.sample_count < min_samples || b.sample_count <= 0 {
79 return Ok(Vec::new());
80 }
81 let ratio = b.overflow_count as f64 / b.sample_count as f64;
82 if ratio < min_overflow_ratio {
83 return Ok(Vec::new());
84 }
85 let overflow_rate = (ratio * 100.0).round() as i64;
86
87 let mut args = Map::new();
88 args.insert("overflow_rate".into(), json!(overflow_rate));
89 args.insert("samples".into(), json!(b.sample_count));
90
91 let mut data = Map::new();
92 data.insert("sample_count".into(), json!(b.sample_count));
93 data.insert("overflow_count".into(), json!(b.overflow_count));
94
95 Ok(vec![RecDraft::new(
96 "entity:budget/assembly".to_string(),
97 ActionKind::Flag,
98 Summary::new("budget.pressure", args),
99 Proposal::Data { data },
100 )
101 .severity(Severity::Medium)])
102 }
103
104 fn manifest(&self) -> &AnalyzerManifest {
105 &self.manifest
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::testkit::TestSubstrate;
113
114 #[test]
115 fn flags_sustained_overflow() {
116 let mut sub = TestSubstrate::new();
117 sub.telemetry_budget(40, 30); let drafts = sub.analyze(&BudgetPressure::new(), 10_000);
119 assert_eq!(drafts.len(), 1);
120 assert_eq!(drafts[0].action_kind, ActionKind::Flag);
121 assert!(drafts[0].summary.render().contains('%'));
122 }
123
124 #[test]
125 fn low_overflow_is_fine() {
126 let mut sub = TestSubstrate::new();
127 sub.telemetry_budget(40, 5); assert!(sub.analyze(&BudgetPressure::new(), 10_000).is_empty());
129 }
130
131 #[test]
132 fn too_few_samples_no_finding() {
133 let mut sub = TestSubstrate::new();
134 sub.telemetry_budget(5, 5); assert!(sub.analyze(&BudgetPressure::new(), 10_000).is_empty());
136 }
137}