assay-cli 3.7.0

CLI for Assay
use crate::cli::args::SimSoakArgs;
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
pub(crate) struct SoakReportV1 {
    pub schema_version: String,
    pub report_version: u32,
    pub assay_version: String,
    pub run: RunInfo,
    pub trace: TraceInfo,
    pub target: TargetInfo,
    pub decision_policy: DecisionPolicy,
    pub summary: Summary,
    pub trials: Vec<TrialResult>,
    pub violations: Vec<RuleViolation>,
    pub canaries: Vec<CanarySignal>,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct RunInfo {
    pub run_id: String,
    pub started_at_utc: String,
    pub duration_seconds: u64,
    pub seed: u64,
    pub iterations: u32,
    pub time_budget_seconds: u64,
    pub evaluation_unit: String,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct TraceInfo {
    pub traceparent: String,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct TargetInfo {
    pub bundle_id: String,
    pub bundle_digest: String,
    pub pack_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct DecisionPolicy {
    pub name: String,
    pub thresholds: serde_json::Value,
    pub fail_mode: String,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct Summary {
    pub pass_all: bool,
    pub pass_rate: f64,
    pub ci_95: [f64; 2],
    pub dimensions: Dimensions,
    pub thresholds_used: serde_json::Value,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct Dimensions {
    pub correctness: DimensionStats,
    pub safety: DimensionStats,
    pub security: DimensionStats,
    pub control: DimensionStats,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct DimensionStats {
    pub pass_rate: f64,
    pub pass_count: u32,
    pub fail_count: u32,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct TrialResult {
    pub index: u32,
    pub seed: u64,
    pub outcome: String,
    pub duration_seconds: f64,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuleViolation {
    pub rule_id: String,
    pub dimension: String,
    pub count: u32,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct CanarySignal {
    pub name: String,
    pub status: String,
    pub trigger_count: u32,
}

pub(crate) fn build_report(assay_version: &str, args: &SimSoakArgs) -> SoakReportV1 {
    let seed = args.seed.unwrap_or(0);
    let iterations = args.iterations;

    let mut trials = Vec::with_capacity(iterations as usize);
    for i in 1..=iterations {
        let outcome = if seed > 0 && i % 10 == 0 {
            "fail"
        } else {
            "pass"
        };
        trials.push(TrialResult {
            index: i,
            seed,
            outcome: outcome.to_string(),
            duration_seconds: 1.0,
        });
    }

    let failed = trials.iter().filter(|t| t.outcome == "fail").count() as u32;
    let passed = iterations.saturating_sub(failed);
    let pass_rate = if iterations == 0 {
        0.0
    } else {
        passed as f64 / iterations as f64
    };

    let violations = if failed > 0 {
        vec![RuleViolation {
            rule_id: "soak.synthetic.failure".to_string(),
            dimension: "correctness".to_string(),
            count: failed,
        }]
    } else {
        Vec::new()
    };

    SoakReportV1 {
        schema_version: "soak-report-v1".to_string(),
        report_version: 1,
        assay_version: assay_version.to_string(),
        run: RunInfo {
            run_id: format!("soak-{}", seed),
            started_at_utc: "1970-01-01T00:00:00Z".to_string(),
            duration_seconds: iterations as u64,
            seed,
            iterations,
            time_budget_seconds: args.time_budget,
            evaluation_unit: "scenario".to_string(),
        },
        trace: TraceInfo {
            traceparent: "00-00000000000000000000000000000000-0000000000000000-00".to_string(),
        },
        target: TargetInfo {
            bundle_id: args.target.clone(),
            bundle_digest: "sha256:unknown".to_string(),
            pack_ids: vec!["default-pack".to_string()],
        },
        decision_policy: DecisionPolicy {
            name: "adr025-i1".to_string(),
            thresholds: serde_json::json!({ "min_pass_rate": 0.95 }),
            fail_mode: "enforce".to_string(),
        },
        summary: Summary {
            pass_all: failed == 0,
            pass_rate,
            ci_95: [pass_rate, pass_rate],
            dimensions: Dimensions {
                correctness: DimensionStats {
                    pass_rate,
                    pass_count: passed,
                    fail_count: failed,
                },
                safety: DimensionStats {
                    pass_rate,
                    pass_count: passed,
                    fail_count: failed,
                },
                security: DimensionStats {
                    pass_rate,
                    pass_count: passed,
                    fail_count: failed,
                },
                control: DimensionStats {
                    pass_rate,
                    pass_count: passed,
                    fail_count: failed,
                },
            },
            thresholds_used: serde_json::json!({ "min_pass_rate": 0.95 }),
        },
        trials,
        violations,
        canaries: vec![CanarySignal {
            name: "synthetic-baseline".to_string(),
            status: "pass".to_string(),
            trigger_count: 0,
        }],
    }
}

pub(crate) fn measurement_exceeded(report: &SoakReportV1) -> bool {
    report.run.duration_seconds > report.run.time_budget_seconds
}