///|
/// Reproducible decision traces for process studies.
pub(all) struct TraceEvent {
sequence : Int
category : String
name : String
input : String
output : String
passed : Bool
note : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct DecisionTrace {
study : String
events : Array[TraceEvent]
passed : Bool
failure_count : Int
warning_count : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct TraceMetric {
name : String
value : Double
lower : Double
upper : Double
unit : String
accepted : Bool
} derive(Debug, Eq, ToJson)
///|
pub fn DecisionTrace::new(study~ : String) -> DecisionTrace {
{ study, events: [], passed: true, failure_count: 0, warning_count: 0 }
}
///|
pub fn DecisionTrace::record(
self : DecisionTrace,
category : String,
name : String,
input : String,
output : String,
passed : Bool,
note : String,
) -> DecisionTrace {
let events = self.events.copy()
events.push({
sequence: events.length() + 1,
category,
name,
input,
output,
passed,
note,
})
{
study: self.study,
events,
passed: self.passed && passed,
failure_count: if passed {
self.failure_count
} else {
self.failure_count + 1
},
warning_count: if note.length() == 0 {
self.warning_count
} else {
self.warning_count + 1
},
}
}
///|
pub fn DecisionTrace::add_metric(
self : DecisionTrace,
metric : TraceMetric,
) -> DecisionTrace {
self.record(
"metric",
metric.name,
metric.value.to_string(),
metric.output_text(),
metric.accepted,
if metric.accepted {
""
} else {
"metric outside acceptance band"
},
)
}
///|
pub fn TraceMetric::output_text(self : TraceMetric) -> String {
self.value.to_string() +
" " +
self.unit +
" [" +
self.lower.to_string() +
", " +
self.upper.to_string() +
"]"
}
///|
pub fn metric(
name : String,
value : Double,
lower : Double,
upper : Double,
unit : String,
) -> TraceMetric {
{
name,
value,
lower,
upper,
unit,
accepted: value >= lower && value <= upper,
}
}
///|
pub fn metric_relative_margin(metric : TraceMetric) -> Double {
if metric.lower <= 0.0 && metric.upper <= 0.0 {
0.0
} else if metric.value < metric.lower {
(metric.value - metric.lower) /
(metric.upper - metric.lower).abs().max(1.0e-12)
} else if metric.value > metric.upper {
(metric.upper - metric.value) /
(metric.upper - metric.lower).abs().max(1.0e-12)
} else {
(metric.value - metric.lower).min(metric.upper - metric.value) /
(metric.upper - metric.lower).abs().max(1.0e-12)
}
}
///|
pub fn trace_events_by_category(
trace : DecisionTrace,
category : String,
) -> Array[TraceEvent] {
let result = []
for event in trace.events {
if event.category == category {
result.push(event)
}
}
result
}
///|
pub fn trace_failures(trace : DecisionTrace) -> Array[TraceEvent] {
let result = []
for event in trace.events {
if !event.passed {
result.push(event)
}
}
result
}
///|
pub fn trace_warnings(trace : DecisionTrace) -> Array[TraceEvent] {
let result = []
for event in trace.events {
if event.note.length() > 0 {
result.push(event)
}
}
result
}
///|
pub fn trace_has_category(trace : DecisionTrace, category : String) -> Bool {
for event in trace.events {
if event.category == category {
return true
}
}
false
}
///|
pub fn trace_replay_summary(trace : DecisionTrace) -> String {
"study=" +
trace.study +
",events=" +
trace.events.length().to_string() +
",passed=" +
trace.passed.to_string() +
",failures=" +
trace.failure_count.to_string() +
",warnings=" +
trace.warning_count.to_string()
}
///|
pub fn trace_csv(trace : DecisionTrace) -> String {
let lines = ["sequence,category,name,input,output,passed,note"]
for event in trace.events {
lines.push(
event.sequence.to_string() +
"," +
event.category +
"," +
event.name +
"," +
event.input +
"," +
event.output +
"," +
event.passed.to_string() +
"," +
event.note,
)
}
lines.join("\n")
}
///|
pub fn trace_markdown(trace : DecisionTrace) -> String {
let lines = [
"# Decision trace: " + trace.study,
"",
"| # | Category | Name | Input | Output | Passed | Note |",
"|---:|---|---|---|---|---|---|",
]
for event in trace.events {
lines.push(
"| " +
event.sequence.to_string() +
" | " +
event.category +
" | " +
event.name +
" | " +
event.input +
" | " +
event.output +
" | " +
event.passed.to_string() +
" | " +
event.note +
" |",
)
}
lines.push("")
let status = if trace.passed { "passed" } else { "failed" }
lines.push("Overall status: " + status)
lines.join("\n")
}
///|
pub fn trace_merge(
first : DecisionTrace,
second : DecisionTrace,
) -> DecisionTrace {
let mut result = DecisionTrace::new(study=first.study)
for event in first.events {
result = result.record(
event.category,
event.name,
event.input,
event.output,
event.passed,
event.note,
)
}
for event in second.events {
result = result.record(
event.category,
event.name,
event.input,
event.output,
event.passed,
event.note,
)
}
result
}
///|
pub fn trace_check(trace : DecisionTrace, minimum_events : Int) -> Bool {
trace.events.length() >= minimum_events && trace.passed
}
///|
pub fn trace_event_count(trace : DecisionTrace, passed : Bool) -> Int {
let mut count = 0
for event in trace.events {
if event.passed == passed {
count += 1
}
}
count
}
///|
pub fn trace_categories(trace : DecisionTrace) -> Array[String] {
let categories = []
for event in trace.events {
let mut found = false
for category in categories {
if category == event.category {
found = true
}
}
if !found {
categories.push(event.category)
}
}
categories
}
///|
pub fn trace_category_counts(trace : DecisionTrace) -> Array[(String, Int)] {
let categories = trace_categories(trace)
let result = []
for category in categories {
let count = trace_events_by_category(trace, category).length()
result.push((category, count))
}
result
}
///|
pub fn metric_summary(metrics : ArrayView[TraceMetric]) -> String {
let mut accepted = 0
for metric in metrics {
if metric.accepted {
accepted += 1
}
}
"metrics=" +
metrics.length().to_string() +
",accepted=" +
accepted.to_string() +
",rejected=" +
(metrics.length() - accepted).to_string()
}
///|
pub fn metrics_to_trace(
study : String,
metrics : ArrayView[TraceMetric],
) -> DecisionTrace {
let mut trace = DecisionTrace::new(study~)
for metric in metrics {
trace = trace.add_metric(metric)
}
trace
}
///|
pub fn trace_json_lines(trace : DecisionTrace) -> String {
let lines = []
for event in trace.events {
lines.push(
"{\"sequence\":" +
event.sequence.to_string() +
",\"category\":\"" +
event.category +
"\",\"name\":\"" +
event.name +
"\",\"input\":\"" +
event.input +
"\",\"output\":\"" +
event.output +
"\",\"passed\":" +
event.passed.to_string() +
",\"note\":\"" +
event.note +
"\"}",
)
}
lines.join("\n")
}
///|
pub fn trace_status_code(trace : DecisionTrace) -> Int {
if trace.passed && trace.warning_count == 0 {
0
} else if trace.passed {
1
} else {
2
}
}