use crate::declarations::{self, Declaration, Hook};
use crate::gate;
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 latest<'a>(gate: &str, steps: &'a [state::Step]) -> Option<&'a state::Step> {
steps.iter().rfind(|s| s.gate == gate)
}
fn settled(declaration: &Declaration, steps: &[state::Step]) -> bool {
latest(&declaration.gate, steps).is_some_and(|step| !step.blocked)
}
fn next<'a>(hook: &'a Hook, steps: &[state::Step]) -> Result<Option<&'a Declaration>, String> {
for declaration in &hook.gates {
if !applies(declaration)? || settled(declaration, steps) {
continue;
}
return Ok(Some(declaration));
}
Ok(None)
}
fn survey(hook: &Hook, steps: &[state::Step]) -> Result<Vec<(String, report::Standing)>, String> {
let mut board = Vec::new();
for declaration in &hook.gates {
let gate = declaration.gate.clone();
let standing = if !applies(declaration)? {
report::Standing::Skipped(declaration.paths.join(", "))
} else if let Some(step) = latest(&gate, steps) {
let counts = state::lookup(&step.token)?
.map(|record| trailer::total(&record.verdicts).render())
.unwrap_or_default();
if step.blocked {
report::Standing::Blocked(counts)
} else {
report::Standing::Passed(counts)
}
} else {
report::Standing::Waiting
};
board.push((gate, standing));
}
Ok(board)
}
fn session_of(step: &state::Step) -> Option<String> {
let record = state::lookup(&step.token).ok()??;
Some(record.verdicts.first()?.session.clone()).filter(|s| !s.is_empty())
}
fn prior_session(declaration: &Declaration, steps: &[state::Step]) -> Option<String> {
session_of(latest(&declaration.gate, steps)?)
}
fn review(
declaration: &Declaration,
agent: &crate::agent::Agent,
intent: &str,
prior: Option<String>,
) -> Result<(Vec<Verdict>, String), String> {
let system = crate::brief::system(declaration)?;
let prompt = match prior {
Some(_) => crate::brief::continuing(),
None => crate::brief::opening(intent),
};
let answer = agent.run(
crate::agent::Role::Review,
&system,
&prompt,
prior.as_deref(),
)?;
let verdicts = crate::runner::verdicts(&answer, declaration.brief.simple)?;
Ok((verdicts, crate::runner::findings(&answer.text)))
}
fn trailers(hook: &Hook, steps: &[state::Step]) -> Result<Vec<String>, String> {
let resets = state::resets()?;
let mut lines = Vec::new();
for declaration in &hook.gates {
if !settled(declaration, steps) || !applies(declaration)? {
continue;
}
let Some(step) = latest(&declaration.gate, steps) else {
continue;
};
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"));
}
if trailers.is_empty() {
return message;
}
message.push('\n');
message.push_str(&trailers.join("\n"));
message.push('\n');
message
}
fn land(hook: &Hook, steps: &[state::Step], intent: &str) -> Result<bool, String> {
let trailers = trailers(hook, steps)?;
report::gates(&survey(hook, steps)?);
let message = compose(intent, &trailers, &state::reasons()?);
let out = git::commit(&message)?;
report::committed(&trailers, &out);
Ok(true)
}
pub fn run(asked: Option<&str>) -> Result<bool, String> {
let hook = declarations::read()?;
let staged_machinery = gate::machinery_staged()?;
if !staged_machinery.is_empty() {
report::maintenance(&staged_machinery);
return Ok(false);
}
let steps = state::progress()?;
let recorded = state::intent()?;
let intent = match (asked, recorded.as_deref()) {
(None, Some(held)) => held.to_string(),
(Some(asked), None) => {
let judge = crate::runner::configured()?;
report::judging();
let answer = judge.run(
crate::agent::Role::JudgeIntent,
&crate::brief::judge_system(),
&crate::brief::judge_prompt(asked),
None,
)?;
crate::runner::judge(&answer, asked)?;
state::set_intent(asked)?;
asked.to_string()
}
(Some(_), Some(held)) => {
return Err(format!(
"this commit already states its aim, and it does not move:\n {held}\nattest takes no --intent after the first run."
))
}
(None, None) => {
return Err("attest needs --intent: no aim is recorded for this commit yet".to_string())
}
};
let intent = intent.as_str();
let Some(declaration) = next(&hook, &steps)? else {
return land(&hook, &steps, intent);
};
let agent = crate::runner::configured()?;
let prior = prior_session(declaration, &steps);
let (verdicts, findings) = review(declaration, &agent, intent, prior)?;
let blocked = verdicts.iter().any(Verdict::blocks);
state::record(&declaration.gate, &verdicts, blocked)?;
let after = state::progress()?;
let remaining = next(&hook, &after)?.map(|d| d.gate.clone());
report::reviewed(
&declaration.gate,
&verdicts,
blocked,
remaining.as_deref(),
&findings,
&survey(&hook, &after)?,
);
Ok(!blocked)
}
pub fn reset(reason: &str) -> Result<bool, String> {
let count = state::log_reset(reason)?;
report::reset_done(count, reason);
Ok(true)
}