use clap::{Parser, Subcommand};
use joeira_core::{
avalia, prova, AmbienteMock, ClasseFalsoPositivo, Ponto, Predicado, Regra, Reversibilidade,
Severidade, Veredito,
};
#[derive(Parser)]
#[command(name = "joeira", about = "The typed git-hook control plane")]
struct Cli {
#[command(subcommand)]
comando: Comando,
}
#[derive(Subcommand)]
enum Comando {
Eval,
Prova,
Pontos,
}
fn main() -> anyhow::Result<()> {
match Cli::parse().comando {
Comando::Eval => cmd_eval(),
Comando::Prova => cmd_prova(),
Comando::Pontos => {
for p in Ponto::todos() {
println!(" {:<12} reads {:?}", p.arquivo(), p.leitura());
}
Ok(())
}
}
}
fn cmd_eval() -> anyhow::Result<()> {
let regras = corpus();
let amb = AmbienteMock::com_mensagem("init")
.com_stage("flake.lock", "a\n<<<<<<< HEAD\nb\n>>>>>>> other\n")
.com_head("app.conf", "listen = 0\n")
.com_stage("app.conf", "listen = 0\npassword: correcthorsebattery\n");
println!("corpus: {} rules", regras.len());
for r in ®ras {
let v = avalia(r, &amb);
let marca = match &v {
Veredito::Achado { .. } => "FOUND ",
Veredito::Limpo => "clean ",
Veredito::NaoSeAplica { .. } => "n/a ",
Veredito::Cego { .. } => "BLIND ",
};
println!(
" {marca} {:<28} floor={:?} ceiling={:?}{}",
r.nome(),
r.severidade(),
r.tecto(),
if v.gateia() { " [would gate]" } else { "" }
);
}
Ok(())
}
fn cmd_prova() -> anyhow::Result<()> {
let regras = corpus();
let (n, rows) = prova(®ras);
anyhow::ensure!(n > 0, "corpus is EMPTY — refusing to report green");
let vermelhas = rows.iter().filter(|r| !r.verde()).count();
for r in &rows {
println!(
" {} {:<28} refuses-its-block-witness={} passes-its-pass-witness={}",
if r.verde() { "ok " } else { "FAIL" },
r.regra,
r.bloqueia_ok,
r.passa_ok
);
}
println!("prova: {}/{n} rules green in both directions", n - vermelhas);
anyhow::ensure!(vermelhas == 0, "{vermelhas} of {n} rules are red");
Ok(())
}
fn corpus() -> Vec<Regra> {
let placeholders: Vec<String> = ["init", "update", "updates", "wip", "fix", "test"]
.iter()
.map(|s| (*s).to_owned())
.collect();
vec![
Regra::nova(
"msg-placeholder-subject",
Ponto::CommitMsg,
Predicado::Algum(vec![
Predicado::AssuntoNaLista {
lista: placeholders.clone(),
},
Predicado::AssuntoCaudaNaLista {
lista: placeholders,
},
]),
Reversibilidade::Custoso,
ClasseFalsoPositivo::TokenExato,
"placeholder subject — say what changed and why",
AmbienteMock::com_mensagem("init"),
AmbienteMock::com_mensagem("joeira: port the incumbent concerns"),
),
Regra::nova(
"msg-ai-attribution-trailer",
Ponto::CommitMsg,
Predicado::MensagemTemTrailer {
marcadores: vec!["Co-Authored-By:".into(), "Claude-Session:".into()],
},
Reversibilidade::Custoso,
ClasseFalsoPositivo::TokenExato,
"AI-attribution trailer present",
AmbienteMock::com_mensagem("feat: x\n\nClaude-Session: abc"),
AmbienteMock::com_mensagem("feat: x\n\nplain body"),
),
Regra::nova(
"vcs-conflict-markers",
Ponto::PreCommit,
Predicado::BlobTemParDeLinhas {
abre: "<<<<<<< ".into(),
fecha: ">>>>>>> ".into(),
},
Reversibilidade::Custoso,
ClasseFalsoPositivo::ZeroEstrutural,
"unresolved merge-conflict markers in the staged blob",
AmbienteMock::default().com_stage("flake.lock", "<<<<<<< HEAD\nx\n>>>>>>> them\n"),
AmbienteMock::default().com_stage("README.md", "# T\n\n=======\n"),
)
.com_piso(Severidade::Bloqueia),
Regra::nova(
"fresh-cargo-gen-lock",
Ponto::PreCommit,
Predicado::AmarraDeHash {
arquivo: "Cargo.lock".into(),
sidecar: "Cargo.gen.lock".into(),
campo: "cargo_lock_sha256".into(),
},
Reversibilidade::Custoso,
ClasseFalsoPositivo::ZeroEstrutural,
"Cargo.gen.lock records a different Cargo.lock than this commit contains",
AmbienteMock::default()
.com_stage("Cargo.lock", "l")
.com_stage("Cargo.gen.lock", r#"{"cargo_lock_sha256": "deadbeef"}"#)
.com_sha("Cargo.lock", "cafebabe"),
AmbienteMock::default()
.com_stage("Cargo.lock", "l")
.com_stage("Cargo.gen.lock", r#"{"cargo_lock_sha256": "cafebabe"}"#)
.com_sha("Cargo.lock", "cafebabe"),
)
.com_piso(Severidade::Bloqueia),
]
}