///|
/// States whether a rule result reports an observed fact or a hypothesis.
pub(all) enum FindingKind {
Observation
Hypothesis
} derive(Debug, Eq, ToJson)
///|
/// A configurable single-event rule over normalized severity and raw text.
pub(all) struct DiagnosticRule {
rule_id : String
title : String
explanation : String
kind : FindingKind
confidence_note : String
severity : Severity?
text_contains : String?
} derive(Debug, Eq, ToJson)
///|
/// A stable reference to an event in the timeline and its untouched raw content.
pub(all) struct EvidenceReference {
event_index : Int
source_id : String
timestamp : NormalizedTimestamp?
raw_content : String
} derive(Debug, Eq, ToJson)
///|
/// One explained rule match with the supporting timeline event reference.
pub(all) struct DiagnosticFinding {
rule_id : String
title : String
explanation : String
kind : FindingKind
confidence_note : String
evidence : Array[EvidenceReference]
} derive(Debug, Eq, ToJson)
///|
/// The deterministic findings produced by evaluating an ordered rule set.
pub(all) struct DiagnosticReport {
findings : Array[DiagnosticFinding]
} derive(Debug, Eq, ToJson)
///|
fn rule_matches(rule : DiagnosticRule, event : NormalizedEvent) -> Bool {
let severity_matches = match rule.severity {
Some(expected) => event.severity == Some(expected)
None => true
}
let text_matches = match rule.text_contains {
Some(fragment) => event.raw_content.contains(fragment)
None => true
}
severity_matches && text_matches
}
///|
/// Evaluates rules in declaration order and events in timeline order.
/// Each finding cites its timeline index, normalized source, time, and raw content.
///
/// # Example
/// ```mbt check
/// test {
/// let event = NormalizedEvent::{
/// timestamp: None,
/// severity: Some(Severity::Error),
/// source_id: "svc/api",
/// raw_content: "database connection failed",
/// }
/// let timeline = build_timeline([event])
/// let rule = DiagnosticRule::{
/// rule_id: "db-error",
/// title: "Database error observed",
/// explanation: "The log reports a database connection failure.",
/// kind: FindingKind::Observation,
/// confidence_note: "Directly present in the source log.",
/// severity: Some(Severity::Error),
/// text_contains: Some("database connection"),
/// }
/// let report = evaluate_diagnostic_rules(timeline, [rule])
/// assert_eq(report.findings.length(), 1)
/// }
/// ```
pub fn evaluate_diagnostic_rules(
timeline : Timeline,
rules : Array[DiagnosticRule],
) -> DiagnosticReport {
let findings : Array[DiagnosticFinding] = []
for rule in rules {
for event_index, event in timeline.events {
if rule_matches(rule, event) {
let evidence : Array[EvidenceReference] = [
{
event_index,
source_id: event.source_id,
timestamp: event.timestamp,
raw_content: event.raw_content,
},
]
findings.push({
rule_id: rule.rule_id,
title: rule.title,
explanation: rule.explanation,
kind: rule.kind,
confidence_note: rule.confidence_note,
evidence,
})
}
}
}
{ findings, }
}