pushkin-core 0.2.1

Core envelope, manifest, pipeline, and waiver types for the pushkin write-gate
Documentation
//! Mixed rule-id population after the sentinel→pushkin rename (remediation
//! pass 3, PART B2). Historical event rows and waiver records keep their
//! `sentinel.*` ids — the log is append-only — while new events carry
//! `pushkin.*`. Aggregation (stats/report/statusline) and waiver matching
//! must group the two spellings as one rule. Committed with its
//! implementation per the pass-1 red-locally/commit-green protocol;
//! read-only hereafter (charter N10).

use pushkin_core::envelope::{CheckResult, Decision, Severity, Violation};
use pushkin_core::events::{EventLog, Telemetry};
use pushkin_core::waivers::WaiverSet;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;

fn violation(rule: &str, file: &str) -> Violation {
    Violation {
        file: file.to_owned(),
        line: 1,
        rule: rule.to_owned(),
        contract: None,
        fix_hint: "fix".to_owned(),
        suggestions: Vec::new(),
        severity: Severity::Error,
    }
}

fn block(rule: &str, file: &str) -> CheckResult {
    CheckResult {
        decision: Decision::Block,
        violations: vec![violation(rule, file)],
        duration_ms: 0.0,
    }
}

fn now() -> Option<OffsetDateTime> {
    OffsetDateTime::parse("2026-08-14T00:00:00Z", &Rfc3339).ok()
}

/// A waiver file as `pushkin waive` would have written it BEFORE the
/// rename: the rule id carries the legacy prefix.
fn legacy_waiver_file(dir: &tempfile::TempDir, rule: &str) -> std::io::Result<std::path::PathBuf> {
    let path = dir.path().join("waivers.toml");
    let record = format!(
        "[[waivers]]\n\
         id = \"legacy-w1\"\n\
         rule = \"{rule}\"\n\
         path = \"app/api/**\"\n\
         reason = \"granted pre-rename\"\n\
         author = \"human\"\n\
         granted_at = \"2026-08-01T00:00:00Z\"\n\
         expires_at = \"2099-01-01T00:00:00Z\"\n"
    );
    std::fs::write(&path, record)?;
    Ok(path)
}

#[test]
fn pre_rename_waiver_suppresses_the_renamed_rule() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = legacy_waiver_file(&dir, "sentinel.suppression.new").unwrap();
    let set = WaiverSet::load(&path).expect("waiver file parses");
    let result = set.apply(
        block("pushkin.suppression.new", "app/api/users/route.ts"),
        now().unwrap(),
    );
    assert_eq!(
        result.decision,
        Decision::Allow,
        "a waiver granted pre-rename must suppress its matching deny post-rename"
    );
    assert!(result.violations.is_empty());
}

#[test]
fn pre_rename_protected_path_waiver_still_never_suppresses() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = legacy_waiver_file(&dir, "sentinel.protected_path").unwrap();
    let set = WaiverSet::load(&path).expect("waiver file parses");
    let result = set.apply(
        block("pushkin.protected_path", "app/api/users/route.ts"),
        now().unwrap(),
    );
    assert_eq!(
        result.decision,
        Decision::Block,
        "the protected-path rule is unwaivable in either spelling"
    );
}

/// Inserts a row the way a PRE-RENAME binary would have written it. Raw
/// SQL on purpose: the current API only emits `pushkin.*` ids.
fn insert_legacy_row(
    db: &std::path::Path,
    (session, seq): (&str, u64),
    decision: &str,
    rule: &str,
    payload: &str,
) -> rusqlite::Result<usize> {
    let conn = rusqlite::Connection::open(db)?;
    conn.execute(
        "INSERT INTO events (session, seq, ts, decision, rule, file, payload)
         VALUES (?1, ?2, '2026-08-01T00:00:00Z', ?3, ?4, 'x.ts', ?5)",
        (session, seq, decision, rule, payload),
    )
}

#[test]
fn stats_group_mixed_rule_spellings_as_one_rule() {
    let dir = tempfile::tempdir().expect("tempdir");
    let db = dir.path().join("events.db");
    let log = EventLog::open(&db).expect("log opens");
    let session = log.begin_session().expect("session begins");
    log.append(&session, &block("pushkin.board.claimed_path", "x.ts"))
        .expect("modern block appends");
    log.append_telemetry(
        &session,
        Telemetry {
            rule: "pushkin.compression",
            payload: r#"{"saved_chars": 5}"#.to_owned(),
        },
    )
    .expect("modern telemetry appends");
    insert_legacy_row(
        &db,
        ("old-sess", 1),
        "block",
        "sentinel.board.claimed_path",
        "{}",
    )
    .unwrap();
    insert_legacy_row(
        &db,
        ("old-sess", 2),
        "telemetry",
        "sentinel.compression",
        r#"{"saved_chars": 10}"#,
    )
    .unwrap();

    let stats = log.stats().expect("stats query");
    assert_eq!(
        stats.blocks_by_rule,
        vec![("pushkin.board.claimed_path".to_owned(), 2)],
        "one grouped row under the modern id, never two spellings"
    );
    assert_eq!(stats.compression_events, 2);
    assert_eq!(stats.compression_saved_chars, 15);
}

#[test]
fn decision_counts_exclude_legacy_escalation_rows() {
    let dir = tempfile::tempdir().expect("tempdir");
    let db = dir.path().join("events.db");
    let log = EventLog::open(&db).expect("log opens");
    let session = log.begin_session().expect("session begins");
    log.append(&session, &block("pushkin.board.claimed_path", "x.ts"))
        .expect("modern block appends");
    insert_legacy_row(
        &db,
        ("old-sess", 1),
        "block",
        "sentinel.escalation",
        r#"{"escalation": true}"#,
    )
    .unwrap();
    let (checks, denials) = log.decision_counts().expect("counts query");
    assert_eq!(
        (checks, denials),
        (1, 1),
        "legacy escalation marker rows must not inflate the statusline counts"
    );
}

#[test]
fn latest_session_escalation_recognizes_the_legacy_spelling() {
    let dir = tempfile::tempdir().expect("tempdir");
    let db = dir.path().join("events.db");
    let log = EventLog::open(&db).expect("log opens");
    insert_legacy_row(
        &db,
        ("old-sess", 1),
        "block",
        "sentinel.escalation",
        r#"{"escalation": true}"#,
    )
    .unwrap();
    assert!(
        log.latest_session_escalated()
            .expect("escalation query runs"),
        "an escalation recorded pre-rename is still an escalation"
    );
}