pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! Distribution stream S2(d) — the staged protected-surface advisory (the
//! finding S1 carried: `check --staged` skips unmapped files before any
//! gate, so protected-surface changes ride through the pre-commit floor
//! silently while the write path denies).
//!
//! Deliberately an ADVISORY, not a deny: the floor runs for humans too,
//! and a human commit of `pushkin.toml` is legitimate — a staged deny
//! would block the manifest's own maintenance (and the write-path agent
//! deny plus this floor cannot tell the author apart). The notice is loud
//! on stderr, never changes the verdict, never claims "failing open", and
//! leaves `--json` stdout parseable.
//!
//! Committed first (RED), read-only hereafter (N10, product-gated).

use assert_cmd::Command;
use std::fs;
use std::path::Path;
use std::process::Command as StdCommand;

type TestResult = Result<(), Box<dyn std::error::Error>>;

const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"

[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]

[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"

[gates]
protected_paths = ["pushkin.toml"]
"#;

const CONTRACT: &str = "import { z } from \"zod\";\n\
                        export const UserCreateSchema = z.object({ name: z.string() });\n\
                        export type UserCreate = z.infer<typeof UserCreateSchema>;\n";

const COMPLIANT: &str = "import { UserCreateSchema } from \"../../../contracts/user.zod\";\n\n\
                         export async function POST(req: Request) {\n  \
                         const body = UserCreateSchema.parse(await req.json());\n  \
                         return Response.json(body);\n}\n";

fn git(root: &Path, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
    let status = StdCommand::new("git")
        .current_dir(root)
        .args(args)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()?;
    if !status.success() {
        return Err(format!("git {args:?} failed").into());
    }
    Ok(())
}

fn binary_dir() -> Result<String, Box<dyn std::error::Error>> {
    Ok(Path::new(env!("CARGO_BIN_EXE_pushkin"))
        .parent()
        .ok_or("binary under test has no parent")?
        .to_string_lossy()
        .into_owned())
}

/// A git repo with everything staged — including `pushkin.toml`, exactly
/// the shape every fresh `git add -A` produces.
fn staged_repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
    let dir = tempfile::tempdir()?;
    let root = dir.path();
    fs::write(root.join("pushkin.toml"), MANIFEST)?;
    fs::create_dir_all(root.join("contracts"))?;
    fs::create_dir_all(root.join("app/api/users"))?;
    fs::write(root.join("contracts/user.zod.ts"), CONTRACT)?;
    fs::write(root.join("app/api/users/route.ts"), COMPLIANT)?;
    git(root, &["init", "-q", "."])?;
    git(root, &["add", "-A"])?;
    Ok(dir)
}

fn check_staged(
    dir: &Path,
    json: bool,
) -> Result<(Option<i32>, String, String), Box<dyn std::error::Error>> {
    let mut args = vec!["check", "--staged"];
    if json {
        args.push("--json");
    }
    let output = Command::cargo_bin("pushkin")?
        .current_dir(dir)
        .env("PATH", format!("{}:/usr/bin:/bin", binary_dir()?))
        .env("HERMES_HOME", dir.join(".hermes-home"))
        .args(args)
        .output()?;
    Ok((
        output.status.code(),
        String::from_utf8_lossy(&output.stdout).into_owned(),
        String::from_utf8_lossy(&output.stderr).into_owned(),
    ))
}

#[test]
fn a_staged_protected_file_prints_the_advisory_without_blocking() -> TestResult {
    let dir = staged_repo()?;

    let (code, _, stderr) = check_staged(dir.path(), false)?;
    assert_eq!(
        code,
        Some(0),
        "the advisory never blocks — a human commit of the manifest is \
         legitimate: {stderr}"
    );
    assert!(
        stderr.contains("protected surface staged") && stderr.contains("pushkin.toml"),
        "the notice is loud and names the file: {stderr}"
    );
    assert!(
        !stderr.contains("failing open"),
        "a floor that RAN is never described as failing open: {stderr}"
    );
    Ok(())
}

#[test]
fn the_json_verdict_stays_parseable_beside_the_notice() -> TestResult {
    let dir = staged_repo()?;

    let (code, stdout, stderr) = check_staged(dir.path(), true)?;
    assert_eq!(code, Some(0), "advisory only: {stderr}");
    let verdict: serde_json::Value = serde_json::from_str(&stdout)?;
    assert_eq!(
        verdict["decision"], "allow",
        "the envelope is unchanged by the advisory: {stdout}"
    );
    assert!(
        stderr.contains("protected surface staged"),
        "the notice travels on stderr, never inside the envelope: {stderr}"
    );
    Ok(())
}

#[test]
fn no_staged_protected_file_means_no_notice() -> TestResult {
    let dir = staged_repo()?;
    git(
        dir.path(),
        &[
            "-c",
            "user.name=Notice Suite",
            "-c",
            "user.email=notice@test",
            "commit",
            "-qm",
            "fixture",
        ],
    )?;
    fs::write(
        dir.path().join("app/api/users/route.ts"),
        format!("{COMPLIANT}// touched\n"),
    )?;
    git(dir.path(), &["add", "app/api/users/route.ts"])?;

    let (code, _, stderr) = check_staged(dir.path(), false)?;
    assert_eq!(code, Some(0), "compliant route stages clean: {stderr}");
    assert!(
        !stderr.contains("protected surface staged"),
        "no protected file staged, no notice: {stderr}"
    );
    Ok(())
}