use pointlock_expr::{EvalError, Scope, eval};
use pointlock_ir::{
ActionStepIR, AssertionIR, AssertionOutcomeRecord, Expr, VerdictPolicy, VerdictStatus,
};
use serde_json::Value;
pub(crate) fn project_output(
step: &ActionStepIR,
raw: &Value,
scope_with_self_raw: &Scope,
) -> Result<Value, EvalError> {
match &step.outputs {
None => Ok(raw.clone()),
Some(map) => {
let mut projected = serde_json::Map::new();
for (name, expr) in map.iter() {
projected.insert(name.as_str().to_owned(), eval(expr, scope_with_self_raw)?);
}
Ok(Value::Object(projected))
}
}
}
pub(crate) fn eval_expr_assertion(
assertion: &AssertionIR,
expr: &Expr,
scope: &Scope,
) -> AssertionOutcomeRecord {
let (result, reason) = match eval(expr, scope) {
Ok(Value::Bool(true)) => (
VerdictStatus::Pass,
"expr predicate evaluated to true".to_owned(),
),
Ok(Value::Bool(false)) => (
VerdictStatus::Fail,
"expr predicate evaluated to false".to_owned(),
),
Ok(other) => (
VerdictStatus::Unknown,
format!("expr predicate evaluated to a non-boolean value: {other}"),
),
Err(error) => (
VerdictStatus::Unknown,
format!("expr predicate could not be evaluated: {error}"),
),
};
AssertionOutcomeRecord {
assert_id: assertion.assert_id.clone(),
result,
channel: None,
reason,
}
}
pub(crate) struct FoldedVerdict {
pub status: VerdictStatus,
pub degraded: bool,
pub summary: String,
}
pub(crate) fn fold_step_verdict(
outcomes: &[AssertionOutcomeRecord],
degraded_execution: bool,
degraded_verify: bool,
policy: VerdictPolicy,
) -> FoldedVerdict {
let (mut status, mut summary) = if let Some(fail) = outcomes
.iter()
.find(|outcome| outcome.result == VerdictStatus::Fail)
{
(
VerdictStatus::Fail,
format!("assertion '{}' failed: {}", fail.assert_id, fail.reason),
)
} else if let Some(unknown) = outcomes
.iter()
.find(|outcome| outcome.result == VerdictStatus::Unknown)
{
(
VerdictStatus::Unknown,
format!(
"assertion '{}' unknown: {}",
unknown.assert_id, unknown.reason
),
)
} else {
(
VerdictStatus::Pass,
format!("all {} assertion(s) passed", outcomes.len()),
)
};
if degraded_execution {
summary.push_str("; execution was degraded by the provider (unauthorized mode)");
}
if degraded_verify {
summary.push_str("; verification was degraded to a non-preferred channel");
}
let degraded = degraded_execution || degraded_verify;
if degraded && status == VerdictStatus::Pass && policy == VerdictPolicy::Strict {
status = VerdictStatus::Unknown;
summary.push_str("; strict verdict policy folds a degraded pass to unknown");
}
FoldedVerdict {
status,
degraded,
summary,
}
}
pub(crate) fn fold_flow_verdict(
step_verdicts: &[(VerdictStatus, bool)],
policy: VerdictPolicy,
) -> Option<FoldedVerdict> {
if step_verdicts.is_empty() {
return None;
}
let fails = count(step_verdicts, VerdictStatus::Fail);
let unknowns = count(step_verdicts, VerdictStatus::Unknown);
let passes = count(step_verdicts, VerdictStatus::Pass);
let degraded = step_verdicts.iter().any(|(_, degraded)| *degraded);
let mut status = if fails > 0 {
VerdictStatus::Fail
} else if unknowns > 0 {
VerdictStatus::Unknown
} else {
VerdictStatus::Pass
};
let mut summary = format!(
"flow verdict over {} judged step(s): {passes} pass, {fails} fail, {unknowns} unknown",
step_verdicts.len()
);
if degraded {
summary.push_str("; at least one verdict is degraded");
if status == VerdictStatus::Pass && policy == VerdictPolicy::Strict {
status = VerdictStatus::Unknown;
summary.push_str("; strict verdict policy folds a degraded pass to unknown");
}
}
Some(FoldedVerdict {
status,
degraded,
summary,
})
}
fn count(verdicts: &[(VerdictStatus, bool)], status: VerdictStatus) -> usize {
verdicts.iter().filter(|(s, _)| *s == status).count()
}