pushkin 0.2.0

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! The mechanism `.claude/skills/pass-review/SKILL.md` §11 declares.
//!
//! The skill's frontmatter `metadata` block names every path, glob, rule id,
//! section number and finding id its body cites. Those declarations rot
//! quietly: a renamed file, a retired rule, or — the one §11.2 calls the
//! important one — a finding whose ledger status moves while the body still
//! argues from the old status. Nothing about the prose looks wrong when that
//! happens, which is why it needs a mechanism rather than a habit.
//!
//! Scope, deliberately narrowed — §11.5 requires saying so rather than letting
//! a check narrow in silence. The body-citation tests cover finding ids and
//! `.md` documents. They do NOT demand that every `.rs` file named in an
//! incident description be declared: prose such as `warm.rs:142` cites code as
//! evidence for a past incident, not as a dependency of the skill, and
//! declaring those would churn the block on every refactor. Test suites the
//! skill instructs you to read stay declared under `paths`, where the
//! existence check still catches a rename.
//!
//! Committed first, read-only hereafter (charter §4.1, N10).

use std::fs;
use std::path::{Path, PathBuf};

const SKILL: &str = ".claude/skills/pass-review/SKILL.md";
const LEDGER: &str = "DESIGN-FINDINGS.md";
const LEDGER_HEADING: &str = "## 18. F-Ledger";
const GUIDE: &str = "AGENT-INSTRUCTIONS.md";
const RULE_DOCS: [&str; 2] = [GUIDE, "docs/STANDING-RULES.md"];
const SECTION_DOCS: [&str; 3] = [GUIDE, "docs/STANDING-RULES.md", "docs/TESTING.md"];

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
}

fn read(relative: &str) -> Result<String, String> {
    fs::read_to_string(repo_root().join(relative)).map_err(|err| format!("{relative}: {err}"))
}

/// Frontmatter and body, split at the closing fence.
fn split_skill() -> Result<(String, String), String> {
    let text = read(SKILL)?;
    let lines: Vec<&str> = text.lines().collect();
    if lines.first().map(|line| line.trim()) != Some("---") {
        return Err(format!("{SKILL} does not open with a frontmatter fence"));
    }
    let close = lines
        .iter()
        .skip(1)
        .position(|line| line.trim() == "---")
        .ok_or_else(|| format!("{SKILL} frontmatter is never closed"))?;
    Ok((lines[1..=close].join("\n"), lines[close + 2..].join("\n")))
}

fn indent(line: &str) -> usize {
    line.len() - line.trim_start().len()
}

fn unquote(raw: &str) -> String {
    raw.trim()
        .trim_matches('"')
        .trim_matches('\'')
        .trim()
        .to_owned()
}

/// Values of a `metadata` list key, in either flow or block form.
fn declared(front: &str, key: &str) -> Vec<String> {
    let lines: Vec<&str> = front.lines().collect();
    let prefix = format!("{key}:");
    let Some(at) = lines
        .iter()
        .position(|line| line.trim_start().starts_with(&prefix))
    else {
        return Vec::new();
    };
    let tail = lines[at].trim_start()[prefix.len()..].trim();
    if tail.starts_with('[') {
        return tail
            .trim_matches('[')
            .trim_matches(']')
            .split(',')
            .map(unquote)
            .filter(|value| !value.is_empty())
            .collect();
    }
    let base = indent(lines[at]);
    lines[at + 1..]
        .iter()
        .take_while(|line| line.trim().is_empty() || indent(line) > base)
        .filter_map(|line| line.trim().strip_prefix("- ").map(unquote))
        .collect()
}

fn scalar(front: &str, key: &str) -> Option<String> {
    let prefix = format!("{key}:");
    front
        .lines()
        .find(|line| line.trim_start().starts_with(&prefix))
        .map(|line| unquote(&line.trim_start()[prefix.len()..]))
}

/// Every finding id the block declares, on any of its three lists.
fn declared_findings(front: &str) -> Vec<String> {
    let mut all = declared(front, "findings-cited-as-closed");
    all.extend(declared(front, "findings-cited-as-open"));
    all.extend(declared(front, "findings-cited-as-neutral"));
    all
}

