#![cfg(feature = "std")]
extern crate std;
use std::format;
use std::string::{String, ToString};
use std::vec::Vec;
use crate::heuristics_bank::HeuristicsBank;
use crate::types::*;
#[derive(Debug, Clone)]
pub struct RenderedEpisodeSummary {
pub episode_id: u32,
pub start_window: u64,
pub end_window: u64,
pub duration_windows: u64,
pub motif: Option<MotifClass>,
pub motif_name: String,
pub motif_provenance: String,
pub motif_evidence: String,
pub motif_taxonomy: String,
pub root_cause_service: Option<String>,
pub root_cause_signal_index: Option<u16>,
pub contributing_signal_count: u16,
pub peak_slew_magnitude: f64,
pub policy_state: PolicyState,
pub dashboard_hint_bound: String,
pub match_confidence: Option<MatchConfidence>,
}
pub fn render_episode_summary<const M: usize>(
episode: &DebugEpisode,
signal_names: &[String],
bank: &HeuristicsBank<M>,
match_confidence: Option<MatchConfidence>,
) -> RenderedEpisodeSummary {
let motif = match episode.matched_motif {
SemanticDisposition::Named(m) => Some(m),
SemanticDisposition::Unknown => None,
};
let entry = motif.and_then(|m| bank.entry_for(m));
let motif_name = motif.map(|m| format!("{:?}", m)).unwrap_or_else(|| "Unknown".to_string());
let motif_provenance = entry.map(|e| format!("{:?}", e.provenance)).unwrap_or_default();
let motif_evidence = entry.map(|e| {
if e.evidence_dataset_doi.is_empty() {
e.evidence_dataset.to_string()
} else {
format!("{} (DOI {})", e.evidence_dataset, e.evidence_dataset_doi)
}
}).unwrap_or_default();
let motif_taxonomy = entry.map(|e| e.taxonomy_ref.to_string()).unwrap_or_default();
let root_cause_service = episode.root_cause_signal_index.and_then(|idx| {
let i = idx as usize;
if i < signal_names.len() {
Some(signal_names[i].clone())
} else {
None
}
});
let dashboard_hint_template = entry.map(|e| e.dashboard_hint).unwrap_or("");
let dashboard_hint_bound = bind_template(
dashboard_hint_template,
episode,
signal_names,
match_confidence,
&motif_name,
);
RenderedEpisodeSummary {
episode_id: episode.episode_id,
start_window: episode.start_window,
end_window: episode.end_window,
duration_windows: episode.structural_signature.duration_windows,
motif,
motif_name,
motif_provenance,
motif_evidence,
motif_taxonomy,
root_cause_service,
root_cause_signal_index: episode.root_cause_signal_index,
contributing_signal_count: episode.contributing_signal_count,
peak_slew_magnitude: episode.structural_signature.peak_slew_magnitude,
policy_state: episode.policy_state,
dashboard_hint_bound,
match_confidence,
}
}
pub fn render_episodes_summary<const M: usize>(
episodes: &[DebugEpisode],
count: usize,
signal_names: &[String],
bank: &HeuristicsBank<M>,
) -> Vec<RenderedEpisodeSummary> {
let mut out = Vec::with_capacity(count);
for ep in episodes.iter().take(count) {
out.push(render_episode_summary(ep, signal_names, bank, None));
}
out
}
fn bind_template(
template: &str,
episode: &DebugEpisode,
signal_names: &[String],
confidence: Option<MatchConfidence>,
motif_name: &str,
) -> String {
if template.is_empty() {
return String::new();
}
let mut out = template.to_string();
let rc_service = episode.root_cause_signal_index
.and_then(|idx| signal_names.get(idx as usize).cloned())
.unwrap_or_else(|| "(unknown service)".to_string());
out = out.replace("${ROOT_CAUSE_SERVICE}", &rc_service);
let rc_idx = episode.root_cause_signal_index
.map(|i| i.to_string())
.unwrap_or_else(|| "?".to_string());
out = out.replace("${ROOT_CAUSE_INDEX}", &rc_idx);
out = out.replace(
"${CONTRIBUTING_COUNT}",
&episode.contributing_signal_count.to_string(),
);
out = out.replace(
"${PEAK_SLEW}",
&format!("{:.3}", episode.structural_signature.peak_slew_magnitude),
);
out = out.replace(
"${DURATION_WINDOWS}",
&episode.structural_signature.duration_windows.to_string(),
);
out = out.replace("${MOTIF}", motif_name);
let margin_str = match confidence {
Some(c) => format!("{:.3}", c.margin),
None => "n/a".to_string(),
};
out = out.replace("${CONFIDENCE_MARGIN}", &margin_str);
let runner_up_str = match confidence.and_then(|c| c.runner_up_motif) {
Some(m) => format!("{:?}", m),
None => "none".to_string(),
};
out = out.replace("${RUNNER_UP_MOTIF}", &runner_up_str);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn ep_with_root(root: Option<u16>) -> DebugEpisode {
DebugEpisode {
episode_id: 7,
start_window: 100,
end_window: 105,
peak_grammar_state: GrammarState::Violation,
primary_reason_code: ReasonCode::AbruptSlewViolation,
matched_motif: SemanticDisposition::Named(MotifClass::CascadingTimeoutSlew),
policy_state: PolicyState::Escalate,
contributing_signal_count: 4,
structural_signature: StructuralSignature {
dominant_drift_direction: DriftDirection::Positive,
peak_slew_magnitude: 0.85,
duration_windows: 6,
signal_correlation: 0.5,
},
root_cause_signal_index: root,
}
}
fn signal_names() -> Vec<String> {
std::vec![
"ts-station-service".to_string(),
"ts-route-service".to_string(),
"ts-travel-service".to_string(),
"ts-order-service".to_string(),
]
}
#[test]
fn renders_with_bound_root_cause_service() {
let bank = HeuristicsBank::<64>::with_canonical_motifs();
let ep = ep_with_root(Some(0));
let names = signal_names();
let r = render_episode_summary(&ep, &names, &bank, None);
assert_eq!(r.motif_name, "CascadingTimeoutSlew");
assert_eq!(r.root_cause_service, Some("ts-station-service".to_string()));
assert_eq!(r.contributing_signal_count, 4);
assert!(r.motif_taxonomy.contains("fault propagation"),
"taxonomy_ref should land in rendered output");
}
#[test]
fn renders_unknown_disposition_gracefully() {
let bank = HeuristicsBank::<64>::with_canonical_motifs();
let mut ep = ep_with_root(None);
ep.matched_motif = SemanticDisposition::Unknown;
let r = render_episode_summary(&ep, &[], &bank, None);
assert_eq!(r.motif_name, "Unknown");
assert_eq!(r.root_cause_service, None);
assert!(r.motif_evidence.is_empty(),
"Unknown motifs have no evidence_dataset");
}
#[test]
fn template_substitution_binds_variables() {
let ep = ep_with_root(Some(2));
let names = signal_names();
let template = "Inspect ${ROOT_CAUSE_SERVICE} (signal ${ROOT_CAUSE_INDEX}); \
${CONTRIBUTING_COUNT} services contribute, \
peak_slew=${PEAK_SLEW}, duration=${DURATION_WINDOWS} windows; \
motif=${MOTIF}, confidence margin ${CONFIDENCE_MARGIN}";
let bound = bind_template(template, &ep, &names, None, "CascadingTimeoutSlew");
assert!(bound.contains("ts-travel-service"),
"ROOT_CAUSE_SERVICE should bind to channels[root_cause_signal_index]");
assert!(bound.contains("(signal 2)"));
assert!(bound.contains("4 services contribute"));
assert!(bound.contains("peak_slew=0.850"));
assert!(bound.contains("duration=6 windows"));
assert!(bound.contains("motif=CascadingTimeoutSlew"));
assert!(bound.contains("confidence margin n/a"));
}
#[test]
fn template_substitution_uses_confidence_margin() {
let ep = ep_with_root(Some(0));
let names = signal_names();
let confidence = MatchConfidence {
disposition: SemanticDisposition::Named(MotifClass::CascadingTimeoutSlew),
top_score: 4.5,
runner_up_score: 1.5,
runner_up_motif: Some(MotifClass::DeploymentRegressionSlew),
margin: 0.667,
tier_consensus_factor: 0.0,
confuser_motif: None,
confuser_score: 0.0,
margin_vs_confuser: 0.0,
};
let bound = bind_template(
"${MOTIF} margin=${CONFIDENCE_MARGIN} runner_up=${RUNNER_UP_MOTIF}",
&ep, &names, Some(confidence), "CascadingTimeoutSlew");
assert_eq!(
bound,
"CascadingTimeoutSlew margin=0.667 runner_up=DeploymentRegressionSlew",
);
}
#[test]
fn missing_signal_names_falls_through_gracefully() {
let bank = HeuristicsBank::<64>::with_canonical_motifs();
let ep = ep_with_root(Some(0));
let r = render_episode_summary(&ep, &[], &bank, None);
assert_eq!(r.root_cause_service, None);
assert!(r.dashboard_hint_bound.contains("ts-")
|| r.dashboard_hint_bound.contains("unknown")
|| r.dashboard_hint_bound.contains("service"));
}
}