use serde::{Deserialize, Serialize};
use super::EvidenceTier;
use super::retention::{RecoveryPath, RetentionReport, assess};
pub const RECEIPT_KIND: &str = "lean-ctx.context-quality-receipt";
pub const RECEIPT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransformationEvidence {
pub source_type: String,
pub transformation_mode: String,
pub input_tokens: usize,
pub delivered_tokens: usize,
pub saved_tokens: usize,
pub compression_ratio: f64,
pub engine_version: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryEvidence {
pub handles_emitted: usize,
pub handles_verified: usize,
pub failures: usize,
pub critical_failures: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DimensionState {
Pass,
Fail,
Unmeasured,
}
impl DimensionState {
pub fn label(self) -> &'static str {
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Unmeasured => "UNMEASURED",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextQualityReceiptV1 {
pub kind: String,
pub schema_version: u32,
pub evidence_tier: EvidenceTier,
pub transformation: TransformationEvidence,
pub retention: RetentionReport,
pub recovery: Option<RecoveryEvidence>,
pub task_quality: DimensionState,
}
impl ContextQualityReceiptV1 {
pub fn assess(
source_type: &str,
transformation_mode: &str,
original: &str,
delivered: &str,
recovery: RecoveryPath,
recovery_evidence: Option<RecoveryEvidence>,
) -> Self {
let input_tokens = crate::core::tokens::count_tokens(original);
let delivered_tokens = crate::core::tokens::count_tokens(delivered);
let compression_ratio = if input_tokens == 0 {
1.0
} else {
delivered_tokens as f64 / input_tokens as f64
};
Self {
kind: RECEIPT_KIND.to_string(),
schema_version: RECEIPT_SCHEMA_VERSION,
evidence_tier: EvidenceTier::DeterministicQuality,
transformation: TransformationEvidence {
source_type: source_type.to_string(),
transformation_mode: transformation_mode.to_string(),
input_tokens,
delivered_tokens,
saved_tokens: input_tokens.saturating_sub(delivered_tokens),
compression_ratio,
engine_version: env!("CARGO_PKG_VERSION").to_string(),
},
retention: assess(original, delivered, recovery),
recovery: recovery_evidence,
task_quality: DimensionState::Unmeasured,
}
}
pub fn retention_state(&self) -> DimensionState {
if !self.retention.passes() {
DimensionState::Fail
} else if self.retention.critical.total() + self.retention.important.total() == 0 {
DimensionState::Unmeasured
} else {
DimensionState::Pass
}
}
pub fn recovery_state(&self) -> DimensionState {
match self.recovery {
None => DimensionState::Unmeasured,
Some(r) if r.failures > 0 => DimensionState::Fail,
Some(_) => DimensionState::Pass,
}
}
pub fn passes(&self) -> bool {
self.retention_state() != DimensionState::Fail
&& self.recovery_state() != DimensionState::Fail
}
pub fn overall_state(&self) -> DimensionState {
let states = [self.retention_state(), self.recovery_state()];
if states.contains(&DimensionState::Fail) {
DimensionState::Fail
} else if states.iter().all(|s| *s == DimensionState::Unmeasured) {
DimensionState::Unmeasured
} else {
DimensionState::Pass
}
}
pub fn render(&self) -> String {
let t = &self.transformation;
let r = &self.retention;
let mut out = String::new();
out.push_str("Context Quality\n");
out.push_str(&format!(
"Representation {} via {}\n input {} tok → delivered {} tok ({:.1}% reduction)\n",
t.source_type,
t.transformation_mode,
t.input_tokens,
t.delivered_tokens,
(1.0 - t.compression_ratio) * 100.0
));
out.push_str(&format!(
"Retention {}\n critical retained {} · recoverable {} · lost {}\n important retained {} · recoverable {} · lost {}\n",
self.retention_state().label(),
r.critical.retained,
r.critical.recoverable,
r.critical.lost,
r.important.retained,
r.important.recoverable,
r.important.lost,
));
if !r.lost_critical_kinds.is_empty() {
let kinds: Vec<String> = r
.lost_critical_kinds
.iter()
.map(|k| format!("{k:?}"))
.collect();
out.push_str(&format!(" lost critical kinds: {}\n", kinds.join(", ")));
}
if r.critical_unchecked {
out.push_str(" too many critical facts to check — failing closed\n");
}
if r.truncated {
out.push_str(" (non-critical probe limit reached — remainder unchecked)\n");
}
match self.recovery {
Some(rec) => out.push_str(&format!(
"Recovery {}\n handles {} · verified {} · failures {}\n",
self.recovery_state().label(),
rec.handles_emitted,
rec.handles_verified,
rec.failures
)),
None => out.push_str("Recovery UNMEASURED\n"),
}
out.push_str(&format!(
"Security secret lines withheld from probing: {}\n",
r.secret_lines_withheld
));
out.push_str(&format!(
"Task quality {} (evidence tier {}; run `lean-ctx eval ab`)\n",
self.task_quality.label(),
self.evidence_tier.code()
));
out.push_str(&format!(
"Verdict Context quality {}\n",
self.overall_state().label()
));
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_reports_unmeasured_dimensions_instead_of_zero() {
let r = ContextQualityReceiptV1::assess(
"shell",
"terse",
"plain prose without facts\n",
"plain prose\n",
RecoveryPath::None,
None,
);
assert_eq!(r.retention_state(), DimensionState::Unmeasured);
assert_eq!(r.recovery_state(), DimensionState::Unmeasured);
let text = r.render();
assert!(text.contains("Retention UNMEASURED"));
assert!(text.contains("Recovery UNMEASURED"));
assert!(text.contains("Task quality UNMEASURED"));
assert_eq!(r.overall_state(), DimensionState::Unmeasured);
assert!(text.contains("Context quality UNMEASURED"));
}
#[test]
fn critical_loss_fails_the_receipt_and_critical_recovery_failure_too() {
let original = "error[E0599]: no method named `total`\n";
let lost = ContextQualityReceiptV1::assess(
"shell",
"aggressive",
original,
"compile issue\n",
RecoveryPath::None,
None,
);
assert!(!lost.passes());
assert!(lost.render().contains("Context quality FAIL"));
let broken_recovery = ContextQualityReceiptV1::assess(
"shell",
"aggressive",
original,
original,
RecoveryPath::None,
Some(RecoveryEvidence {
handles_emitted: 1,
handles_verified: 0,
failures: 1,
critical_failures: 0,
}),
);
assert!(!broken_recovery.passes());
}
#[test]
fn receipt_is_deterministic_and_carries_no_content() {
let original = "test result: FAILED. 3 passed; 2 failed\nsee src/lib.rs:10\n";
let a = ContextQualityReceiptV1::assess(
"shell",
"lite",
original,
original,
RecoveryPath::None,
None,
);
let b = ContextQualityReceiptV1::assess(
"shell",
"lite",
original,
original,
RecoveryPath::None,
None,
);
let ja = serde_json::to_string(&a).unwrap();
assert_eq!(ja, serde_json::to_string(&b).unwrap());
assert!(
!ja.contains("src/lib.rs"),
"receipt must not copy content: {ja}"
);
}
}