///|
/// Named scalar metrics exposed to dashboards and CI summaries.
pub struct AnalysisMetric {
name : String
signal_id : String?
value : Double
count : Int
} derive(Debug, Eq)
///|
/// Calculate quality and activity metrics for one signal.
pub fn signal_analysis_metrics(
trace : Trace,
signal_id : String,
) -> Array[AnalysisMetric] {
let distribution = value_distribution(trace, signal_id)
let activity_item = activity(trace, signal_id)
let frequency = frequency_estimate(trace, signal_id)
let mean_period = match frequency.mean_period {
Some(value) => value
None => 0.0
}
[
{
name: "known_ratio",
signal_id: Some(signal_id),
value: known_value_ratio(trace, distribution.signal_id),
count: distribution.total,
},
{
name: "change_rate",
signal_id: Some(distribution.signal_id),
value: change_rate(trace, signal_id),
count: activity_item.transition_count,
},
{
name: "active_fraction",
signal_id: Some(signal_id),
value: if trace.end_time == 0L {
0.0
} else {
activity_item.active_time.to_double() / trace.end_time.to_double()
},
count: activity_item.samples,
},
{
name: "mean_period",
signal_id: Some(signal_id),
value: mean_period,
count: frequency.period_count,
},
{
name: "unknown_ratio",
signal_id: Some(signal_id),
value: unknown_value_ratio(trace, signal_id),
count: distribution.unknown + distribution.high_impedance,
},
]
}
///|
/// Calculate metrics for every declared signal in declaration order.
pub fn trace_analysis_metrics(trace : Trace) -> Array[AnalysisMetric] {
let result : Array[AnalysisMetric] = []
for signal in trace.signals {
result.append(signal_analysis_metrics(trace, signal.id))
}
result
}
///|
/// Render metrics as a stable CSV document.
pub fn analysis_metrics_csv(metrics : Array[AnalysisMetric]) -> String {
let out : Array[String] = ["signal_id,metric,value,count"]
for metric in metrics {
let signal_id = match metric.signal_id {
Some(value) => value
None => ""
}
out.push(
escape_text(signal_id) +
"," +
escape_text(metric.name) +
"," +
metric.value.to_string() +
"," +
metric.count.to_string(),
)
}
out.join("\n")
}
///|
/// Render metrics as a compact JSON array.
pub fn analysis_metrics_json(metrics : Array[AnalysisMetric]) -> String {
let body = metrics
.map(fn(metric) {
let signal_id = match metric.signal_id {
Some(value) => escape_text(value)
None => ""
}
"{\"signal_id\":\"" +
signal_id +
"\",\"metric\":\"" +
escape_text(metric.name) +
"\",\"value\":" +
metric.value.to_string() +
",\"count\":" +
metric.count.to_string() +
"}"
})
.join(",")
"[" + body + "]"
}
///|
/// Return a bounded quality score in the range [0, 100].
pub fn trace_quality_score(trace : Trace) -> Int {
if trace.signals.length() == 0 {
return 0
}
let mut total = 0.0
for signal in trace.signals {
let known = known_value_ratio(trace, signal.id)
let validation = if deep_is_valid(trace) { 1.0 } else { 0.5 }
total += known * validation
}
Int::max(
0,
Int::min(100, (total / trace.signals.length().to_double() * 100.0).to_int()),
)
}
///|
/// Produce a compact quality report for verification logs.
pub fn trace_quality_report(trace : Trace) -> String {
"quality=" +
trace_quality_score(trace).to_string() +
"/100 signals=" +
trace.signals.length().to_string() +
" valid=" +
deep_is_valid(trace).to_string() +
" unknown_changes=" +
trace.changes.count_if(fn(change) { change.value.is_unknown() }).to_string()
}
///|
/// Return the number of metrics in a catalog.
pub fn analysis_metric_count(metrics : Array[AnalysisMetric]) -> Int {
metrics.length()
}
///|
/// Find a metric by name and signal identifier.
pub fn find_analysis_metric(
metrics : Array[AnalysisMetric],
signal_id : String,
name : String,
) -> AnalysisMetric? {
for metric in metrics {
match metric.signal_id {
Some(id) if id == signal_id && metric.name == name => return Some(metric)
_ => ()
}
}
None
}