mod gancho;
mod oraculo;
use clap::{Parser, Subcommand};
use joeira_core::{
AmbienteMock, Ponto, Predicado, Regra, Severidade, Veredito, Verificador, avalia, prova,
};
#[derive(Parser)]
#[command(name = "joeira", about = "The typed git-hook control plane")]
struct Cli {
#[command(subcommand)]
comando: Comando,
}
#[derive(Subcommand)]
enum Comando {
Eval,
Prova {
#[arg(long)]
all: bool,
},
Pontos,
PreCommit,
CommitMsg { arquivo: std::path::PathBuf },
PrePush { remoto: String, url: String },
Explain {
#[arg(long)]
staged: bool,
},
Status,
Oraculo {
#[arg(long, default_value = ".")]
repo: std::path::PathBuf,
#[arg(long, default_value_t = 500)]
n: usize,
},
}
fn main() -> anyhow::Result<()> {
match Cli::parse().comando {
Comando::Eval => cmd_eval(),
Comando::Prova { all } => cmd_prova(all),
Comando::Oraculo { repo, n } => {
let regras = corpus()?;
let linhas = oraculo::corre(&repo, n, ®ras)?;
oraculo::relata(&linhas)
}
Comando::PreCommit => gancho_com_corpus(gancho::pre_commit),
Comando::CommitMsg { arquivo } => gancho_com_corpus(|r| gancho::commit_msg(r, &arquivo)),
Comando::PrePush { remoto, url } => {
gancho_com_corpus(|r| gancho::pre_push(r, &remoto, &url))
}
Comando::Explain { staged } => gancho::explain(&corpus()?, staged),
Comando::Status => {
gancho::status(&corpus()?);
Ok(())
}
Comando::Pontos => {
for p in Ponto::todos() {
println!(" {:<12} reads {:?}", p.arquivo(), p.leitura());
}
Ok(())
}
}
}
fn gancho_com_corpus(corre: impl FnOnce(&[Regra])) -> anyhow::Result<()> {
if gancho::desligado() {
corre(&[]);
return Ok(());
}
match corpus() {
Ok(regras) => corre(®ras),
Err(e) => eprintln!("joeira: DEGRADED, the corpus did not build: {e:#} (exit 0)"),
}
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(all: bool) -> anyhow::Result<()> {
let regras = corpus()?;
if all {
let no_catalogo = joeira_lisp::ler_catalogo(joeira_lisp::CATALOGO)
.map_err(|e| anyhow::anyhow!("catalog: {e}"))?
.len();
println!(
"catalog: {no_catalogo} forms, corpus: {} rules",
regras.len()
);
anyhow::ensure!(
regras.len() == no_catalogo,
"corpus has {} rules but the catalog declares {no_catalogo} — refusing to report green over a subset",
regras.len()
);
}
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() -> anyhow::Result<Vec<Regra>> {
let formas = joeira_lisp::ler_catalogo(joeira_lisp::CATALOGO)
.map_err(|e| anyhow::anyhow!("the compiled-in catalog does not parse: {e}"))?;
anyhow::ensure!(
!formas.is_empty(),
"the catalog is EMPTY — refusing to build a corpus that proves nothing"
);
let mut regras = Vec::with_capacity(formas.len());
for f in &formas {
let t = testemunhas(f).ok_or_else(|| {
anyhow::anyhow!(
"catalog form {:?} has no witnesses in this binary — a rule that \
cannot be proven in both directions must not ship silently",
f.nome
)
})?;
let rev = f
.reversibilidade
.ok_or_else(|| anyhow::anyhow!("catalog form {:?} omits :reversibilidade", f.nome))?;
let fp =
f.fp.ok_or_else(|| anyhow::anyhow!("catalog form {:?} omits :fp", f.nome))?;
let mut r = Regra::nova(
&f.nome,
f.ponto,
t.predicado,
rev,
fp,
&f.mensagem,
t.bloqueia,
t.passa,
);
if let Some(piso) = t.piso {
r = r.com_piso(piso);
}
let nivel = f
.nivel
.ok_or_else(|| anyhow::anyhow!("catalog form {:?} omits :nivel", f.nome))?;
r = r.com_origem(nivel, f.gatilho.clone());
regras.push(r);
}
Ok(regras)
}
struct Testemunhas {
predicado: Predicado,
bloqueia: AmbienteMock,
passa: AmbienteMock,
piso: Option<Severidade>,
}
fn testemunhas(f: &joeira_lisp::FormaLida) -> Option<Testemunhas> {
Some(match f.nome.as_str() {
"msg-placeholder-subject" => Testemunhas {
predicado: Predicado::Algum(vec![
Predicado::AssuntoNaLista {
lista: f.lista.clone(),
},
Predicado::AssuntoCaudaNaLista {
lista: f.lista.clone(),
},
]),
bloqueia: AmbienteMock::com_mensagem("init"),
passa: AmbienteMock::com_mensagem("joeira: port the incumbent concerns"),
piso: None,
},
"msg-ai-attribution-trailer" => Testemunhas {
predicado: Predicado::MensagemTemTrailer {
marcadores: vec!["Co-Authored-By:".into(), "Claude-Session:".into()],
},
bloqueia: AmbienteMock::com_mensagem("feat: x\n\nClaude-Session: abc"),
passa: AmbienteMock::com_mensagem("feat: x\n\nplain body"),
piso: None,
},
"vcs-conflict-markers" => Testemunhas {
predicado: Predicado::BlobTemParDeLinhas {
abre: "<<<<<<< ".into(),
fecha: ">>>>>>> ".into(),
},
bloqueia: AmbienteMock::default()
.com_stage("flake.lock", "<<<<<<< HEAD\nx\n>>>>>>> them\n"),
passa: AmbienteMock::default().com_stage("README.md", "# T\n\n=======\n"),
piso: Some(Severidade::Bloqueia),
},
"gen-lock-tie" => Testemunhas {
predicado: Predicado::AmarraDeHash {
arquivo: "Cargo.lock".into(),
sidecar: "Cargo.gen.lock".into(),
campo: "cargo_lock_sha256".into(),
},
bloqueia: AmbienteMock::default()
.com_stage("Cargo.lock", "l")
.com_stage("Cargo.gen.lock", r#"{"cargo_lock_sha256": "deadbeef"}"#)
.com_sha("Cargo.lock", "cafebabe"),
passa: AmbienteMock::default()
.com_stage("Cargo.lock", "l")
.com_stage("Cargo.gen.lock", r#"{"cargo_lock_sha256": "cafebabe"}"#)
.com_sha("Cargo.lock", "cafebabe"),
piso: Some(Severidade::Bloqueia),
},
"sec-plaintext-credential" => Testemunhas {
predicado: Predicado::LinhaAdicionadaCasa {
padroes: vec![
joeira_core::Padrao::novo(
"plaintext-credential",
r"(?i)pass(word|wd|phrase)\s*[:=]\s*[^:/\s<{$][^\s<{$]{7,}",
)
.ok()?,
joeira_core::Padrao::novo(
"plaintext-secret-assignment",
r"(?i)(^|[^A-Za-z0-9])((client|api|app|consumer)[_-])?secret([_-](key|token|access[_-]key))?\s*[:=]\s*[^:/\s<{$][^\s<{$]{15,}",
)
.ok()?,
],
},
bloqueia: AmbienteMock::default()
.com_stage("notes.txt", "password: correcthorsebattery\n"),
passa: AmbienteMock::default().com_stage(
"lib.rs",
"use cofre_secret::Secret::new;\nlet x = std::secret::y;\n",
),
piso: None,
},
"nix-flake-untracked" => Testemunhas {
predicado: Predicado::Nao(Box::new(Predicado::Rastreado {
caminho: "flake.nix".into(),
})),
bloqueia: AmbienteMock::default().com_arvore("flake.nix"),
passa: AmbienteMock::default().com_rastreado("flake.nix"),
piso: None,
},
"nix-flake-check" => externo(Verificador::NixFlakeCheck, "flake.nix"),
"nix-flake-lock-age" => {
let lock = r#"{"nodes":{"nixpkgs":{"locked":{"lastModified":1000000}},"root":{}}}"#;
Testemunhas {
predicado: Predicado::LockMaisVelhoQue {
arquivo: "flake.lock".into(),
dias: 30,
},
bloqueia: AmbienteMock::default()
.com_head("flake.lock", lock)
.com_agora(1_000_000 + 31 * 86_400),
passa: AmbienteMock::default()
.com_head("flake.lock", lock)
.com_agora(1_000_000 + 86_400),
piso: None,
}
}
"rust-cargo-metadata" => externo(Verificador::CargoMetadata, "Cargo.toml"),
"rust-cargo-fetch-locked" => externo(Verificador::CargoFetchLocked, "Cargo.lock"),
"rust-cargo-fmt" => externo(Verificador::CargoFmtCheck, "Cargo.toml"),
"blue-bluefile-confirm" => externo(Verificador::BlueBluefileConfirm, "Bluefile"),
"go-mod-tidy" => externo(Verificador::GoModTidyDiff, "go.mod"),
"go-vet" => externo(Verificador::GoVet, "go.mod"),
"ci-actionlint" => Testemunhas {
predicado: Predicado::Todos(vec![
Predicado::CaminhoEmStageSob {
prefixo: ".github/workflows/".into(),
},
Predicado::Externo {
verificador: Verificador::Actionlint,
},
]),
bloqueia: AmbienteMock::default()
.com_stage(".github/workflows/ci.yml", "on: push\n")
.com_execucao(Verificador::Actionlint, 1),
passa: AmbienteMock::default()
.com_stage(".github/workflows/ci.yml", "on: push\n")
.com_execucao(Verificador::Actionlint, 0),
piso: None,
},
"shell-dockerfile-new" => Testemunhas {
predicado: Predicado::CaminhoAdicionadoCasa {
padroes: vec![
joeira_core::Padrao::novo(
"shell-dockerfile-new",
r"^(Dockerfile|Containerfile)(\..+)?$|\.(Dockerfile|Containerfile)$",
)
.ok()?,
],
},
bloqueia: AmbienteMock::default().com_adicionado("svc/Dockerfile", "FROM x\n"),
passa: AmbienteMock::default()
.com_head("svc/Dockerfile", "FROM x\n")
.com_stage("svc/Dockerfile", "FROM y\n")
.com_adicionado("docs/dockerfiles.md", "x\n"),
piso: None,
},
"shell-script-beyond-glue" => Testemunhas {
predicado: Predicado::ScriptAlemDaCola {
sufixo: ".sh".into(),
linhas_cola: 3,
},
bloqueia: AmbienteMock::default()
.com_adicionado("run.sh", "#!/bin/sh\na\nb\nc\nd\n"),
passa: AmbienteMock::default()
.com_adicionado("run.sh", "#!/bin/sh\nexec tool \"$@\"\n")
.com_head("old.sh", "a\nb\nc\nd\ne\n")
.com_stage("old.sh", "a\nb\nc\nd\n"),
piso: None,
},
_ => return None,
})
}
fn externo(v: Verificador, gatilho: &str) -> Testemunhas {
Testemunhas {
predicado: Predicado::Externo { verificador: v },
bloqueia: AmbienteMock::default()
.com_arvore(gatilho)
.com_execucao(v, 1),
passa: AmbienteMock::default()
.com_arvore(gatilho)
.com_execucao(v, 0),
piso: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_catalog_and_the_witnesses_are_in_bijection() {
let formas = joeira_lisp::ler_catalogo(joeira_lisp::CATALOGO).expect("catalog parses");
let regras = corpus().expect("corpus builds");
assert_eq!(
regras.len(),
formas.len(),
"every catalog form must reach the corpus"
);
let arms = [
"msg-placeholder-subject",
"msg-ai-attribution-trailer",
"vcs-conflict-markers",
"gen-lock-tie",
"sec-plaintext-credential",
"nix-flake-untracked",
"nix-flake-check",
"nix-flake-lock-age",
"rust-cargo-metadata",
"rust-cargo-fetch-locked",
"rust-cargo-fmt",
"blue-bluefile-confirm",
"go-mod-tidy",
"go-vet",
"ci-actionlint",
"shell-dockerfile-new",
"shell-script-beyond-glue",
];
for a in arms {
assert!(
formas.iter().any(|f| f.nome == a),
"witness arm {a:?} has no catalog form — it proves nothing"
);
}
assert_eq!(arms.len(), formas.len(), "no arm is unreachable");
}
#[test]
fn the_placeholder_rule_carries_all_77_words() {
let formas = joeira_lisp::ler_catalogo(joeira_lisp::CATALOGO).expect("parses");
let msg = formas
.iter()
.find(|f| f.nome == "msg-placeholder-subject")
.expect("present");
assert_eq!(msg.lista.len(), 77);
let t = testemunhas(msg).expect("witnessed");
match t.predicado {
Predicado::Algum(ref arms) => {
assert_eq!(arms.len(), 2, "whole-subject AND tail-after-colon");
for a in arms {
match a {
Predicado::AssuntoNaLista { lista }
| Predicado::AssuntoCaudaNaLista { lista } => {
assert_eq!(lista.len(), 77, "both arms carry the full list");
}
outro => panic!("unexpected arm: {outro:?}"),
}
}
}
ref outro => panic!("expected Algum, got {outro:?}"),
}
}
}