1use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::ledger::{Ledger, LedgerEvent};
15use crate::residual::{EnergyComponent, ResidualClass, ResidualEvent};
16
17pub fn phi(accepted_energy: f64, rho_gate: f64, remaining_budget: u32) -> f64 {
19 1.0 + accepted_energy / rho_gate + remaining_budget as f64
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct WorkflowPotential {
25 pub workflow_id: String,
26 pub accepted_energy: f64,
27 pub rho_gate: f64,
28 pub remaining_budget: u32,
29 pub potential: f64,
30}
31
32impl WorkflowPotential {
33 pub fn new(
34 workflow_id: impl Into<String>,
35 accepted_energy: f64,
36 rho_gate: f64,
37 remaining_budget: u32,
38 ) -> Self {
39 Self {
40 workflow_id: workflow_id.into(),
41 accepted_energy,
42 rho_gate,
43 remaining_budget,
44 potential: phi(accepted_energy, rho_gate, remaining_budget),
45 }
46 }
47}
48
49pub fn backlog_gauge(workflows: &[WorkflowPotential]) -> f64 {
51 workflows.iter().map(|w| w.potential).sum()
52}
53
54pub fn arriving_potential_per_step(arrivals_per_step: &[Vec<WorkflowPotential>]) -> f64 {
60 if arrivals_per_step.is_empty() {
61 return 0.0;
62 }
63 let total: f64 = arrivals_per_step
64 .iter()
65 .map(|arrivals| backlog_gauge(arrivals))
66 .sum();
67 total / arrivals_per_step.len() as f64
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
72pub struct TrajectoryProjection {
73 pub accepted: usize,
74 pub rejected: usize,
75 pub certificates: usize,
76 pub graph_revisions: usize,
77 pub energy_timeline: Vec<(String, u32, f64)>,
79}
80
81impl TrajectoryProjection {
82 pub fn from_ledger(ledger: &Ledger) -> Self {
84 let mut p = TrajectoryProjection::default();
85 for rec in ledger.records() {
86 match &rec.event {
87 LedgerEvent::CandidateAccepted {
88 node_id,
89 generation,
90 energy,
91 } => {
92 p.accepted += 1;
93 p.energy_timeline
94 .push((node_id.clone(), *generation, *energy));
95 }
96 LedgerEvent::CandidateRejected { .. } => p.rejected += 1,
97 LedgerEvent::ResidualCertificateIssued { .. } => p.certificates += 1,
98 LedgerEvent::GraphRevisionAccepted { .. } => p.graph_revisions += 1,
99 _ => {}
100 }
101 }
102 p
103 }
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
108pub struct ResidualHeatmap {
109 pub by_component: BTreeMap<EnergyComponent, usize>,
110 pub by_class: BTreeMap<ResidualClass, usize>,
111}
112
113pub fn residual_heatmap(residuals: &[ResidualEvent]) -> ResidualHeatmap {
115 let mut heatmap = ResidualHeatmap::default();
116 for r in residuals {
117 *heatmap.by_component.entry(r.component).or_default() += 1;
118 *heatmap.by_class.entry(r.class).or_default() += 1;
119 }
120 heatmap
121}
122
123#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
125pub struct CapabilityAudit {
126 pub grants: usize,
127 pub revocations: usize,
128 pub denials: usize,
129}
130
131impl CapabilityAudit {
132 pub fn from_ledger(ledger: &Ledger) -> Self {
133 let mut a = CapabilityAudit::default();
134 for rec in ledger.records() {
135 match &rec.event {
136 LedgerEvent::CapabilityGranted { .. } => a.grants += 1,
137 LedgerEvent::CapabilityRevoked { .. } => a.revocations += 1,
138 LedgerEvent::EffectDenied { .. } => a.denials += 1,
139 _ => {}
140 }
141 }
142 a
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::residual::{IndependenceRoute, ResidualSeverity, SensorRef};
150
151 #[test]
152 fn phi_matches_formula() {
153 assert_eq!(phi(10.0, 0.5, 3), 24.0);
155 }
156
157 #[test]
158 fn backlog_gauge_sums_potentials() {
159 let workflows = vec![
160 WorkflowPotential::new("w1", 10.0, 0.5, 3), WorkflowPotential::new("w2", 5.0, 0.5, 1), ];
163 assert_eq!(backlog_gauge(&workflows), 36.0);
164 }
165
166 #[test]
167 fn trajectory_projection_counts_ledger_events() {
168 let mut ledger = Ledger::new();
169 ledger
170 .append(LedgerEvent::CandidateAccepted {
171 node_id: "a".into(),
172 generation: 0,
173 energy: 5.0,
174 })
175 .unwrap();
176 ledger
177 .append(LedgerEvent::CandidateRejected {
178 node_id: "a".into(),
179 generation: 1,
180 })
181 .unwrap();
182 ledger
183 .append(LedgerEvent::CandidateAccepted {
184 node_id: "a".into(),
185 generation: 2,
186 energy: 0.0,
187 })
188 .unwrap();
189 ledger
190 .append(LedgerEvent::GraphRevisionAccepted {
191 revision_id: "r1".into(),
192 sequence: 1,
193 })
194 .unwrap();
195 let p = TrajectoryProjection::from_ledger(&ledger);
196 assert_eq!(p.accepted, 2);
197 assert_eq!(p.rejected, 1);
198 assert_eq!(p.graph_revisions, 1);
199 assert_eq!(p.energy_timeline.len(), 2);
200 }
201
202 #[test]
203 fn residual_heatmap_groups_by_component_and_class() {
204 let sensor = SensorRef::new("c", IndependenceRoute::Compiler);
205 let residuals = vec![
206 ResidualEvent::new(
207 "n",
208 0,
209 ResidualClass::Type,
210 ResidualSeverity::Error,
211 1.0,
212 sensor.clone(),
213 )
214 .unwrap(),
215 ResidualEvent::new(
216 "n",
217 0,
218 ResidualClass::Type,
219 ResidualSeverity::Error,
220 1.0,
221 sensor.clone(),
222 )
223 .unwrap(),
224 ResidualEvent::new(
225 "n",
226 0,
227 ResidualClass::TestFailure,
228 ResidualSeverity::Error,
229 1.0,
230 sensor,
231 )
232 .unwrap(),
233 ];
234 let heatmap = residual_heatmap(&residuals);
235 assert_eq!(heatmap.by_component[&EnergyComponent::Syn], 2);
236 assert_eq!(heatmap.by_component[&EnergyComponent::Log], 1);
237 assert_eq!(heatmap.by_class[&ResidualClass::Type], 2);
238 }
239
240 #[test]
241 fn capability_audit_counts_grants_and_denials() {
242 let mut ledger = Ledger::new();
243 ledger
244 .append(LedgerEvent::CapabilityGranted {
245 capability_id: "c1".into(),
246 holder: "a".into(),
247 })
248 .unwrap();
249 ledger
250 .append(LedgerEvent::EffectDenied {
251 proposal_id: "p1".into(),
252 reason: "scope".into(),
253 })
254 .unwrap();
255 ledger
256 .append(LedgerEvent::EffectDenied {
257 proposal_id: "p2".into(),
258 reason: "budget".into(),
259 })
260 .unwrap();
261 let audit = CapabilityAudit::from_ledger(&ledger);
262 assert_eq!(audit.grants, 1);
263 assert_eq!(audit.denials, 2);
264 }
265
266 #[test]
267 fn alpha_is_arriving_potential_not_arrival_count() {
268 let light = vec![
272 WorkflowPotential::new("a", 0.0, 0.5, 0),
273 WorkflowPotential::new("b", 0.0, 0.5, 0),
274 WorkflowPotential::new("c", 0.0, 0.5, 0),
275 ];
276 let heavy = vec![WorkflowPotential::new("d", 50.0, 0.5, 3)];
277 assert!(backlog_gauge(&heavy) > backlog_gauge(&light));
278 let alpha = arriving_potential_per_step(&[light, heavy]);
279 assert!((alpha - (3.0 + 104.0) / 2.0).abs() < 1e-9, "{alpha}");
280 }
281}