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
54#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
56pub struct TrajectoryProjection {
57 pub accepted: usize,
58 pub rejected: usize,
59 pub certificates: usize,
60 pub graph_revisions: usize,
61 pub energy_timeline: Vec<(String, u32, f64)>,
63}
64
65impl TrajectoryProjection {
66 pub fn from_ledger(ledger: &Ledger) -> Self {
68 let mut p = TrajectoryProjection::default();
69 for rec in ledger.records() {
70 match &rec.event {
71 LedgerEvent::CandidateAccepted {
72 node_id,
73 generation,
74 energy,
75 } => {
76 p.accepted += 1;
77 p.energy_timeline
78 .push((node_id.clone(), *generation, *energy));
79 }
80 LedgerEvent::CandidateRejected { .. } => p.rejected += 1,
81 LedgerEvent::ResidualCertificateIssued { .. } => p.certificates += 1,
82 LedgerEvent::GraphRevisionAccepted { .. } => p.graph_revisions += 1,
83 _ => {}
84 }
85 }
86 p
87 }
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
92pub struct ResidualHeatmap {
93 pub by_component: BTreeMap<EnergyComponent, usize>,
94 pub by_class: BTreeMap<ResidualClass, usize>,
95}
96
97pub fn residual_heatmap(residuals: &[ResidualEvent]) -> ResidualHeatmap {
99 let mut heatmap = ResidualHeatmap::default();
100 for r in residuals {
101 *heatmap.by_component.entry(r.component).or_default() += 1;
102 *heatmap.by_class.entry(r.class).or_default() += 1;
103 }
104 heatmap
105}
106
107#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
109pub struct CapabilityAudit {
110 pub grants: usize,
111 pub revocations: usize,
112 pub denials: usize,
113}
114
115impl CapabilityAudit {
116 pub fn from_ledger(ledger: &Ledger) -> Self {
117 let mut a = CapabilityAudit::default();
118 for rec in ledger.records() {
119 match &rec.event {
120 LedgerEvent::CapabilityGranted { .. } => a.grants += 1,
121 LedgerEvent::CapabilityRevoked { .. } => a.revocations += 1,
122 LedgerEvent::EffectDenied { .. } => a.denials += 1,
123 _ => {}
124 }
125 }
126 a
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::residual::{IndependenceRoute, ResidualSeverity, SensorRef};
134
135 #[test]
136 fn phi_matches_formula() {
137 assert_eq!(phi(10.0, 0.5, 3), 24.0);
139 }
140
141 #[test]
142 fn backlog_gauge_sums_potentials() {
143 let workflows = vec![
144 WorkflowPotential::new("w1", 10.0, 0.5, 3), WorkflowPotential::new("w2", 5.0, 0.5, 1), ];
147 assert_eq!(backlog_gauge(&workflows), 36.0);
148 }
149
150 #[test]
151 fn trajectory_projection_counts_ledger_events() {
152 let mut ledger = Ledger::new();
153 ledger
154 .append(LedgerEvent::CandidateAccepted {
155 node_id: "a".into(),
156 generation: 0,
157 energy: 5.0,
158 })
159 .unwrap();
160 ledger
161 .append(LedgerEvent::CandidateRejected {
162 node_id: "a".into(),
163 generation: 1,
164 })
165 .unwrap();
166 ledger
167 .append(LedgerEvent::CandidateAccepted {
168 node_id: "a".into(),
169 generation: 2,
170 energy: 0.0,
171 })
172 .unwrap();
173 ledger
174 .append(LedgerEvent::GraphRevisionAccepted {
175 revision_id: "r1".into(),
176 sequence: 1,
177 })
178 .unwrap();
179 let p = TrajectoryProjection::from_ledger(&ledger);
180 assert_eq!(p.accepted, 2);
181 assert_eq!(p.rejected, 1);
182 assert_eq!(p.graph_revisions, 1);
183 assert_eq!(p.energy_timeline.len(), 2);
184 }
185
186 #[test]
187 fn residual_heatmap_groups_by_component_and_class() {
188 let sensor = SensorRef::new("c", IndependenceRoute::Compiler);
189 let residuals = vec![
190 ResidualEvent::new(
191 "n",
192 0,
193 ResidualClass::Type,
194 ResidualSeverity::Error,
195 1.0,
196 sensor.clone(),
197 )
198 .unwrap(),
199 ResidualEvent::new(
200 "n",
201 0,
202 ResidualClass::Type,
203 ResidualSeverity::Error,
204 1.0,
205 sensor.clone(),
206 )
207 .unwrap(),
208 ResidualEvent::new(
209 "n",
210 0,
211 ResidualClass::TestFailure,
212 ResidualSeverity::Error,
213 1.0,
214 sensor,
215 )
216 .unwrap(),
217 ];
218 let heatmap = residual_heatmap(&residuals);
219 assert_eq!(heatmap.by_component[&EnergyComponent::Syn], 2);
220 assert_eq!(heatmap.by_component[&EnergyComponent::Log], 1);
221 assert_eq!(heatmap.by_class[&ResidualClass::Type], 2);
222 }
223
224 #[test]
225 fn capability_audit_counts_grants_and_denials() {
226 let mut ledger = Ledger::new();
227 ledger
228 .append(LedgerEvent::CapabilityGranted {
229 capability_id: "c1".into(),
230 holder: "a".into(),
231 })
232 .unwrap();
233 ledger
234 .append(LedgerEvent::EffectDenied {
235 proposal_id: "p1".into(),
236 reason: "scope".into(),
237 })
238 .unwrap();
239 ledger
240 .append(LedgerEvent::EffectDenied {
241 proposal_id: "p2".into(),
242 reason: "budget".into(),
243 })
244 .unwrap();
245 let audit = CapabilityAudit::from_ledger(&ledger);
246 assert_eq!(audit.grants, 1);
247 assert_eq!(audit.denials, 2);
248 }
249}