pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! `pushkin instructions`: gate instructions generated FROM the manifest so
//! prose can never drift from enforcement (addendum §6). `--for-subagent`
//! prints the compact variant for subagent system prompts (~150–200 words);
//! `--digest` prints the ≤2 KiB session-injection digest (spec §7.2): a
//! canonical behavioral preamble — replaceable per process via the
//! `PUSHKIN_INSTRUCTIONS` env-named file, else `.pushkin/instructions.md` —
//! followed by the manifest-derived facts block, which is always generated
//! and never truncated, so tuned wording can never drift from enforcement.

use anyhow::{Context, Result};
use pushkin_core::manifest::Manifest;

use super::load_manifest;

const DIGEST_CAP_BYTES: usize = 2048;

const CANONICAL_PREAMBLE: &str = "This repository is gated by Pushkin. \
Writes to gated paths are checked against the project contracts before they land; \
a denial names the rule and the exact fix — apply it and retry once. If the same \
write is blocked three times, STOP and report the blocker to the human instead of \
trying variations. Suppression comments are blocked on gated paths — fix the \
underlying issue. Waivers are human-only (`pushkin waive <rule>`). Run \
`pushkin instructions` for the full version.";

/// The `pushkin instructions` invocation: the full generated report, the
/// compact `--for-subagent` variant, or the capped `--digest`. Bundled so the
/// two mode flags never read as a bare `run(false, true)` at the call site.
#[derive(Clone, Copy)]
pub struct InstructionsArgs {
    pub for_subagent: bool,
    pub digest: bool,
}

pub fn run(args: InstructionsArgs) -> Result<i32> {
    let InstructionsArgs {
        for_subagent,
        digest,
    } = args;
    let manifest = load_manifest()?;

    if digest {
        print!("{}", render_digest(&manifest)?);
        return Ok(0);
    }

    let globs: Vec<&str> = manifest.mappings.iter().map(|m| m.glob.as_str()).collect();
    let contracts: Vec<&str> = manifest.contracts.iter().map(|c| c.name.as_str()).collect();
    let protected: Vec<&str> = manifest
        .gates
        .protected_paths
        .iter()
        .map(String::as_str)
        .collect();

    if for_subagent {
        println!(
            "This repository is gated by Pushkin. Writes to files matching \
             {globs} are checked against the project contracts ({contracts}) before they \
             land; a write that reads request input without parsing it through the \
             contract schema will be blocked, and the denial message tells you the exact \
             fix — apply it and retry once. Suppression comments (@ts-ignore, \
             eslint-disable, noqa) are blocked on gated paths: fix the underlying issue \
             instead. These paths are protected and must never be edited: {protected}. \
             If the same write is blocked three times you will be told to STOP — report \
             the blocker to the human instead of trying variations. Waivers exist but \
             only a human can grant them. Run `pushkin instructions` for the full \
             version, and `contract_show <name>` appears in denial messages when you \
             need a contract's shape.",
            globs = globs.join(", "),
            contracts = contracts.join(", "),
            protected = protected.join(", "),
        );
        return Ok(0);
    }

    println!("Pushkin — gate instructions (generated from pushkin.toml)");
    println!();
    println!("Gated paths (writes checked against contracts):");
    for mapping in &manifest.mappings {
        let names: Vec<&str> = mapping
            .contracts
            .iter()
            .map(pushkin_core::manifest::ContractName::as_str)
            .collect();
        println!(
            "  {}{} ({})",
            mapping.glob,
            names.join(", "),
            mapping.require.as_deref().unwrap_or("conformance")
        );
    }
    println!();
    println!("Protected paths (agent edits always blocked):");
    for path in &protected {
        println!("  {path}");
    }
    println!();
    println!("Suppression comments are denied on gated paths.");
    println!("Denials include the rule, the fix, and an attempt counter (cap 3, then STOP).");
    println!("Waivers: human-only, via `pushkin waive <rule>`.");
    Ok(0)
}

/// Preamble prose + facts block, capped at 2 KiB. The prose truncates to
/// fit; the facts block never does (a manifest whose facts alone exceed
/// the cap emits facts only — enforcement truth outranks the cap).
/// `pub(crate)`: the AGENTS.md managed block (init `agents-md` pack) is
/// rendered by this same function — one source of truth, literally.
pub(crate) fn render_digest(manifest: &Manifest) -> Result<String> {
    let prose = override_prose()?.unwrap_or_else(|| CANONICAL_PREAMBLE.to_owned());
    let facts = manifest_facts(manifest);
    let budget = DIGEST_CAP_BYTES.saturating_sub(facts.len() + 2);
    let mut preamble = prose.trim().to_owned();
    if preamble.len() > budget {
        let mut cut = budget;
        while cut > 0 && !preamble.is_char_boundary(cut) {
            cut -= 1;
        }
        preamble.truncate(cut);
    }
    Ok(format!("{preamble}\n\n{facts}"))
}

/// The per-process prose override (spec §7.2 channel 4): the
/// `PUSHKIN_INSTRUCTIONS` env-named file wins; else the repo-local
/// `.pushkin/instructions.md`; else `None` (canonical preamble).
fn override_prose() -> Result<Option<String>> {
    if let Ok(path) = std::env::var("PUSHKIN_INSTRUCTIONS") {
        let text = std::fs::read_to_string(&path)
            .with_context(|| format!("PUSHKIN_INSTRUCTIONS names an unreadable file: {path}"))?;
        return Ok(Some(text));
    }
    match std::fs::read_to_string(".pushkin/instructions.md") {
        Ok(text) => Ok(Some(text)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error).context("cannot read .pushkin/instructions.md"),
    }
}

/// The manifest-derived facts block — always generated, never overridden.
fn manifest_facts(manifest: &Manifest) -> String {
    use std::fmt::Write as _;

    let mut facts = String::from("Gated paths:\n");
    for mapping in &manifest.mappings {
        let names: Vec<&str> = mapping
            .contracts
            .iter()
            .map(pushkin_core::manifest::ContractName::as_str)
            .collect();
        writeln!(
            facts,
            "  {} -> {} ({})",
            mapping.glob,
            names.join(", "),
            mapping.require.as_deref().unwrap_or("conformance")
        )
        .ok();
    }
    facts.push_str("Protected (never edit): ");
    let protected: Vec<&str> = manifest
        .gates
        .protected_paths
        .iter()
        .map(String::as_str)
        .collect();
    facts.push_str(&protected.join(", "));
    facts.push('\n');
    facts
}