use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::store::{Store, StoreError};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionBudget {
pub session_id: String,
pub spent: u64,
pub op_count: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cap: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remaining: Option<i64>,
}
impl Store {
pub fn session_budget(&self, session_id: &str) -> Result<SessionBudget, StoreError> {
let all = self.all_session_budgets()?;
if let Some(b) = all.into_iter().find(|b| b.session_id == session_id) {
return Ok(b);
}
let cap = self.session_budget_cap(session_id)?;
let remaining = cap.map(|c| c as i64);
Ok(SessionBudget {
session_id: session_id.into(),
spent: 0,
op_count: 0,
cap,
remaining,
})
}
pub fn session_budget_cap(&self, session_id: &str) -> Result<Option<u64>, StoreError> {
let policy = crate::policy::load(self.root())?.unwrap_or_default();
Ok(policy.session_budgets.cap_for(session_id))
}
pub fn all_session_budgets(&self) -> Result<Vec<SessionBudget>, StoreError> {
let log = lex_vcs::OpLog::open(self.root())?;
let intent_log = lex_vcs::IntentLog::open(self.root())?;
let mut visited: std::collections::BTreeSet<lex_vcs::OpId> = Default::default();
let mut records: Vec<lex_vcs::OperationRecord> = Vec::new();
for branch_name in self.list_branches()? {
let Some(branch) = self.get_branch(&branch_name)? else { continue };
let Some(head) = branch.head_op else { continue };
for rec in log.walk_back(&head, None)? {
if visited.insert(rec.op_id.clone()) {
records.push(rec);
}
}
}
let mut intent_to_session: BTreeMap<String, Option<String>> = BTreeMap::new();
let mut buckets: BTreeMap<String, (u64, usize)> = BTreeMap::new();
for rec in &records {
let Some(intent_id) = rec.op.intent_id.as_deref() else { continue };
let session = match intent_to_session.get(intent_id) {
Some(s) => s.clone(),
None => {
let s = intent_log.get(&intent_id.to_string())?
.map(|i| i.session_id);
intent_to_session.insert(intent_id.into(), s.clone());
s
}
};
let Some(session_id) = session else { continue };
let increment = monotonic_spend(&rec.op.kind);
if increment == 0 { continue; }
let entry = buckets.entry(session_id).or_insert((0, 0));
entry.0 += increment;
entry.1 += 1;
}
let policy = crate::policy::load(self.root())?.unwrap_or_default();
let out: Vec<SessionBudget> = buckets
.into_iter()
.map(|(session_id, (spent, op_count))| {
let cap = policy.session_budgets.cap_for(&session_id);
let remaining = cap.map(|c| (c as i64) - (spent as i64));
SessionBudget { session_id, spent, op_count, cap, remaining }
})
.collect();
Ok(out)
}
}
fn monotonic_spend(kind: &lex_vcs::OperationKind) -> u64 {
monotonic_spend_of(kind)
}
pub(crate) fn monotonic_spend_of(kind: &lex_vcs::OperationKind) -> u64 {
let (from, to) = kind.budget_delta();
match (from, to) {
(None, Some(n)) => n,
(Some(f), Some(t)) if t > f => t - f,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn monotonic_spend_handles_each_shape() {
use lex_vcs::OperationKind;
let k = OperationKind::AddFunction {
sig_id: "f".into(),
stage_id: "s".into(),
effects: Default::default(),
budget_cost: Some(10),
in_file: None,
};
assert_eq!(monotonic_spend(&k), 10);
let k = OperationKind::ModifyBody {
sig_id: "f".into(),
from_stage_id: "a".into(),
to_stage_id: "b".into(),
from_budget: Some(10),
to_budget: Some(15),
};
assert_eq!(monotonic_spend(&k), 5);
let k = OperationKind::ModifyBody {
sig_id: "f".into(),
from_stage_id: "a".into(),
to_stage_id: "b".into(),
from_budget: Some(15),
to_budget: Some(10),
};
assert_eq!(monotonic_spend(&k), 0, "decrease doesn't refund");
let k = OperationKind::ModifyBody {
sig_id: "f".into(),
from_stage_id: "a".into(),
to_stage_id: "b".into(),
from_budget: None,
to_budget: None,
};
assert_eq!(monotonic_spend(&k), 0);
}
}