///|
/// Default quality thresholds for the deterministic package fixtures.
pub fn default_quality_gate() -> QualityGateConfig {
{
max_mae: 1000000.0,
max_rmse: 1000000.0,
min_confidence: 0.0,
min_points: 1,
require_monotonic_distance: true,
}
}
///|
/// Return a gate with non-negative limits and sensible cardinality.
pub fn sanitize_quality_gate(config : QualityGateConfig) -> QualityGateConfig {
{
max_mae: if config.max_mae < 0.0 {
0.0
} else {
config.max_mae
},
max_rmse: if config.max_rmse < 0.0 {
0.0
} else {
config.max_rmse
},
min_confidence: clamp(config.min_confidence, 0.0, 1.0),
min_points: if config.min_points < 0 {
0
} else {
config.min_points
},
require_monotonic_distance: config.require_monotonic_distance,
}
}
///|
fn gate_reason(reasons : Array[String], message : String) -> Unit {
reasons.push(message)
}
///|
/// Evaluate forecast metrics and confidence thresholds.
pub fn evaluate_quality_gate(
results : Array[EtaResult],
points : Array[ForecastPoint],
config : QualityGateConfig,
) -> QualityGateResult {
let safe = sanitize_quality_gate(config)
let reasons : Array[String] = []
let summary = summarize_errors(points)
if points.length() < safe.min_points {
gate_reason(reasons, "not enough evaluation points")
}
if summary.mae > safe.max_mae {
gate_reason(reasons, "MAE exceeds threshold")
}
if summary.rmse > safe.max_rmse {
gate_reason(reasons, "RMSE exceeds threshold")
}
for result in results {
if result.confidence < safe.min_confidence {
gate_reason(reasons, "confidence below threshold")
}
}
{
passed: reasons.length() == 0,
reasons,
checked_points: points.length(),
summary,
}
}
///|
/// Check monotonic distance progression in a trace.
pub fn monotonic_distance(observations : Array[Observation]) -> Bool {
let mut previous = -1.0
let mut valid = true
for observation in observations {
if observation.distance < previous {
valid = false
}
previous = observation.distance
}
valid
}
///|
/// Evaluate a batch with input progression and metric checks.
pub fn evaluate_batch_quality(
observations : Array[Observation],
batch : BatchForecast,
config : QualityGateConfig,
) -> QualityGateResult {
let base = evaluate_quality_gate(batch.results, batch.points, config)
if config.require_monotonic_distance && !monotonic_distance(observations) {
let reasons = base.reasons.copy()
reasons.push("distance is not monotonic")
{ ..base, passed: false, reasons }
} else {
base
}
}
///|
/// Render a gate result for CI output.
pub fn format_quality_gate(result : QualityGateResult) -> String {
let builder = StringBuilder()
builder.write_string(
"passed=\{result.passed} points=\{result.checked_points} ",
)
builder.write_string(format_error_summary(result.summary))
if result.reasons.length() > 0 {
builder.write_string(" reasons=")
builder.write_string(result.reasons.join(";"))
}
builder.to_string()
}
///|
/// Return a strict gate suitable for a production caller.
pub fn strict_quality_gate() -> QualityGateConfig {
{
max_mae: 120.0,
max_rmse: 180.0,
min_confidence: 0.05,
min_points: 3,
require_monotonic_distance: true,
}
}
///|
/// Return whether a report contains no gate failures.
pub fn quality_gate_passed(result : QualityGateResult) -> Bool {
result.passed
}