pushkin 0.1.0

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! CLI verb implementations.

pub mod affected;
pub mod authoring;
pub mod board;
pub mod check;
pub mod compile;
pub mod daemon;
pub mod db;
pub mod doctor;
pub mod git;
pub mod hook;
pub mod init;
pub mod instructions;
pub mod policy;
pub mod report;
pub mod stats;
pub mod waive;

use anyhow::{Context, Result};
use pushkin_core::envelope::CheckResult;
use pushkin_core::manifest::Manifest;
use std::path::PathBuf;

pub const MANIFEST_FILE: &str = "pushkin.toml";
pub const EVENTS_DB: &str = ".pushkin/events.db";
pub const CONSENT_FILE: &str = ".pushkin/consent.json";
pub const CLAUDE_SETTINGS: &str = ".claude/settings.json";
// v2: the pushkin naming pass changed the consented footprint (filenames,
// marker, binary name) — pre-rename consent is re-prompted (rem. pass 3).
pub const CONSENT_VERSION: u32 = 2;
pub const PUSHKIN_MARKER: &str = "pushkin-v1";

pub fn load_manifest() -> Result<Manifest> {
    let text = std::fs::read_to_string(MANIFEST_FILE)
        .with_context(|| format!("cannot read {MANIFEST_FILE} in the current directory"))?;
    Manifest::parse(&text).context("manifest rejected")
}

pub fn events_db_path() -> Result<PathBuf> {
    let path = PathBuf::from(EVENTS_DB);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).context("cannot create .pushkin/")?;
    }
    Ok(path)
}

/// The CLI-owned half of the read-only-paths gate (S1c): appends the
/// `pushkin.read_only_path` violation when `path` falls under a
/// `[gates] read_only_paths` glob AND is committed — present in git HEAD,
/// which is N10's own boundary ("committed first, read-only hereafter").
/// A new file, and every edit while it stays uncommitted, passes
/// untouched: that is the RED-suite authoring window. Applied uniformly
/// to the single-write, staged, and hook (warm and cold) paths; the Stop
/// sweep is exempt by design — it inspects committed files at rest, not
/// writes.
#[must_use]
pub fn gate_read_only(manifest: &Manifest, mut result: CheckResult, path: &str) -> CheckResult {
    if manifest.is_read_only(path) && committed_in_head(path) {
        result
            .violations
            .push(pushkin_core::pipeline::read_only_violation(path));
        result.decision = pushkin_core::envelope::Decision::Block;
    }
    result
}

/// Positive evidence of committed-ness: `git cat-file -e HEAD:<path>`.
/// No git, no HEAD, or an untracked path all mean "not committed" — the
/// deny requires the file to actually be in history.
pub(crate) fn committed_in_head(path: &str) -> bool {
    std::process::Command::new("git")
        .args(["cat-file", "-e", &format!("HEAD:{path}")])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok_and(|status| status.success())
}

/// Waiver pass over a gate result (spec §7): unexpired scoped waivers
/// suppress their violations. A broken waivers file suppresses NOTHING —
/// the gate fails closed and doctor reports the parse error.
#[must_use]
pub fn apply_waivers(result: pushkin_core::envelope::CheckResult) -> CheckResult {
    match pushkin_core::waivers::WaiverSet::load(std::path::Path::new(waive::WAIVERS_FILE)) {
        Ok(set) => set.apply_now(result),
        Err(_) => result,
    }
}