pushkin-core 0.2.0

Core envelope, manifest, pipeline, and waiver types for the pushkin write-gate
Documentation
//! Pipeline parity with the Phase 0 Bun spike (the reference implementation).
//! Same violations, same rule IDs, same decisions (Phase 1 test plan).

use pushkin_core::manifest::Manifest;
use pushkin_core::pipeline::{check_write, WriteRequest};

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

[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod", "pydantic", "rust", "sql"]

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

[gates]
suppression_comments = "deny"
protected_paths = ["pushkin.toml", ".claude/settings.json", "schemas/**"]
"#;

const NONCONFORMING: &str = r"export async function POST(req: Request) {
  const body = await req.json();
  return Response.json({ name: body.name });
}
";

const CONFORMING: &str = r#"import { UserCreateSchema } from "../../../contracts/user.zod";

export async function POST(req: Request) {
  const body = UserCreateSchema.parse(await req.json());
  return Response.json(body);
}
"#;

fn request(file_path: &str, content: &str) -> WriteRequest {
    WriteRequest {
        file_path: file_path.to_owned(),
        content: content.to_owned(),
    }
}

#[test]
fn blocks_unvalidated_handler_write() {
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("app/api/users/route.ts", NONCONFORMING),
    );
    assert_eq!(result.decision.as_str(), "block");
    let rules: Vec<&str> = result.violations.iter().map(|v| v.rule.as_str()).collect();
    assert!(rules.contains(&"contract.boundary.unvalidated_input"));
}

#[test]
fn allows_conforming_write() {
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("app/api/users/route.ts", CONFORMING),
    );
    assert_eq!(result.decision.as_str(), "allow");
    assert!(result.violations.is_empty());
}

#[test]
fn allows_unmapped_path_write() {
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("docs/notes.md", NONCONFORMING),
    );
    assert_eq!(result.decision.as_str(), "allow");
}

#[test]
fn blocks_new_suppression_comment() {
    let content = format!("// @ts-ignore\n{CONFORMING}");
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("app/api/users/route.ts", &content),
    );
    assert_eq!(result.decision.as_str(), "block");
    let rules: Vec<&str> = result.violations.iter().map(|v| v.rule.as_str()).collect();
    assert!(rules.contains(&"pushkin.suppression.new"));
}

#[test]
fn blocks_protected_path_write() {
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request(".claude/settings.json", "{}"),
    );
    assert_eq!(result.decision.as_str(), "block");
    let rules: Vec<&str> = result.violations.iter().map(|v| v.rule.as_str()).collect();
    assert!(rules.contains(&"pushkin.protected_path"));

    let schema = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("schemas/user.schema.json", "{}"),
    );
    assert_eq!(schema.decision.as_str(), "block");
}

#[test]
fn deny_reason_contains_fix_hint() {
    let result = check_write(
        &Manifest::parse(MANIFEST).unwrap(),
        &request("app/api/users/route.ts", NONCONFORMING),
    );
    let violation = result
        .violations
        .iter()
        .find(|v| v.rule == "contract.boundary.unvalidated_input")
        .unwrap();
    assert_eq!(violation.file, "app/api/users/route.ts");
    assert!(violation.line >= 1);
    assert_eq!(violation.contract.as_deref(), Some("user"));
    assert!(!violation.fix_hint.is_empty());
    assert!(!violation.suggestions.is_empty());
}