fn ledger_status(ledger: &str, id: &str) -> Option<String> {
    let start = ledger.find(LEDGER_HEADING)?;
    let row = format!("| {id} |");
    ledger[start..]
        .lines()
        .find(|line| line.starts_with(&row))
        .and_then(|line| line.split('|').nth(4))
        .map(|cell| cell.trim().to_uppercase())
}

fn reads_closed(status: &str) -> bool {
    status.contains("FIXED") || status.contains("CLOSED")
}

fn is_finding_id(token: &str) -> bool {
    let Some(digits) = token.strip_prefix('F') else {
        return false;
    };
    digits.len() == 2 && digits.chars().all(|character| character.is_ascii_digit())
}

/// A section number resolves as a literal `§a.b` in any guide, or as item `b`
/// of the numbered list under `## a.` — §9's rules are a list, not headings.
fn section_resolves(section: &str) -> bool {
    for doc in SECTION_DOCS {
        if let Ok(text) = read(doc) {
            if text.contains(&format!("§{section}")) {
                return true;
            }
        }
    }
    numbered_item_resolves(section)
}

fn numbered_item_resolves(section: &str) -> bool {
    let Some((major, minor)) = section.split_once('.') else {
        return false;
    };
    let Ok(text) = read(GUIDE) else {
        return false;
    };
    let heading = format!("## {major}.");
    let item = format!("{minor}. ");
    text.lines()
        .skip_while(|line| !line.starts_with(&heading))
        .skip(1)
        .take_while(|line| !line.starts_with("## "))
        .any(|line| line.trim_start().starts_with(&item))
}

fn segment_matches(pattern: &str, name: &str) -> bool {
    match pattern.split_once('*') {
        None => pattern == name,
        Some((head, tail)) => {
            name.len() >= head.len() + tail.len() && name.starts_with(head) && name.ends_with(tail)
        }
    }
}

fn glob_matches(pattern: &str, candidate: &str) -> bool {
    let wanted: Vec<&str> = pattern.split('/').collect();
    let actual: Vec<&str> = candidate.split('/').collect();
    wanted.len() == actual.len()
        && wanted
            .iter()
            .zip(&actual)
            .all(|(left, right)| segment_matches(left, right))
}

fn walk(root: &Path, dir: &Path, out: &mut Vec<String>) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let raw = entry.file_name();
        let name = raw.to_string_lossy();
        if name == "target" || name == "node_modules" || name.starts_with('.') {
            continue;
        }
        let path = entry.path();
        if path.is_dir() {
            walk(root, &path, out);
        } else if let Ok(relative) = path.strip_prefix(root) {
            out.push(relative.to_string_lossy().replace('\\', "/"));
        }
    }
}

fn tokens(body: &str) -> Vec<String> {
    body.split(|character: char| !(character.is_ascii_alphanumeric() || "_./-".contains(character)))
        .map(str::to_owned)
        .collect()
}

#[test]
fn declared_paths_exist() {
    let (front, _) = split_skill().unwrap();
    let paths = declared(&front, "paths");
    assert!(!paths.is_empty(), "the block declares no paths at all");
    for path in &paths {
        assert!(
            repo_root().join(path).exists(),
            "the block declares `{path}`, which is not in the tree"
        );
    }
}

#[test]
fn declared_globs_match_at_least_one_file() {
    let (front, _) = split_skill().unwrap();
    let root = repo_root();
    for pattern in declared(&front, "globs") {
        assert!(
            !pattern.contains("**"),
            "`{pattern}` uses `**`; this matcher handles single-`*` segments only"
        );
        let head: Vec<&str> = pattern
            .split('/')
            .take_while(|part| !part.contains('*'))
            .collect();
        let mut found = Vec::new();
        walk(&root, &root.join(head.join("/")), &mut found);
        assert!(
            found
                .iter()
                .any(|candidate| glob_matches(&pattern, candidate)),
            "glob `{pattern}` matches nothing in the tree"
        );
    }
}

#[test]
fn declared_rules_are_defined_in_the_rule_documents() {
    let (front, _) = split_skill().unwrap();
    let mut corpus = String::new();
    for doc in RULE_DOCS {
        corpus.push_str(&read(doc).unwrap());
    }
    for rule in declared(&front, "rules") {
        assert!(
            corpus.contains(&rule),
            "rule `{rule}` is declared but defined in neither {RULE_DOCS:?}"
        );
    }
}

