///|
/// Explainable decision support built on HRV quality, recovery, and load data.
/// It intentionally emits findings and actions instead of opaque medical claims.
pub(all) enum DecisionSeverity {
DecisionInfo
DecisionAdvisory
DecisionWarning
DecisionUrgent
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) enum DecisionDomain {
DecisionSignal
DecisionRecovery
DecisionTraining
DecisionSleep
DecisionConsistency
DecisionSystem
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct DecisionFinding {
code : String
domain : DecisionDomain
severity : DecisionSeverity
title : String
evidence : String
observed : Double
reference : Double
confidence : Double
action : String
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct DecisionContext {
quality_report : QualityReport?
recovery_report : LongitudinalRecoveryReport?
load_plan : TrainingLoadPlan?
sleep_hours : Double
sleep_efficiency : Double
symptom_score : Double
user_goal : String
requested_intensity : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct DecisionAction {
priority : Int
title : String
instruction : String
duration_minutes : Double
intensity_ceiling : Double
requires_recheck : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct DecisionPlan {
score : Double
confidence : Double
level : DecisionSeverity
findings : Array[DecisionFinding]
actions : Array[DecisionAction]
headline : String
disclaimer : String
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct DecisionThresholds {
quality_floor : Double
readiness_floor : Double
readiness_ceiling : Double
load_ratio_watch : Double
load_ratio_warning : Double
sleep_floor_hours : Double
confidence_floor : Double
symptom_warning : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
pub fn DecisionThresholds::default() -> DecisionThresholds {
{
quality_floor: 0.70,
readiness_floor: 48.0,
readiness_ceiling: 70.0,
load_ratio_watch: 1.10,
load_ratio_warning: 1.30,
sleep_floor_hours: 6.0,
confidence_floor: 0.35,
symptom_warning: 5.0,
}
}
///|
pub fn decision_severity_name(value : DecisionSeverity) -> String {
match value {
DecisionInfo => "info"
DecisionAdvisory => "advisory"
DecisionWarning => "warning"
DecisionUrgent => "urgent"
}
}
///|
pub fn decision_domain_name(value : DecisionDomain) -> String {
match value {
DecisionSignal => "signal"
DecisionRecovery => "recovery"
DecisionTraining => "training"
DecisionSleep => "sleep"
DecisionConsistency => "consistency"
DecisionSystem => "system"
}
}
///|
fn decision_bound(value : Double, low : Double, high : Double) -> Double {
if value.is_nan() || value.is_inf() {
low
} else {
value.clamp(min=low, max=high)
}
}
///|
fn decision_severity_score(value : DecisionSeverity) -> Double {
match value {
DecisionInfo => 0.0
DecisionAdvisory => 0.33
DecisionWarning => 0.67
DecisionUrgent => 1.0
}
}
///|
fn decision_more_severe(
left : DecisionSeverity,
right : DecisionSeverity,
) -> Bool {
decision_severity_score(left) > decision_severity_score(right)
}
///|
pub fn make_decision_context(
quality_report : QualityReport?,
recovery_report : LongitudinalRecoveryReport?,
load_plan : TrainingLoadPlan?,
sleep_hours : Double,
sleep_efficiency : Double,
symptom_score : Double,
user_goal : String,
requested_intensity : Double,
) -> DecisionContext {
{
quality_report,
recovery_report,
load_plan,
sleep_hours: decision_bound(sleep_hours, 0.0, 24.0),
sleep_efficiency: decision_bound(sleep_efficiency, 0.0, 1.0),
symptom_score: decision_bound(symptom_score, 0.0, 10.0),
user_goal,
requested_intensity: decision_bound(requested_intensity, 0.0, 1.0),
}
}
///|
pub fn decision_finding(
code : String,
domain : DecisionDomain,
severity : DecisionSeverity,
title : String,
evidence : String,
observed : Double,
reference : Double,
confidence : Double,
action : String,
) -> DecisionFinding {
{
code,
domain,
severity,
title,
evidence,
observed,
reference,
confidence: decision_bound(confidence, 0.0, 1.0),
action,
}
}
///|
pub fn decision_action(
priority : Int,
title : String,
instruction : String,
duration_minutes : Double,
intensity_ceiling : Double,
requires_recheck : Bool,
) -> DecisionAction {
{
priority: priority.max(0),
title,
instruction,
duration_minutes: decision_bound(duration_minutes, 0.0, 1440.0),
intensity_ceiling: decision_bound(intensity_ceiling, 0.0, 1.0),
requires_recheck,
}
}
///|
pub fn decision_quality_finding(
report : QualityReport,
thresholds : DecisionThresholds,
) -> DecisionFinding? {
let ratio = report.clean_ratio
if ratio >= thresholds.quality_floor {
None
} else {
Some(
decision_finding(
"quality_floor",
DecisionSignal,
if ratio < thresholds.quality_floor * 0.60 {
DecisionUrgent
} else {
DecisionWarning
},
"Signal quality is below the operating floor",
"Usable beat coverage is insufficient for a confident recovery decision.",
ratio,
thresholds.quality_floor,
ratio.clamp(min=0.0, max=1.0),
"Repeat the recording with stable sensor contact and minimal movement.",
),
)
}
}
///|
pub fn decision_recovery_findings(
report : LongitudinalRecoveryReport,
thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
let findings = []
if report.current_status == RecoveryStrained {
findings.push(
decision_finding(
"recovery_strained",
DecisionRecovery,
DecisionWarning,
"Recovery is below the preferred range",
recovery_report_message(report),
report.current_score,
thresholds.readiness_floor,
report.quality_ratio,
"Reduce intensity and prioritize sleep before the next hard session.",
),
)
} else if report.current_status == RecoveryUnavailable {
findings.push(
decision_finding(
"recovery_unavailable",
DecisionRecovery,
DecisionAdvisory,
"Recovery cannot be scored confidently",
"The available baseline or current signal is incomplete.",
report.current_score,
thresholds.readiness_floor,
report.quality_ratio,
"Collect another standardized morning measurement.",
),
)
} else if report.current_score < thresholds.readiness_ceiling {
findings.push(
decision_finding(
"recovery_moderate",
DecisionRecovery,
DecisionAdvisory,
"Recovery is moderate",
"The score is usable but does not support maximal intensity.",
report.current_score,
thresholds.readiness_ceiling,
report.quality_ratio,
"Keep the session aerobic or shorten the planned hard block.",
),
)
}
if report.trajectory == RecoveryWorsening {
findings.push(
decision_finding(
"recovery_trend",
DecisionRecovery,
DecisionWarning,
"Recovery trend is worsening",
"Recent recovery scores have a negative robust trend.",
-1.0,
0.0,
report.quality_ratio,
"Review cumulative load and sleep over the previous seven days.",
),
)
}
findings
}
///|
pub fn decision_load_findings(
plan : TrainingLoadPlan,
thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
let findings = []
let ratio = plan.profile.acute_chronic_ratio
if ratio >= thresholds.load_ratio_warning {
findings.push(
decision_finding(
"load_ratio_warning",
DecisionTraining,
DecisionWarning,
"Acute load is above the reference window",
load_plan_recovery_message(plan),
ratio,
thresholds.load_ratio_warning,
load_plan_quality(plan),
"Insert a recovery day and avoid stacking another high-load session.",
),
)
} else if ratio >= thresholds.load_ratio_watch {
findings.push(
decision_finding(
"load_ratio_watch",
DecisionTraining,
DecisionAdvisory,
"Training load is rising",
load_plan_recovery_message(plan),
ratio,
thresholds.load_ratio_watch,
load_plan_quality(plan),
"Keep the next session below the recent average intensity.",
),
)
}
if plan.profile.monotony >= 2.0 {
findings.push(
decision_finding(
"load_monotony",
DecisionTraining,
DecisionAdvisory,
"Daily load has low variation",
"Repeated similar loads can reduce the value of nominal rest days.",
plan.profile.monotony,
2.0,
load_plan_quality(plan),
"Vary easy and hard days and keep at least one true rest day.",
),
)
}
findings
}
///|
pub fn decision_sleep_findings(
context : DecisionContext,
thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
let findings = []
if context.sleep_hours > 0.0 &&
context.sleep_hours < thresholds.sleep_floor_hours {
findings.push(
decision_finding(
"sleep_duration",
DecisionSleep,
DecisionWarning,
"Sleep duration is short",
"Short sleep can lower confidence in a next-session intensity decision.",
context.sleep_hours,
thresholds.sleep_floor_hours,
context.sleep_efficiency,
"Prefer recovery work and protect the next sleep opportunity.",
),
)
}
if context.sleep_efficiency > 0.0 && context.sleep_efficiency < 0.75 {
findings.push(
decision_finding(
"sleep_efficiency",
DecisionSleep,
DecisionAdvisory,
"Sleep efficiency is reduced",
"Fragmented sleep is included as a context signal, not a diagnosis.",
context.sleep_efficiency,
0.75,
context.sleep_efficiency,
"Review sleep timing and avoid using one night as a long-term conclusion.",
),
)
}
findings
}
///|
pub fn decision_symptom_finding(
context : DecisionContext,
thresholds : DecisionThresholds,
) -> DecisionFinding? {
if context.symptom_score < thresholds.symptom_warning {
None
} else {
Some(
decision_finding(
"reported_symptoms",
DecisionSystem,
if context.symptom_score >= 8.0 {
DecisionUrgent
} else {
DecisionWarning
},
"Reported symptoms require priority",
"Self-reported symptoms take precedence over a favorable wearable score.",
context.symptom_score,
thresholds.symptom_warning,
1.0,
"Stop the planned hard session and seek appropriate professional advice.",
),
)
}
}
///|
fn decision_append_all(
target : Array[DecisionFinding],
source : Array[DecisionFinding],
) -> Unit {
for item in source {
target.push(item)
}
}
///|
pub fn decision_collect_findings(
context : DecisionContext,
thresholds : DecisionThresholds,
) -> Array[DecisionFinding] {
let findings = []
match context.quality_report {
Some(report) =>
match decision_quality_finding(report, thresholds) {
Some(finding) => findings.push(finding)
None => ()
}
None =>
findings.push(
decision_finding(
"quality_missing",
DecisionSignal,
DecisionAdvisory,
"Signal quality is not available",
"The decision engine received no quality report.",
0.0,
thresholds.quality_floor,
0.0,
"Require a quality report before interpreting recovery.",
),
)
}
match context.recovery_report {
Some(report) =>
decision_append_all(
findings,
decision_recovery_findings(report, thresholds),
)
None =>
findings.push(
decision_finding(
"recovery_missing",
DecisionRecovery,
DecisionAdvisory,
"Recovery context is missing",
"No longitudinal recovery report was supplied.",
0.0,
thresholds.readiness_floor,
0.0,
"Collect a standardized morning measurement first.",
),
)
}
match context.load_plan {
Some(plan) =>
decision_append_all(findings, decision_load_findings(plan, thresholds))
None => ()
}
decision_append_all(findings, decision_sleep_findings(context, thresholds))
match decision_symptom_finding(context, thresholds) {
Some(finding) => findings.push(finding)
None => ()
}
findings
}
///|
pub fn decision_max_severity(
findings : Array[DecisionFinding],
) -> DecisionSeverity {
let mut result = DecisionInfo
for finding in findings {
if decision_more_severe(finding.severity, result) {
result = finding.severity
}
}
result
}
///|
pub fn decision_confidence(findings : Array[DecisionFinding]) -> Double {
if findings.length() == 0 {
1.0
} else {
mean_value(findings.map(finding => finding.confidence))
}
}
///|
pub fn decision_score(
context : DecisionContext,
findings : Array[DecisionFinding],
) -> Double {
let mut score = 80.0
for finding in findings {
score -= decision_severity_score(finding.severity) *
18.0 *
finding.confidence
}
if context.requested_intensity > 0.80 {
score -= 5.0
}
decision_bound(score, 0.0, 100.0)
}
///|
pub fn decision_actions(
context : DecisionContext,
findings : Array[DecisionFinding],
score : Double,
) -> Array[DecisionAction] {
let actions = []
let mut priority = 1
for finding in findings {
if finding.severity != DecisionInfo {
actions.push(
decision_action(
priority,
finding.title,
finding.action,
if finding.domain == DecisionTraining {
45.0
} else {
20.0
},
if finding.severity == DecisionUrgent {
0.20
} else {
0.60
},
finding.severity != DecisionInfo,
),
)
priority += 1
}
}
if actions.length() == 0 {
actions.push(
decision_action(
1,
"Proceed with the planned session",
if score >= 70.0 {
"Use the planned intensity and continue monitoring."
} else {
"Use a controlled version of the planned session."
},
60.0,
if context.requested_intensity < 0.80 {
context.requested_intensity
} else {
0.80
},
false,
),
)
}
actions.sort_by((left, right) => left.priority - right.priority)
actions
}
///|
pub fn build_decision_plan(
context : DecisionContext,
thresholds : DecisionThresholds,
) -> DecisionPlan {
let findings = decision_collect_findings(context, thresholds)
let score = decision_score(context, findings)
let level = decision_max_severity(findings)
let confidence = decision_confidence(findings)
let actions = decision_actions(context, findings, score)
let headline = match level {
DecisionInfo => "No decision blockers were detected."
DecisionAdvisory => "Proceed with a controlled plan."
DecisionWarning => "Reduce training stress before progressing."
DecisionUrgent => "Pause the planned intensity and reassess."
}
{
score,
confidence,
level,
findings,
actions,
headline,
disclaimer: "This is a signal-informed training aid, not a medical diagnosis.",
}
}
///|
pub fn decision_plan_is_actionable(plan : DecisionPlan) -> Bool {
plan.actions.length() > 0 && plan.confidence >= 0.20
}
///|
pub fn decision_plan_feature_vector(plan : DecisionPlan) -> Array[Double] {
let mut intensity_sum = 0.0
for action in plan.actions {
intensity_sum += action.intensity_ceiling
}
[
plan.score,
plan.confidence,
decision_severity_score(plan.level),
plan.findings.length().to_double(),
plan.actions.length().to_double(),
plan.actions.filter(action => action.requires_recheck).length().to_double(),
intensity_sum,
]
}
///|
pub fn decision_plan_csv(plan : DecisionPlan) -> String {
let grid = [
[
"code", "domain", "severity", "title", "observed", "reference", "confidence",
"action",
],
]
for finding in plan.findings {
grid.push([
finding.code,
decision_domain_name(finding.domain),
decision_severity_name(finding.severity),
finding.title,
finding.observed.to_string(),
finding.reference.to_string(),
finding.confidence.to_string(),
finding.action,
])
}
to_csv(grid)
}
///|
pub fn decision_actions_csv(plan : DecisionPlan) -> String {
let grid = [
[
"priority", "title", "instruction", "duration_minutes", "intensity_ceiling",
"requires_recheck",
],
]
for action in plan.actions {
grid.push([
action.priority.to_string(),
action.title,
action.instruction,
action.duration_minutes.to_string(),
action.intensity_ceiling.to_string(),
action.requires_recheck.to_string(),
])
}
to_csv(grid)
}
///|
pub fn decision_plan_summary(plan : DecisionPlan) -> String {
"\{plan.headline} score=\{plan.score.to_string()} confidence=\{plan.confidence.to_string()} findings=\{plan.findings.length().to_string()}"
}
///|
pub fn decision_plan_recheck_required(plan : DecisionPlan) -> Bool {
plan.actions.any(action => action.requires_recheck) ||
plan.level == DecisionUrgent
}
///|
pub fn decision_domain_counts(findings : Array[DecisionFinding]) -> Array[Int] {
[
findings.filter(item => item.domain == DecisionSignal).length(),
findings.filter(item => item.domain == DecisionRecovery).length(),
findings.filter(item => item.domain == DecisionTraining).length(),
findings.filter(item => item.domain == DecisionSleep).length(),
findings.filter(item => item.domain == DecisionConsistency).length(),
findings.filter(item => item.domain == DecisionSystem).length(),
]
}
///|
pub fn decision_plan_has_domain(
plan : DecisionPlan,
domain : DecisionDomain,
) -> Bool {
plan.findings.any(item => item.domain == domain)
}
///|
pub fn decision_plan_risk_score(plan : DecisionPlan) -> Double {
(1.0 - plan.score / 100.0).clamp(min=0.0, max=1.0) *
(0.50 + decision_severity_score(plan.level) * 0.50)
}
///|
pub fn decision_plan_intensity_ceiling(plan : DecisionPlan) -> Double {
if plan.actions.length() == 0 {
0.0
} else {
let mut result = 1.0
for action in plan.actions {
if action.intensity_ceiling < result {
result = action.intensity_ceiling
}
}
result
}
}
///|
pub fn decision_findings_for_severity(
plan : DecisionPlan,
severity : DecisionSeverity,
) -> Array[DecisionFinding] {
plan.findings.filter(item => item.severity == severity)
}
///|
pub fn decision_plan_message(plan : DecisionPlan) -> String {
if decision_plan_recheck_required(plan) {
"Recheck recovery or signal quality before increasing intensity."
} else {
"The current evidence supports a controlled continuation."
}
}