use crate::declarations::{self, Declaration, Hook};
use crate::git;
use crate::report;
use crate::state;
use crate::trailer::{self, Verdict};
fn applies(declaration: &Declaration) -> Result<bool, String> {
Ok(!git::staged(&declaration.paths)?.is_empty())
}
fn next<'a>(hook: &'a Hook, steps: &[state::Step]) -> Result<Option<&'a Declaration>, String> {
let passed: Vec<&str> = steps
.iter()
.filter(|s| !s.blocked)
.map(|s| s.gate.as_str())
.collect();
for declaration in &hook.gates {
if passed.contains(&declaration.gate.as_str()) || !applies(declaration)? {
continue;
}
return Ok(Some(declaration));
}
Ok(None)
}
fn hold_intent(intent: &str, steps: &[state::Step]) -> Result<(), String> {
let sent_back = steps.last().is_some_and(|s| s.blocked);
if let Some(previous) = state::intent()? {
if previous != intent && !sent_back {
let detail = "the intent may only change after a MAJOR: state the same aim, or reset with a reason";
return Err(detail.to_string());
}
}
state::set_intent(intent)
}
fn review(
declaration: &Declaration,
runner: &crate::runner::Runner,
intent: &str,
) -> Result<(Vec<Verdict>, String), String> {
let files = git::staged_existing(&declaration.paths)?;
let brief = crate::brief::compose(declaration, Some(intent), &files)?;
report::reviewing(&declaration.gate);
let output = crate::runner::invoke(runner, &brief)?;
let verdicts = crate::runner::verdicts(&output, declaration.brief.simple)?;
Ok((verdicts, crate::runner::findings(&output)))
}
fn trailers(hook: &Hook, steps: &[state::Step]) -> Result<Vec<String>, String> {
let resets = state::resets()?;
let mut lines = Vec::new();
for step in steps.iter().filter(|s| !s.blocked) {
let Some(record) = state::lookup(&step.token)? else {
continue;
};
for verdict in record.verdicts {
let verdict = Verdict { resets, ..verdict };
lines.push(trailer::render(&record.gate, &verdict));
}
}
if lines.is_empty() {
if git::staged(&[])?.is_empty() {
return Err("nothing staged: nothing to review, nothing to commit".to_string());
}
return Err(format!(
"{} declared no gate this commit reaches",
hook.path
));
}
Ok(lines)
}
fn compose(intent: &str, trailers: &[String], resets: &[String]) -> String {
let mut message = format!("{intent}\n");
for reason in resets {
message.push_str(&format!("\nReset: {reason}\n"));
}
message.push('\n');
message.push_str(&trailers.join("\n"));
message.push('\n');
message
}
fn moved_since_review(hook: &Hook, steps: &[state::Step]) -> Result<Vec<String>, String> {
let mut moved = Vec::new();
for step in steps.iter().filter(|s| !s.blocked) {
let Some(declaration) = hook.gates.iter().find(|d| d.gate == step.gate) else {
continue;
};
if state::content_digest(&declaration.paths)? != step.content {
moved.push(step.gate.clone());
}
}
Ok(moved)
}
fn land(hook: &Hook, steps: &[state::Step], intent: &str) -> Result<bool, String> {
let trailers = trailers(hook, steps)?;
report::moved(&moved_since_review(hook, steps)?);
let message = compose(intent, &trailers, &state::reasons()?);
let out = git::commit(&message)?;
report::committed(&trailers, &out);
Ok(true)
}
pub fn run(intent: &str) -> Result<bool, String> {
let hook = declarations::read()?;
let docs: Vec<String> = hook.gates.iter().flat_map(|d| d.docs.clone()).collect();
let rubrics = crate::gate::staged_rubrics(&docs)?;
if !rubrics.is_empty() {
report::preflight(&rubrics);
return Ok(false);
}
let steps = state::progress()?;
hold_intent(intent, &steps)?;
let Some(declaration) = next(&hook, &steps)? else {
return land(&hook, &steps, intent);
};
let runner = crate::runner::configured()?;
let (verdicts, findings) = review(declaration, &runner, intent)?;
let blocked = !declaration.brief.simple && verdicts.iter().any(Verdict::blocks);
let content = state::content_digest(&declaration.paths)?;
state::record(&declaration.gate, &verdicts, blocked, &content)?;
let remaining = next(&hook, &state::progress()?)?.map(|d| d.gate.clone());
report::reviewed(
&declaration.gate,
&verdicts,
blocked,
remaining.as_deref(),
&findings,
);
Ok(!blocked)
}
pub fn reset(reason: &str) -> Result<bool, String> {
let count = state::log_reset(reason)?;
report::reset_done(count, reason);
Ok(true)
}