use serde::{Deserialize, Serialize};
use crate::identity::FrameId;
use crate::usage::UsageReport;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextUse {
pub frame: FrameId,
#[serde(default)]
pub selected: bool,
#[serde(default)]
pub rendered: bool,
#[serde(default)]
pub cited: bool,
}
impl ContextUse {
pub fn selected(frame: FrameId) -> Self {
Self {
frame,
selected: true,
rendered: false,
cited: false,
}
}
pub fn rendered(mut self) -> Self {
self.rendered = true;
self
}
pub fn cited(mut self) -> Self {
self.cited = true;
self
}
pub fn is_coherent(&self) -> bool {
(!self.cited || self.rendered) && (!self.rendered || self.selected)
}
pub fn is_rendered_but_uncited(&self) -> bool {
self.rendered && !self.cited
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttributionReport {
#[serde(default)]
pub uses: Vec<ContextUse>,
pub usage: UsageReport,
}
impl AttributionReport {
pub fn new(uses: Vec<ContextUse>, usage: UsageReport) -> Self {
Self { uses, usage }
}
pub fn use_of(&self, frame: &FrameId) -> Option<&ContextUse> {
self.uses.iter().find(|entry| &entry.frame == frame)
}
pub fn cited_tokens(&self) -> u64 {
self.tokens_where(|entry| entry.cited)
}
pub fn uncited_rendered_tokens(&self) -> u64 {
self.tokens_where(ContextUse::is_rendered_but_uncited)
}
pub fn cited_token_share(&self) -> Option<f64> {
let rendered = self.tokens_where(|entry| entry.rendered);
if rendered == 0 {
return None;
}
Some(self.cited_tokens() as f64 / rendered as f64)
}
pub fn is_reconcilable(&self) -> bool {
self.uses.iter().all(|entry| {
entry.is_coherent()
&& self.usage.providers.iter().any(|provider| {
provider
.served_frames
.iter()
.any(|s| s.frame == entry.frame)
})
})
}
fn tokens_where(&self, predicate: impl Fn(&ContextUse) -> bool) -> u64 {
self.uses
.iter()
.filter(|entry| predicate(entry))
.filter_map(|entry| {
self.usage
.providers
.iter()
.flat_map(|provider| provider.served_frames.iter())
.find(|served| served.frame == entry.frame)
.map(|served| served.token_cost as u64)
})
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::usage::{ProviderUsage, ServedFrame};
fn frame(id: &str) -> FrameId {
FrameId::new("docs", id, Some(format!("sha256:{id}")))
}
fn report(costs: &[(&str, u32)]) -> UsageReport {
let served: Vec<ServedFrame> = costs
.iter()
.map(|(id, cost)| ServedFrame {
frame: frame(id),
token_cost: *cost,
})
.collect();
let total: u64 = served.iter().map(|s| s.token_cost as u64).sum();
UsageReport {
budget_requested: 4096,
budget_consumed: total,
as_of: "2026-07-25T00:00:00Z".into(),
providers: vec![ProviderUsage {
provider_id: "docs".into(),
frames_served: served.len() as u32,
frames_rejected: 0,
token_cost: total,
served_frames: served,
}],
}
}
#[test]
fn cost_and_outcome_together_give_a_value_signal() {
let usage = report(&[("a", 100), ("b", 300)]);
let attribution = AttributionReport::new(
vec![
ContextUse::selected(frame("a")).rendered().cited(),
ContextUse::selected(frame("b")).rendered(),
],
usage,
);
assert_eq!(attribution.cited_tokens(), 100);
assert_eq!(attribution.uncited_rendered_tokens(), 300);
assert_eq!(attribution.cited_token_share(), Some(0.25));
}
#[test]
fn a_request_that_rendered_nothing_has_no_quality_to_report() {
let attribution = AttributionReport::new(
vec![ContextUse::selected(frame("a"))],
report(&[("a", 100)]),
);
assert_eq!(attribution.cited_token_share(), None);
}
#[test]
fn selected_but_unrendered_costs_nothing_against_quality() {
let attribution = AttributionReport::new(
vec![
ContextUse::selected(frame("a")).rendered().cited(),
ContextUse::selected(frame("b")),
],
report(&[("a", 100), ("b", 300)]),
);
assert_eq!(attribution.cited_token_share(), Some(1.0));
assert_eq!(attribution.uncited_rendered_tokens(), 0);
}
#[test]
fn incoherent_records_are_detectable() {
let cited_without_rendering = ContextUse {
frame: frame("a"),
selected: true,
rendered: false,
cited: true,
};
assert!(!cited_without_rendering.is_coherent());
let rendered_without_selecting = ContextUse {
frame: frame("a"),
selected: false,
rendered: true,
cited: false,
};
assert!(!rendered_without_selecting.is_coherent());
assert!(
ContextUse::selected(frame("a"))
.rendered()
.cited()
.is_coherent()
);
}
#[test]
fn a_record_naming_an_unbilled_frame_does_not_reconcile() {
let attribution = AttributionReport::new(
vec![ContextUse::selected(frame("ghost")).rendered()],
report(&[("a", 100)]),
);
assert!(!attribution.is_reconcilable());
let honest = AttributionReport::new(
vec![ContextUse::selected(frame("a")).rendered()],
report(&[("a", 100)]),
);
assert!(honest.is_reconcilable());
}
}