use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use serde_json::json;
use crate::graph_index::GraphIndex;
use crate::retrieval::{GroundIndex, Hit, RelationMatch, ground_subgraph, ground_with};
use crate::schema::{Edge, Graph, Kind};
use crate::workflow::{
TaskContextItem, TaskEvent, TaskOperator, TaskState, apply_task_event, draft_task_dag,
initial_run, ready_tasks, task_context_slice, validate_dag,
};
use super::{
ContextEdgeExpectation, GoldenCase, RelationMatchExpectation, kind_label,
missing_relation_matches, receipt_coverage,
};
#[derive(Serialize, Clone, Debug, Default)]
pub struct DogfoodDifficultyStat {
pub total: usize,
pub passed: usize,
pub mean_score: f64,
}
#[derive(Serialize, Clone, Debug)]
pub struct DogfoodCaseResult {
pub query: String,
pub difficulty: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_ok: Option<bool>,
pub route_top: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_partition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route_partition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partition_ok: Option<bool>,
pub route_rank: Option<usize>,
pub route_ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_focus_route: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub expected_focus_routes: Vec<String>,
pub focus_route: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_route_ok: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub route_relation_matches: Vec<RelationMatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route_relation_ok: Option<bool>,
pub route_relation_expected: usize,
pub route_relation_missing: usize,
pub context_recall: Option<f64>,
pub context_noise: Option<f64>,
pub edge_recall: Option<f64>,
pub receipt_coverage: f64,
pub requires_code_ok: bool,
pub requires_docs_ok: bool,
#[serde(rename = "brief_valid")]
pub workflow_valid: bool,
#[serde(rename = "brief_bounded")]
pub workflow_bounded: bool,
pub expected_operators_ok: bool,
pub task_context_ok: bool,
pub run_advancement_ok: bool,
pub score: f64,
pub passed: bool,
pub pack_size: usize,
pub node_count: usize,
pub missing_context: Vec<String>,
pub leaked_context: Vec<String>,
pub missing_edges: Vec<ContextEdgeExpectation>,
pub missing_route_relation_matches: Vec<RelationMatchExpectation>,
pub missing_operators: Vec<String>,
pub missing_task_context: Vec<String>,
pub ready_after_context: Vec<String>,
pub protocol: DogfoodProtocolComparison,
}
#[derive(Serialize, Clone, Debug, Default)]
pub struct DogfoodProtocolRun {
pub request_tokens: usize,
pub response_tokens: usize,
pub total_tokens: usize,
pub route_ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence_band: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_verdict: Option<String>,
pub context_recall: Option<f64>,
pub receipt_coverage: f64,
}
#[derive(Serialize, Clone, Debug, Default)]
pub struct DogfoodProtocolComparison {
pub legacy: DogfoodProtocolRun,
pub ekf_compact: DogfoodProtocolRun,
pub request_token_overhead: isize,
pub response_token_delta: isize,
pub total_token_delta: isize,
pub response_token_savings_rate: f64,
pub total_token_savings_rate: f64,
pub quality_preserved: bool,
}
#[derive(Serialize, Clone, Debug)]
pub struct DogfoodReport {
pub dogfood_cases: usize,
pub pass_rate: f64,
pub mean_score: f64,
pub route_hit_rate: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_route_match_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partition_match_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relation_evidence_recall: Option<f64>,
pub relation_evidence_expected: usize,
pub relation_evidence_missing: usize,
pub context_recall: f64,
pub context_noise: f64,
pub edge_recall: f64,
pub receipt_coverage: f64,
pub code_context_rate: f64,
pub docs_context_rate: f64,
#[serde(rename = "brief_valid_rate")]
pub workflow_valid_rate: f64,
#[serde(rename = "brief_bounded_rate")]
pub workflow_bounded_rate: f64,
pub operator_coverage_rate: f64,
pub task_context_rate: f64,
pub run_advancement_rate: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_match_rate: Option<f64>,
pub protocol: DogfoodProtocolSummary,
pub by_difficulty: BTreeMap<String, DogfoodDifficultyStat>,
pub cases: Vec<DogfoodCaseResult>,
}
#[derive(Serialize, Clone, Debug, Default)]
pub struct DogfoodProtocolSummary {
pub legacy_request_tokens: usize,
pub legacy_response_tokens: usize,
pub legacy_total_tokens: usize,
pub ekf_request_tokens: usize,
pub ekf_response_tokens: usize,
pub ekf_total_tokens: usize,
pub request_token_overhead: isize,
pub response_token_delta: isize,
pub total_token_delta: isize,
pub response_token_savings_rate: f64,
pub total_token_savings_rate: f64,
pub ekf_quality_preservation_rate: f64,
pub ekf_route_hit_rate: f64,
pub ekf_context_recall: f64,
pub ekf_confidence_coverage_rate: f64,
pub ekf_trust_tier_coverage_rate: f64,
pub confidence_bands: BTreeMap<String, usize>,
pub trust_tiers: BTreeMap<String, usize>,
pub trust_verdicts: BTreeMap<String, usize>,
}
pub struct DogfoodProtocolInput<'a> {
pub case: &'a GoldenCase,
pub hits: &'a [Hit],
pub route_ok: bool,
pub context_order: &'a [String],
pub edges: &'a [Edge],
pub context_judgment: &'a DogfoodContextJudgment,
pub receipt_coverage: f64,
pub route_trust_tier: Option<String>,
pub route_trust_verdict: Option<String>,
pub limit: usize,
pub depth: usize,
pub width: usize,
}
#[derive(Clone, Debug)]
pub struct DogfoodContextJudgment {
pub recall: Option<f64>,
pub noise: Option<f64>,
pub edge_recall: Option<f64>,
pub missing: Vec<String>,
pub leaked: Vec<String>,
pub missing_edges: Vec<ContextEdgeExpectation>,
}
pub fn evaluate_dogfood(
graph: &Graph,
cases: &[GoldenCase],
limit: usize,
depth: usize,
width: usize,
max_nodes: usize,
min_case_score: f64,
) -> DogfoodReport {
let ground_index = GroundIndex::build(graph);
let graph_index = GraphIndex::build(graph);
let mut results = Vec::new();
for c in cases.iter().filter(|case| !case.garbage) {
let hits = ground_with(graph, &ground_index, &c.query, limit);
let route_top = hits.first().map(|hit| hit.id.clone());
let route_partition = route_top
.as_deref()
.and_then(|id| graph_index.node(id))
.and_then(|node| node.partition.clone());
let partition_ok = c
.expected_partition
.as_ref()
.map(|expected| route_partition.as_deref() == Some(expected.as_str()));
let route_rank = hits
.iter()
.position(|hit| c.expect.iter().any(|expected| expected == &hit.id))
.map(|idx| idx + 1);
let route_ok =
(c.expect.is_empty() || route_rank.is_some()) && partition_ok.unwrap_or(true);
let route_relation_matches = hits
.first()
.map(|hit| hit.relation_matches.clone())
.unwrap_or_default();
let missing_route_relation_matches =
missing_relation_matches(&c.route_relation_must, &route_relation_matches);
let route_relation_ok = (!c.route_relation_must.is_empty())
.then_some(missing_route_relation_matches.is_empty());
let route_relation_expected = c.route_relation_must.len();
let route_relation_missing = missing_route_relation_matches.len();
let sg = ground_subgraph(graph, &c.query, limit, depth, width);
let focus_route_ok =
acceptable_focus_routes(c).map(|expected| expected.contains(&sg.route));
let context_ids = sg
.context_order
.iter()
.map(String::as_str)
.collect::<BTreeSet<_>>();
let context_judgment = judge_dogfood_context(c, &context_ids, &sg.edges);
let pack_size = sg.context_order.len();
let receipt_coverage = receipt_coverage(&graph_index, &sg.context_order);
let protocol = compare_protocol_economics(DogfoodProtocolInput {
case: c,
hits: &hits,
route_ok,
context_order: &sg.context_order,
edges: &sg.edges,
context_judgment: &context_judgment,
receipt_coverage,
route_trust_tier: None,
route_trust_verdict: None,
limit,
depth,
width,
});
let has_code_context = sg.context_order.iter().any(|id| {
graph_index
.node(id)
.is_some_and(|node| is_dogfood_code_kind(node.kind))
});
let has_docs_context = sg.context_order.iter().any(|id| {
graph_index
.node(id)
.is_some_and(|node| is_dogfood_docs_kind(node.kind))
});
let requires_code_ok = !c.requires_code || has_code_context;
let requires_docs_ok = !c.requires_docs || has_docs_context;
let context = sg
.context_order
.iter()
.filter_map(|id| {
let node = graph_index.node(id)?;
Some(TaskContextItem {
id: node.id.clone(),
title: node.title.clone(),
kind: kind_label(node.kind),
source: graph_index.source_of(&node.id).unwrap_or_default(),
})
})
.collect::<Vec<_>>();
let dag = draft_task_dag(&c.query, &context);
let workflow_valid = validate_dag(&dag).is_ok();
let workflow_bounded = dag.nodes.len() <= max_nodes;
let operators = dag
.nodes
.iter()
.map(|node| dogfood_operator_label(node.operator))
.collect::<BTreeSet<_>>();
let missing_operators = c
.expected_operators
.iter()
.filter(|operator| !operators.contains(operator.as_str()))
.cloned()
.collect::<Vec<_>>();
let expected_operators_ok = missing_operators.is_empty();
let (run_advancement_ok, ready_after_context) = advance_dogfood_context_step(&dag);
let missing_task_context = ready_after_context
.iter()
.filter(|task_id| !dogfood_task_context_is_useful(&dag, task_id, &context))
.cloned()
.collect::<Vec<_>>();
let task_context_ok = missing_task_context.is_empty();
let mut checks = Vec::new();
checks.push(route_ok);
if let Some(ok) = focus_route_ok {
checks.push(ok);
}
if let Some(recall) = context_judgment.recall {
checks.push(recall >= 1.0);
}
if let Some(noise) = context_judgment.noise {
checks.push(noise == 0.0);
}
if let Some(edge_recall) = context_judgment.edge_recall {
checks.push(edge_recall >= 1.0);
}
if let Some(ok) = route_relation_ok {
checks.push(ok);
}
checks.push(receipt_coverage >= 1.0);
checks.push(requires_code_ok);
checks.push(requires_docs_ok);
checks.push(workflow_valid);
checks.push(workflow_bounded);
checks.push(expected_operators_ok);
checks.push(task_context_ok);
checks.push(run_advancement_ok);
let score = score_dogfood_checks(&checks);
results.push(DogfoodCaseResult {
query: c.query.clone(),
difficulty: dogfood_case_difficulty(c),
expected_profile: None,
profile: None,
profile_ok: None,
route_top,
expected_partition: c.expected_partition.clone(),
route_partition,
partition_ok,
route_rank,
route_ok,
expected_focus_route: c.expected_focus_route.clone(),
expected_focus_routes: c.expected_focus_routes.clone(),
focus_route: sg.route,
focus_route_ok,
route_relation_matches,
route_relation_ok,
route_relation_expected,
route_relation_missing,
context_recall: context_judgment.recall,
context_noise: context_judgment.noise,
edge_recall: context_judgment.edge_recall,
receipt_coverage,
requires_code_ok,
requires_docs_ok,
workflow_valid,
workflow_bounded,
expected_operators_ok,
task_context_ok,
run_advancement_ok,
score,
passed: score >= min_case_score,
pack_size,
node_count: dag.nodes.len(),
missing_context: context_judgment.missing,
leaked_context: context_judgment.leaked,
missing_edges: context_judgment.missing_edges,
missing_route_relation_matches,
missing_operators,
missing_task_context,
ready_after_context,
protocol,
});
}
summarize_dogfood(results)
}
pub fn dogfood_case_difficulty(case: &GoldenCase) -> String {
case.difficulty
.clone()
.or_else(|| case.class.clone())
.unwrap_or_else(|| "unclassified".to_string())
}
pub fn judge_dogfood_context(
case: &GoldenCase,
context_ids: &BTreeSet<&str>,
edges: &[Edge],
) -> DogfoodContextJudgment {
let missing = case
.context_must
.iter()
.filter(|id| !context_ids.contains(id.as_str()))
.cloned()
.collect::<Vec<_>>();
let leaked = case
.context_must_not
.iter()
.filter(|id| context_ids.contains(id.as_str()))
.cloned()
.collect::<Vec<_>>();
let recall = if case.context_must.is_empty() {
None
} else {
Some((case.context_must.len() - missing.len()) as f64 / case.context_must.len() as f64)
};
let noise = if case.context_must_not.is_empty() {
None
} else {
Some(leaked.len() as f64 / case.context_must_not.len() as f64)
};
let missing_edges = case
.context_edges_must
.iter()
.filter(|expected| !edge_present(expected, edges))
.cloned()
.collect::<Vec<_>>();
let edge_recall = if case.context_edges_must.is_empty() {
None
} else {
Some(
(case.context_edges_must.len() - missing_edges.len()) as f64
/ case.context_edges_must.len() as f64,
)
};
DogfoodContextJudgment {
recall,
noise,
edge_recall,
missing,
leaked,
missing_edges,
}
}
fn edge_present(expected: &ContextEdgeExpectation, edges: &[Edge]) -> bool {
edges.iter().any(|edge| {
edge.from == expected.from && edge.to == expected.to && edge.relation == expected.relation
})
}
pub fn score_dogfood_checks(checks: &[bool]) -> f64 {
if checks.is_empty() {
0.0
} else {
checks.iter().filter(|ok| **ok).count() as f64 / checks.len() as f64
}
}
pub fn summarize_dogfood(cases: Vec<DogfoodCaseResult>) -> DogfoodReport {
let n = cases.len();
let frac = |count: usize| if n == 0 { 0.0 } else { count as f64 / n as f64 };
let mean = |sum: f64| if n == 0 { 0.0 } else { sum / n as f64 };
let mean_optional = |values: Vec<Option<f64>>, default: f64| {
let present = values.into_iter().flatten().collect::<Vec<_>>();
if present.is_empty() {
default
} else {
present.iter().sum::<f64>() / present.len() as f64
}
};
let mut by_difficulty = BTreeMap::<String, DogfoodDifficultyStat>::new();
let mut score_by_difficulty = BTreeMap::<String, f64>::new();
for case in &cases {
let stat = by_difficulty.entry(case.difficulty.clone()).or_default();
stat.total += 1;
stat.passed += case.passed as usize;
*score_by_difficulty
.entry(case.difficulty.clone())
.or_default() += case.score;
}
for (difficulty, stat) in &mut by_difficulty {
stat.mean_score = score_by_difficulty
.get(difficulty)
.copied()
.unwrap_or_default()
/ stat.total as f64;
}
let relation_evidence_expected = cases
.iter()
.map(|case| case.route_relation_expected)
.sum::<usize>();
let relation_evidence_missing = cases
.iter()
.map(|case| case.route_relation_missing)
.sum::<usize>();
let relation_evidence_recall = if relation_evidence_expected == 0 {
None
} else {
Some(
(relation_evidence_expected - relation_evidence_missing) as f64
/ relation_evidence_expected as f64,
)
};
DogfoodReport {
dogfood_cases: n,
pass_rate: frac(cases.iter().filter(|case| case.passed).count()),
mean_score: mean(cases.iter().map(|case| case.score).sum()),
route_hit_rate: frac(cases.iter().filter(|case| case.route_ok).count()),
focus_route_match_rate: focus_route_match_rate(&cases),
partition_match_rate: partition_match_rate(&cases),
relation_evidence_recall,
relation_evidence_expected,
relation_evidence_missing,
context_recall: mean_optional(cases.iter().map(|case| case.context_recall).collect(), 0.0),
context_noise: mean_optional(cases.iter().map(|case| case.context_noise).collect(), 0.0),
edge_recall: mean_optional(cases.iter().map(|case| case.edge_recall).collect(), 0.0),
receipt_coverage: mean(cases.iter().map(|case| case.receipt_coverage).sum()),
code_context_rate: frac(cases.iter().filter(|case| case.requires_code_ok).count()),
docs_context_rate: frac(cases.iter().filter(|case| case.requires_docs_ok).count()),
workflow_valid_rate: frac(cases.iter().filter(|case| case.workflow_valid).count()),
workflow_bounded_rate: frac(cases.iter().filter(|case| case.workflow_bounded).count()),
operator_coverage_rate: frac(
cases
.iter()
.filter(|case| case.expected_operators_ok)
.count(),
),
task_context_rate: frac(cases.iter().filter(|case| case.task_context_ok).count()),
run_advancement_rate: frac(cases.iter().filter(|case| case.run_advancement_ok).count()),
profile_match_rate: profile_match_rate(&cases),
protocol: summarize_protocol(&cases),
by_difficulty,
cases,
}
}
pub fn compare_protocol_economics(input: DogfoodProtocolInput<'_>) -> DogfoodProtocolComparison {
const EKF_MAX_ITEMS: usize = 3;
const EKF_MAX_CONTEXT: usize = 40;
const EKF_MAX_RECEIPTS: usize = 3;
let legacy_request = json!({
"query": input.case.query.as_str(),
"limit": input.limit,
"depth": input.depth,
"width": input.width
});
let edge_refs = input
.edges
.iter()
.map(|edge| {
json!({
"from": edge.from.as_str(),
"to": edge.to.as_str(),
"relation": edge.relation.as_str()
})
})
.collect::<Vec<_>>();
let hit_ids = input
.hits
.iter()
.map(|hit| hit.id.as_str())
.collect::<Vec<_>>();
let legacy_response = json!({
"route_top": input.hits.first().map(|hit| hit.id.as_str()),
"hits": hit_ids,
"context": input.context_order,
"edges": edge_refs,
"receipt_coverage": input.receipt_coverage,
"context_recall": input.context_judgment.recall
});
let partitions = input
.case
.expected_partition
.as_ref()
.map(|partition| vec![partition.as_str()])
.unwrap_or_default();
let ekf_request = json!({
"ekf": {
"ekf_version": "0.1",
"intent": {
"query": input.case.query.as_str(),
"task": input.case.class.as_deref()
},
"scope": {
"partitions": partitions
},
"evidence": {
"require_receipts": true,
"include_relation_evidence": !input.case.route_relation_must.is_empty()
|| !input.case.context_edges_must.is_empty()
},
"output": {
"format": "evidence_packet",
"verbosity": "compact",
"max_items": EKF_MAX_ITEMS,
"max_context": EKF_MAX_CONTEXT,
"max_receipts": EKF_MAX_RECEIPTS,
"include_next_tools": true
}
}
});
let ekf_context = input
.context_order
.iter()
.take(EKF_MAX_CONTEXT)
.cloned()
.collect::<Vec<_>>();
let ekf_context_ids = ekf_context
.iter()
.map(String::as_str)
.collect::<BTreeSet<_>>();
let ekf_context_judgment = judge_dogfood_context(input.case, &ekf_context_ids, input.edges);
let ekf_evidence = input
.hits
.iter()
.take(EKF_MAX_ITEMS)
.map(|hit| hit.id.as_str())
.collect::<Vec<_>>();
let ekf_response = json!({
"kind": "eidos.evidence_packet",
"route_top": input.hits.first().map(|hit| hit.id.as_str()),
"evidence": ekf_evidence,
"context": ekf_context,
"omitted_context": input.context_order.len().saturating_sub(EKF_MAX_CONTEXT),
"receipts_retained": input.context_order.len().min(EKF_MAX_RECEIPTS),
"receipt_coverage": input.receipt_coverage,
"context_recall": ekf_context_judgment.recall,
"ekf_request": ekf_request["ekf"]
});
let legacy_request_tokens = estimate_tokens(&legacy_request);
let legacy_response_tokens = estimate_tokens(&legacy_response);
let ekf_request_tokens = estimate_tokens(&ekf_request);
let ekf_response_tokens = estimate_tokens(&ekf_response);
let legacy_total_tokens = legacy_request_tokens + legacy_response_tokens;
let ekf_total_tokens = ekf_request_tokens + ekf_response_tokens;
let confidence_band = input
.hits
.first()
.map(|hit| format!("{:?}", hit.confidence).to_lowercase());
let legacy = DogfoodProtocolRun {
request_tokens: legacy_request_tokens,
response_tokens: legacy_response_tokens,
total_tokens: legacy_total_tokens,
route_ok: input.route_ok,
confidence_band: confidence_band.clone(),
trust_tier: input.route_trust_tier.clone(),
trust_verdict: input.route_trust_verdict.clone(),
context_recall: input.context_judgment.recall,
receipt_coverage: input.receipt_coverage,
};
let ekf_compact = DogfoodProtocolRun {
request_tokens: ekf_request_tokens,
response_tokens: ekf_response_tokens,
total_tokens: ekf_total_tokens,
route_ok: input.route_ok,
confidence_band,
trust_tier: input.route_trust_tier.clone(),
trust_verdict: input.route_trust_verdict.clone(),
context_recall: ekf_context_judgment.recall,
receipt_coverage: input.receipt_coverage,
};
let request_token_overhead = ekf_request_tokens as isize - legacy_request_tokens as isize;
let response_token_delta = legacy_response_tokens as isize - ekf_response_tokens as isize;
let total_token_delta = legacy_total_tokens as isize - ekf_total_tokens as isize;
let quality_preserved = input.route_ok
&& ekf_context_judgment
.recall
.zip(input.context_judgment.recall)
.is_none_or(|(ekf, legacy)| ekf >= legacy);
DogfoodProtocolComparison {
legacy,
ekf_compact,
request_token_overhead,
response_token_delta,
total_token_delta,
response_token_savings_rate: savings_rate(response_token_delta, legacy_response_tokens),
total_token_savings_rate: savings_rate(total_token_delta, legacy_total_tokens),
quality_preserved,
}
}
fn estimate_tokens(value: &serde_json::Value) -> usize {
serde_json::to_string(value)
.map(|text| text.len().div_ceil(4).max(1))
.unwrap_or(1)
}
fn savings_rate(delta: isize, baseline_tokens: usize) -> f64 {
if baseline_tokens == 0 {
0.0
} else {
delta as f64 / baseline_tokens as f64
}
}
fn summarize_protocol(cases: &[DogfoodCaseResult]) -> DogfoodProtocolSummary {
let mut summary = DogfoodProtocolSummary::default();
for case in cases {
summary.legacy_request_tokens += case.protocol.legacy.request_tokens;
summary.legacy_response_tokens += case.protocol.legacy.response_tokens;
summary.legacy_total_tokens += case.protocol.legacy.total_tokens;
summary.ekf_request_tokens += case.protocol.ekf_compact.request_tokens;
summary.ekf_response_tokens += case.protocol.ekf_compact.response_tokens;
summary.ekf_total_tokens += case.protocol.ekf_compact.total_tokens;
}
summary.request_token_overhead =
summary.ekf_request_tokens as isize - summary.legacy_request_tokens as isize;
summary.response_token_delta =
summary.legacy_response_tokens as isize - summary.ekf_response_tokens as isize;
summary.total_token_delta =
summary.legacy_total_tokens as isize - summary.ekf_total_tokens as isize;
summary.response_token_savings_rate =
savings_rate(summary.response_token_delta, summary.legacy_response_tokens);
summary.total_token_savings_rate =
savings_rate(summary.total_token_delta, summary.legacy_total_tokens);
let n = cases.len();
if n > 0 {
summary.ekf_quality_preservation_rate = cases
.iter()
.filter(|case| case.protocol.quality_preserved)
.count() as f64
/ n as f64;
summary.ekf_route_hit_rate = cases
.iter()
.filter(|case| case.protocol.ekf_compact.route_ok)
.count() as f64
/ n as f64;
summary.ekf_confidence_coverage_rate = cases
.iter()
.filter(|case| case.protocol.ekf_compact.confidence_band.is_some())
.count() as f64
/ n as f64;
summary.ekf_trust_tier_coverage_rate = cases
.iter()
.filter(|case| case.protocol.ekf_compact.trust_tier.is_some())
.count() as f64
/ n as f64;
let recalls = cases
.iter()
.filter_map(|case| case.protocol.ekf_compact.context_recall)
.collect::<Vec<_>>();
if !recalls.is_empty() {
summary.ekf_context_recall = recalls.iter().sum::<f64>() / recalls.len() as f64;
}
}
for case in cases {
if let Some(confidence) = &case.protocol.ekf_compact.confidence_band {
*summary
.confidence_bands
.entry(confidence.clone())
.or_default() += 1;
}
if let Some(tier) = &case.protocol.ekf_compact.trust_tier {
*summary.trust_tiers.entry(tier.clone()).or_default() += 1;
}
if let Some(verdict) = &case.protocol.ekf_compact.trust_verdict {
*summary.trust_verdicts.entry(verdict.clone()).or_default() += 1;
}
}
summary
}
fn focus_route_match_rate(cases: &[DogfoodCaseResult]) -> Option<f64> {
let judged = cases
.iter()
.filter_map(|case| case.focus_route_ok)
.collect::<Vec<_>>();
if judged.is_empty() {
None
} else {
Some(judged.iter().filter(|ok| **ok).count() as f64 / judged.len() as f64)
}
}
fn acceptable_focus_routes(c: &GoldenCase) -> Option<Vec<String>> {
let mut expected = c.expected_focus_routes.clone();
if let Some(route) = &c.expected_focus_route {
expected.push(route.clone());
}
expected.sort();
expected.dedup();
(!expected.is_empty()).then_some(expected)
}
fn partition_match_rate(cases: &[DogfoodCaseResult]) -> Option<f64> {
let judged = cases
.iter()
.filter_map(|case| case.partition_ok)
.collect::<Vec<_>>();
(!judged.is_empty())
.then(|| judged.iter().filter(|ok| **ok).count() as f64 / judged.len() as f64)
}
fn profile_match_rate(cases: &[DogfoodCaseResult]) -> Option<f64> {
let judged = cases
.iter()
.filter_map(|case| case.profile_ok)
.collect::<Vec<_>>();
if judged.is_empty() {
None
} else {
Some(judged.iter().filter(|ok| **ok).count() as f64 / judged.len() as f64)
}
}
pub fn dogfood_task_context_is_useful(
dag: &crate::workflow::TaskDag,
task_id: &str,
context: &[TaskContextItem],
) -> bool {
let Ok(slice) = task_context_slice(dag, task_id, context) else {
return false;
};
if slice.tools.is_empty() || slice.required_checks.is_empty() {
return false;
}
if slice.operator == TaskOperator::ReadContext {
!slice.context.is_empty() && !slice.receipts.is_empty()
} else {
!slice.context.is_empty() || !slice.artifact_inputs.is_empty() || !slice.receipts.is_empty()
}
}
pub fn is_dogfood_code_kind(kind: Kind) -> bool {
matches!(
kind,
Kind::Function | Kind::Type | Kind::Trait | Kind::Module
)
}
pub fn is_dogfood_docs_kind(kind: Kind) -> bool {
matches!(kind, Kind::Doc | Kind::Skill | Kind::Section | Kind::Agent)
}
pub fn dogfood_operator_label(operator: TaskOperator) -> &'static str {
match operator {
TaskOperator::ReadContext => "read_context",
TaskOperator::Decompose => "decompose",
TaskOperator::Edit => "edit",
TaskOperator::Check => "check",
TaskOperator::Review => "review",
TaskOperator::ProposeDoc => "propose_doc",
TaskOperator::HumanApproval => "human_approval",
}
}
pub fn advance_dogfood_context_step(dag: &crate::workflow::TaskDag) -> (bool, Vec<String>) {
let Ok(run) = initial_run(dag) else {
return (false, Vec::new());
};
if run.states.get("context") != Some(&TaskState::Ready) {
return (false, Vec::new());
}
let Ok(run) = apply_task_event(
dag,
&run,
TaskEvent {
task_id: "context".to_string(),
to: TaskState::Running,
evidence: vec!["dogfood:focus-context".to_string()],
note: None,
},
) else {
return (false, Vec::new());
};
let Ok(run) = apply_task_event(
dag,
&run,
TaskEvent {
task_id: "context".to_string(),
to: TaskState::Passed,
evidence: vec!["dogfood:context-read".to_string()],
note: None,
},
) else {
return (false, Vec::new());
};
let Ok(ready) = ready_tasks(dag, &run) else {
return (false, Vec::new());
};
let ok = !ready.is_empty() && !ready.iter().any(|id| id == "context");
(ok, ready)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summarize_dogfood_reports_profile_match_rate_for_judged_cases() {
let report = summarize_dogfood(vec![
dogfood_case(Some(true)),
dogfood_case(Some(false)),
dogfood_case(None),
]);
assert_eq!(report.dogfood_cases, 3);
assert_eq!(report.profile_match_rate, Some(0.5));
}
#[test]
fn summarize_dogfood_reports_relation_evidence_recall_for_judged_cases() {
let mut pass = dogfood_case(None);
pass.route_relation_expected = 1;
pass.route_relation_missing = 0;
let mut fail = dogfood_case(None);
fail.route_relation_expected = 1;
fail.route_relation_missing = 1;
let report = summarize_dogfood(vec![pass, fail, dogfood_case(None)]);
assert_eq!(report.relation_evidence_recall, Some(0.5));
assert_eq!(report.relation_evidence_expected, 2);
assert_eq!(report.relation_evidence_missing, 1);
}
fn dogfood_case(profile_ok: Option<bool>) -> DogfoodCaseResult {
DogfoodCaseResult {
query: "task".to_string(),
difficulty: "easy".to_string(),
expected_profile: profile_ok.map(|_| "safe-change".to_string()),
profile: profile_ok.map(|ok| {
if ok {
"safe-change".to_string()
} else {
"other".to_string()
}
}),
profile_ok,
route_top: Some("doc.task".to_string()),
expected_partition: None,
route_partition: None,
partition_ok: None,
route_rank: Some(1),
route_ok: true,
expected_focus_route: None,
expected_focus_routes: Vec::new(),
focus_route: "doc.task".to_string(),
focus_route_ok: None,
route_relation_matches: Vec::new(),
route_relation_ok: None,
route_relation_expected: 0,
route_relation_missing: 0,
context_recall: Some(1.0),
context_noise: Some(0.0),
edge_recall: Some(1.0),
receipt_coverage: 1.0,
requires_code_ok: true,
requires_docs_ok: true,
workflow_valid: true,
workflow_bounded: true,
expected_operators_ok: true,
task_context_ok: true,
run_advancement_ok: true,
score: 1.0,
passed: true,
pack_size: 1,
node_count: 1,
missing_context: Vec::new(),
leaked_context: Vec::new(),
missing_edges: Vec::new(),
missing_route_relation_matches: Vec::new(),
missing_operators: Vec::new(),
missing_task_context: Vec::new(),
ready_after_context: Vec::new(),
protocol: DogfoodProtocolComparison::default(),
}
}
}