#[test]
fn declared_numbered_sections_resolve() {
    let (front, _) = split_skill().unwrap();
    let sections = declared(&front, "numbered-sections");
    assert!(!sections.is_empty(), "the block declares no sections");
    for section in sections {
        assert!(
            section_resolves(&section),
            "§{section} is declared but resolves in none of {SECTION_DOCS:?}"
        );
    }
}

#[test]
fn declared_findings_have_a_ledger_row() {
    let (front, _) = split_skill().unwrap();
    let ledger = read(LEDGER).unwrap();
    for id in declared_findings(&front) {
        assert!(
            ledger_status(&ledger, &id).is_some(),
            "`{id}` is declared but has no §18 ledger row"
        );
    }
}

#[test]
fn findings_cited_as_closed_read_closed_in_the_ledger() {
    let (front, _) = split_skill().unwrap();
    let ledger = read(LEDGER).unwrap();
    for id in declared(&front, "findings-cited-as-closed") {
        let status = ledger_status(&ledger, &id).unwrap_or_default();
        assert!(
            reads_closed(&status),
            "the skill treats `{id}` as closed; the ledger reads `{status}`"
        );
    }
}

#[test]
fn findings_cited_as_open_do_not_read_closed_in_the_ledger() {
    let (front, _) = split_skill().unwrap();
    let ledger = read(LEDGER).unwrap();
    for id in declared(&front, "findings-cited-as-open") {
        let status = ledger_status(&ledger, &id).unwrap_or_default();
        assert!(
            !reads_closed(&status),
            "the skill argues from `{id}` as open; the ledger reads `{status}` — the prose citing it is stale"
        );
    }
}

#[test]
fn every_finding_cited_in_the_body_is_declared() {
    let (front, body) = split_skill().unwrap();
    let known = declared_findings(&front);
    for token in tokens(&body) {
        if !is_finding_id(&token) {
            continue;
        }
        assert!(
            known.contains(&token),
            "the body cites `{token}`, which the block does not declare"
        );
    }
}

#[test]
fn every_document_cited_in_the_body_is_declared() {
    let (front, body) = split_skill().unwrap();
    let paths = declared(&front, "paths");
    for token in tokens(&body) {
        let is_markdown = Path::new(&token)
            .extension()
            .is_some_and(|extension| extension.eq_ignore_ascii_case("md"));
        if !is_markdown {
            continue;
        }
        let suffix = format!("/{token}");
        assert!(
            paths
                .iter()
                .any(|path| path == &token || path.ends_with(&suffix)),
            "the body cites `{token}`, which the block does not declare"
        );
    }
}

#[test]
fn floor_command_is_a_makefile_target() {
    let (front, _) = split_skill().unwrap();
    let command = scalar(&front, "floor-command").unwrap_or_default();
    let target = command.strip_prefix("make ").unwrap_or(&command).to_owned();
    let makefile = read("Makefile").unwrap();
    assert!(
        makefile
            .lines()
            .any(|line| line.starts_with(&format!("{target}:"))),
        "`{command}` is declared, but `{target}:` is not a Makefile target"
    );
}

#[test]
fn lint_invariants_hold_in_their_named_files() {
    let (front, _) = split_skill().unwrap();
    let invariants = declared(&front, "lint-invariants");
    assert!(
        !invariants.is_empty(),
        "the block declares no lint invariants"
    );
    for invariant in invariants {
        let split = invariant.split_once(": ");
        assert!(
            split.is_some(),
            "`{invariant}` is not in `<file>: <needle>` form"
        );
        let Some((file, needle)) = split else {
            continue;
        };
        let text = read(file).unwrap();
        assert!(
            text.contains(needle),
            "`{file}` no longer contains `{needle}` — §4's RED-commit rule rests on it"
        );
    }
}

#[test]
fn every_external_claim_carries_a_verification_date() {
    let (front, _) = split_skill().unwrap();
    let block: Vec<&str> = front
        .lines()
        .skip_while(|line| !line.trim_start().starts_with("external-claims:"))
        .collect();
    let claims = block
        .iter()
        .filter(|line| line.trim_start().starts_with("- claim:"))
        .count();
    let dates = block
        .iter()
        .filter(|line| line.trim_start().starts_with("verified:"))
        .count();
    assert!(claims > 0, "the block declares no external claims");
    assert_eq!(
        claims, dates,
        "§11.3 forbids an undated external claim: {claims} claim(s), {dates} date(s)"
    );
}