assay-core 3.9.1

High-performance evaluation framework for LLM agents (Core)
Documentation
use super::super::{
    consumer_contract::{project_consumer_contract, DECISION_CONSUMER_CONTRACT_VERSION_V1},
    context_contract::{project_context_contract, DECISION_CONTEXT_CONTRACT_VERSION_V1},
    deny_convergence::{project_deny_convergence, DENY_PRECEDENCE_VERSION_V1},
    outcome_convergence::classify_decision_outcome,
    replay_compat::{project_replay_compat, DECISION_BASIS_VERSION_V1},
};
use super::event_types::{
    Decision, DecisionData, FulfillmentDecisionPath, ObligationOutcome, ObligationOutcomeStatus,
};

const OUTCOME_STAGE_HANDLER: &str = "handler";
const OUTCOME_REASON_CODE_APPLIED: &str = "obligation_applied";
const OUTCOME_REASON_CODE_SKIPPED: &str = "obligation_skipped";
const OUTCOME_REASON_CODE_ERROR: &str = "obligation_error";
const OUTCOME_NORMALIZATION_VERSION_V1: &str = "v1";

fn normalize_obligation_outcome(mut outcome: ObligationOutcome) -> ObligationOutcome {
    if outcome.reason_code.is_none() {
        outcome.reason_code = Some(
            match outcome.status {
                ObligationOutcomeStatus::Applied => OUTCOME_REASON_CODE_APPLIED,
                ObligationOutcomeStatus::Skipped => OUTCOME_REASON_CODE_SKIPPED,
                ObligationOutcomeStatus::Error => OUTCOME_REASON_CODE_ERROR,
            }
            .to_string(),
        );
    }
    if outcome.enforcement_stage.is_none() {
        outcome.enforcement_stage = Some(OUTCOME_STAGE_HANDLER.to_string());
    }
    if outcome.normalization_version.is_none() {
        outcome.normalization_version = Some(OUTCOME_NORMALIZATION_VERSION_V1.to_string());
    }
    outcome
}

fn normalize_obligation_outcomes(outcomes: Vec<ObligationOutcome>) -> Vec<ObligationOutcome> {
    outcomes
        .into_iter()
        .map(normalize_obligation_outcome)
        .collect()
}

fn classify_fulfillment_decision_path(data: &DecisionData) -> FulfillmentDecisionPath {
    match data.decision {
        Decision::Allow => FulfillmentDecisionPath::PolicyAllow,
        Decision::Deny => {
            if data
                .fail_closed
                .as_ref()
                .map(|ctx| ctx.fail_closed_applied)
                .unwrap_or(false)
            {
                FulfillmentDecisionPath::FailClosedDeny
            } else {
                FulfillmentDecisionPath::PolicyDeny
            }
        }
        Decision::Error => FulfillmentDecisionPath::DecisionError,
    }
}

pub(crate) fn refresh_fulfillment_normalization(data: &mut DecisionData) {
    let outcomes = std::mem::take(&mut data.obligation_outcomes);
    data.obligation_outcomes = normalize_obligation_outcomes(outcomes);
    data.obligation_applied_present = Some(
        data.obligation_outcomes
            .iter()
            .any(|outcome| outcome.status == ObligationOutcomeStatus::Applied),
    );
    data.obligation_skipped_present = Some(
        data.obligation_outcomes
            .iter()
            .any(|outcome| outcome.status == ObligationOutcomeStatus::Skipped),
    );
    data.obligation_error_present = Some(
        data.obligation_outcomes
            .iter()
            .any(|outcome| outcome.status == ObligationOutcomeStatus::Error),
    );
    let outcome = classify_decision_outcome(
        data.decision,
        data.reason_code.as_str(),
        data.fail_closed
            .as_ref()
            .map(|ctx| ctx.fail_closed_applied)
            .unwrap_or(false),
        data.obligation_applied_present.unwrap_or(false),
        data.obligation_skipped_present.unwrap_or(false),
        data.obligation_error_present.unwrap_or(false),
    );
    data.decision_outcome_kind = Some(outcome.kind);
    data.decision_origin = Some(outcome.origin);
    data.outcome_compat_state = Some(outcome.compat_state);
    data.fulfillment_decision_path = Some(classify_fulfillment_decision_path(data));
    let fail_closed_applied = data
        .fail_closed
        .as_ref()
        .map(|ctx| ctx.fail_closed_applied)
        .unwrap_or(false);
    let replay_projection = project_replay_compat(
        data.decision_outcome_kind,
        data.decision_origin,
        data.outcome_compat_state,
        data.fulfillment_decision_path,
        data.decision,
    );
    let deny_projection = project_deny_convergence(
        data.decision_outcome_kind,
        data.decision_origin,
        data.fulfillment_decision_path,
        data.decision,
        fail_closed_applied,
        data.reason_code.as_str(),
    );
    data.decision_basis_version = Some(DECISION_BASIS_VERSION_V1.to_string());
    data.compat_fallback_applied = Some(replay_projection.compat_fallback_applied);
    data.classification_source = Some(replay_projection.classification_source);
    data.replay_diff_reason = Some(replay_projection.replay_diff_reason.to_string());
    data.legacy_shape_detected = Some(replay_projection.legacy_shape_detected);
    let consumer_projection = project_consumer_contract(
        data.decision_outcome_kind,
        data.decision_origin,
        data.fulfillment_decision_path,
        data.decision_basis_version.as_deref(),
        data.compat_fallback_applied,
        data.classification_source,
        data.legacy_shape_detected,
    );
    data.decision_consumer_contract_version =
        Some(DECISION_CONSUMER_CONTRACT_VERSION_V1.to_string());
    data.consumer_read_path = Some(consumer_projection.read_path);
    data.consumer_fallback_applied = Some(consumer_projection.fallback_applied);
    data.consumer_payload_state = Some(consumer_projection.payload_state);
    data.required_consumer_fields = consumer_projection.required_consumer_fields;
    let context_projection = project_context_contract(
        data.lane.as_deref(),
        data.principal.as_deref(),
        data.auth_context_summary.as_deref(),
        data.approval_state.as_deref(),
    );
    data.decision_context_contract_version = Some(DECISION_CONTEXT_CONTRACT_VERSION_V1.to_string());
    data.context_payload_state = Some(context_projection.payload_state);
    data.required_context_fields = context_projection.required_context_fields;
    data.missing_context_fields = context_projection.missing_context_fields;
    data.policy_deny = Some(deny_projection.policy_deny);
    data.fail_closed_deny = Some(deny_projection.fail_closed_deny);
    data.enforcement_deny = Some(deny_projection.enforcement_deny);
    data.deny_precedence_version = Some(DENY_PRECEDENCE_VERSION_V1.to_string());
    data.deny_classification_source = Some(deny_projection.classification_source);
    data.deny_legacy_fallback_applied = Some(deny_projection.legacy_fallback_applied);
    data.deny_convergence_reason = Some(deny_projection.deny_convergence_reason.to_string());
}

pub(crate) fn refresh_contract_projections(data: &mut DecisionData) {
    refresh_fulfillment_normalization(data);
}