pushkin 0.1.0

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! `pushkin report` + `pushkin statusline` (integration doc §8; §7
//! task-split rider): read-only consumers of the event log and waiver
//! file. Report: denials by rule, mean-time-to-compliance, active
//! waivers. Statusline: one compact segment; escalation state wins; no
//! event log = empty output, exit 0 — the one place silence-on-failure
//! is correct (a statusline must never break a prompt).

use anyhow::Result;
use pushkin_core::events::EventLog;
use pushkin_core::pipeline::RULE_UNVALIDATED_INPUT;
use pushkin_core::waivers::WaiverSet;
use std::path::Path;

use super::waive::WAIVERS_FILE;
use super::EVENTS_DB;

/// The manifest-level requirement a rule enforces, when one exists —
/// the report speaks the language humans wrote in `pushkin.toml`.
fn requirement_label(rule: &str) -> Option<&'static str> {
    (rule == RULE_UNVALIDATED_INPUT).then_some("boundary-validation")
}

pub fn run_report() -> Result<i32> {
    println!("pushkin report");
    println!("===============");

    if Path::new(EVENTS_DB).exists() {
        let log = EventLog::open(EVENTS_DB)?;
        let stats = log.stats()?;
        println!("\nDenials by rule:");
        if stats.blocks_by_rule.is_empty() {
            println!("  (none recorded)");
        }
        for (rule, count) in &stats.blocks_by_rule {
            match requirement_label(rule) {
                Some(req) => println!("  {count}  {rule} (requires: {req})"),
                None => println!("  {count}  {rule}"),
            }
        }
        match log.mean_attempts_to_compliance()? {
            Some(mean) => println!("\nMean-time-to-compliance: {mean:.1} attempts to fix"),
            None => println!("\nMean-time-to-compliance: no recovered denials yet"),
        }
    } else {
        println!("\nNo event log yet — no gate decisions recorded.");
    }

    let waivers = WaiverSet::load(Path::new(WAIVERS_FILE))?;
    let active = waivers.active_now();
    println!("\nActive waivers:");
    if active.is_empty() {
        println!("  (none)");
    }
    for waiver in active {
        println!(
            "  {} on {} until {}{} (by {})",
            waiver.rule, waiver.path, waiver.expires_at, waiver.reason, waiver.author
        );
    }
    Ok(0)
}

pub fn run_statusline() -> i32 {
    // No log = no segment: print nothing and exit clean. A statusline
    // helper that errors would corrupt every prompt render.
    if !Path::new(EVENTS_DB).exists() {
        return 0;
    }
    let Ok(log) = EventLog::open(EVENTS_DB) else {
        return 0;
    };
    let (Ok((checks, denials)), Ok(escalated)) =
        (log.decision_counts(), log.latest_session_escalated())
    else {
        return 0;
    };

    let waiver_count =
        WaiverSet::load(Path::new(WAIVERS_FILE)).map_or(0, |set| set.active_now().len());

    let waived = if waiver_count > 0 {
        format!(", {waiver_count} waived")
    } else {
        String::new()
    };
    let escalation = if escalated {
        " — ESCALATED: human review requested"
    } else {
        ""
    };
    println!("⛨ pushkin: {checks} checks, {denials} denied{waived}{escalation}");
    0
}