use crate::ir_nodes::{IRFlow, IRFlowNode, IRToolSpec};
use crate::lambda_data::apply_provenance_ceiling;
use serde::{Deserialize, Serialize};
pub const EPISTEMIC_LEVELS: &[&str] = &["doubt", "speculate", "believe", "know"];
pub const KNOW_CEILING: f64 = 0.99;
pub fn level_ceiling(level: &str) -> Option<f64> {
match level {
"doubt" => Some(0.50),
"speculate" => Some(0.80),
"believe" => Some(0.95),
"know" => Some(KNOW_CEILING),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EpistemicEnvelope {
pub base: String,
pub scope: String,
pub confidence: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_type: Option<String>,
}
pub fn epistemic_level_of(effect_row: &[String]) -> Option<&str> {
effect_row
.iter()
.find_map(|e| e.strip_prefix("epistemic:"))
.filter(|level| !level.is_empty())
}
pub fn capture(
effect_row: &[String],
scope: &str,
input_confidence: f64,
output_type: Option<&str>,
) -> Option<EpistemicEnvelope> {
let level = epistemic_level_of(effect_row)?;
let ceiling = level_ceiling(level)?;
Some(EpistemicEnvelope {
base: level.to_string(),
scope: scope.to_string(),
confidence: apply_provenance_ceiling(input_confidence, ceiling),
output_type: output_type.map(|s| s.to_string()),
})
}
pub fn collect_for_flow(
flow: &IRFlow,
tools: &[IRToolSpec],
input_confidence: f64,
) -> Vec<EpistemicEnvelope> {
flow.steps
.iter()
.filter_map(|node| match node {
IRFlowNode::UseTool(u) => {
let tool = tools.iter().find(|t| t.name == u.tool_name)?;
capture(
&tool.effect_row,
&format!("tool:{}", u.tool_name),
input_confidence,
tool.output_type.as_deref(),
)
}
_ => None,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ceilings_are_monotone_non_decreasing_along_the_lattice() {
let mut prev = 0.0;
for level in EPISTEMIC_LEVELS {
let c = level_ceiling(level).expect("every catalog level has a ceiling");
assert!(
c >= prev,
"ceiling for {level} ({c}) must be ≥ the previous level's ({prev}) — \
the lattice is ordered doubt ⊑ speculate ⊑ believe ⊑ know"
);
prev = c;
}
}
#[test]
fn know_ceiling_is_the_theorem_5_1_derived_apex() {
assert_eq!(level_ceiling("know"), Some(0.99));
assert_eq!(KNOW_CEILING, 0.99, "must mirror the C23 kernel constant");
}
#[test]
fn unknown_level_has_no_ceiling() {
assert_eq!(level_ceiling("certain"), None);
assert_eq!(level_ceiling(""), None);
}
#[test]
fn extracts_the_epistemic_level_from_a_mixed_effect_row() {
let row = vec!["network".to_string(), "epistemic:speculate".to_string()];
assert_eq!(epistemic_level_of(&row), Some("speculate"));
}
#[test]
fn no_epistemic_entry_yields_none() {
let row = vec!["network".to_string(), "read".to_string()];
assert_eq!(epistemic_level_of(&row), None);
assert_eq!(capture(&row, "tool:Search", 0.9, None), None);
}
#[test]
fn high_confidence_input_is_degraded_to_the_level_ceiling() {
let row = vec!["epistemic:speculate".to_string()];
let env = capture(&row, "tool:WebSearch", 0.97, None).expect("epistemic envelope");
assert_eq!(env.base, "speculate");
assert_eq!(env.scope, "tool:WebSearch");
assert_eq!(env.confidence, 0.80);
assert_eq!(env.output_type, None);
}
#[test]
fn low_confidence_input_is_left_untouched_by_a_higher_ceiling() {
let row = vec!["epistemic:know".to_string()];
let env = capture(&row, "step:Resolve", 0.30, None).expect("epistemic envelope");
assert_eq!(env.base, "know");
assert_eq!(env.confidence, 0.30, "the ceiling is a max, never a floor");
}
#[test]
fn out_of_range_input_is_clamped_into_the_unit_interval_then_capped() {
let row = vec!["epistemic:doubt".to_string()];
assert_eq!(capture(&row, "tool:T", 1.5, None).unwrap().confidence, 0.50);
assert_eq!(capture(&row, "tool:T", -0.2, None).unwrap().confidence, 0.0);
}
fn tool(name: &str, effects: &[&str]) -> IRToolSpec {
IRToolSpec {
node_type: "tool",
source_line: 0,
source_column: 0,
name: name.into(),
provider: String::new(),
max_results: None,
filter_expr: String::new(),
timeout: String::new(),
runtime: String::new(),
sandbox: None,
input_schema: Vec::new(),
output_schema: String::new(),
parameters: Vec::new(),
output_type: None,
effect_row: effects.iter().map(|e| e.to_string()).collect(),
}
}
fn use_tool(tool_name: &str) -> IRFlowNode {
IRFlowNode::UseTool(crate::ir_nodes::IRUseToolStep {
node_type: "use_tool",
source_line: 0,
source_column: 0,
tool_name: tool_name.into(),
argument: "${query}".into(),
named_args: Vec::new(),
})
}
fn flow_with_steps(steps: Vec<IRFlowNode>) -> IRFlow {
IRFlow {
node_type: "flow",
source_line: 0,
source_column: 0,
name: "F".into(),
parameters: Vec::new(),
return_type_name: "Unit".into(),
return_type_generic: String::new(),
return_type_optional: false,
steps,
edges: Vec::new(),
execution_levels: Vec::new(),
}
}
#[test]
fn collects_one_envelope_per_epistemic_tool_dispatch() {
let tools = vec![
tool("WebSearch", &["network", "epistemic:speculate"]),
tool("ExactLookup", &["compute", "epistemic:know"]),
];
let flow = flow_with_steps(vec![use_tool("WebSearch"), use_tool("ExactLookup")]);
let envs = collect_for_flow(&flow, &tools, 1.0);
assert_eq!(envs.len(), 2);
assert_eq!(envs[0], EpistemicEnvelope {
base: "speculate".into(),
scope: "tool:WebSearch".into(),
confidence: 0.80,
output_type: None,
});
assert_eq!(envs[1], EpistemicEnvelope {
base: "know".into(),
scope: "tool:ExactLookup".into(),
confidence: 0.99,
output_type: None,
});
}
fn tool_with_output(name: &str, effects: &[&str], output_type: &str) -> IRToolSpec {
let mut t = tool(name, effects);
t.output_type = Some(output_type.to_string());
t
}
#[test]
fn output_type_rides_the_envelope_when_declared() {
let env = capture(
&["epistemic:believe".to_string()],
"tool:CrmRadar",
1.0,
Some("CrmReport"),
)
.expect("epistemic envelope");
assert_eq!(env.base, "believe");
assert_eq!(env.confidence, 0.95, "believe ceiling");
assert_eq!(
env.output_type.as_deref(),
Some("CrmReport"),
"the declared output type rides the envelope (D9)"
);
}
#[test]
fn collect_for_flow_attaches_output_type_from_the_tool() {
let tools = vec![tool_with_output(
"CrmRadar",
&["network", "epistemic:speculate"],
"CrmReport",
)];
let flow = flow_with_steps(vec![use_tool("CrmRadar")]);
let envs = collect_for_flow(&flow, &tools, 1.0);
assert_eq!(envs.len(), 1);
assert_eq!(
envs[0],
EpistemicEnvelope {
base: "speculate".into(),
scope: "tool:CrmRadar".into(),
confidence: 0.80,
output_type: Some("CrmReport".into()),
}
);
}
#[test]
fn output_type_is_elided_from_the_wire_when_absent() {
let env = capture(&["epistemic:doubt".to_string()], "tool:T", 0.4, None).unwrap();
let json = serde_json::to_value(&env).unwrap();
assert!(
json.get("output_type").is_none(),
"output_type must be elided (not null) when absent: {json}"
);
assert_eq!(json["base"], "doubt");
assert_eq!(json["confidence"], 0.4);
}
#[test]
fn output_type_is_present_on_the_wire_when_declared() {
let env = capture(
&["epistemic:know".to_string()],
"tool:Exact",
1.0,
Some("Answer"),
)
.unwrap();
let json = serde_json::to_value(&env).unwrap();
assert_eq!(json["output_type"], "Answer");
}
#[test]
fn envelope_round_trips_through_json_with_output_type() {
let env = capture(
&["epistemic:believe".to_string()],
"tool:CrmRadar",
1.0,
Some("CrmReport"),
)
.unwrap();
let json = serde_json::to_string(&env).unwrap();
let back: EpistemicEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(back, env);
}
#[test]
fn legacy_wire_without_output_type_deserializes_to_none() {
let legacy = r#"{"base":"speculate","scope":"tool:Old","confidence":0.8}"#;
let env: EpistemicEnvelope = serde_json::from_str(legacy).unwrap();
assert_eq!(env.output_type, None);
assert_eq!(env.base, "speculate");
}
#[test]
fn a_non_epistemic_tool_contributes_no_envelope() {
let tools = vec![tool("PlainHttp", &["network"])];
let flow = flow_with_steps(vec![use_tool("PlainHttp")]);
assert!(collect_for_flow(&flow, &tools, 1.0).is_empty());
}
#[test]
fn an_undeclared_tool_is_skipped_not_panicked() {
let tools: Vec<IRToolSpec> = Vec::new();
let flow = flow_with_steps(vec![use_tool("Ghost")]);
assert!(collect_for_flow(&flow, &tools, 1.0).is_empty());
}
}