use std::io::Write as IoWrite;
use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::schema::{LedgerEntry, LedgerKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceQuality {
pub proof_has_commit: bool,
pub proof_has_tests: bool,
pub proof_has_files: bool,
pub proof_has_manual_validation: bool,
pub closure_summary_present: bool,
pub touched_symbols_annotated: bool,
pub evidence_quality_score: f64,
}
impl EvidenceQuality {
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowSummary {
pub workflow_type: String,
pub steps_detected: Vec<String>,
pub missing_recommended_steps: Vec<String>,
pub evidence_quality: EvidenceQuality,
pub task_id: String,
pub plan_id: String,
pub closed_at: String,
pub symbols_annotated: usize,
pub ledger_entries_written: usize,
#[serde(default)]
pub db_state: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub db_state_note: String,
}
impl WorkflowSummary {
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
}
pub fn score_evidence_quality(
pre_existing_entries: &[LedgerEntry],
was_validated: bool,
evidence_text: Option<&str>,
proof_was_explicit: bool,
touched_count: usize,
entries_written: usize,
) -> EvidenceQuality {
let proof_has_commit = pre_existing_entries
.iter()
.any(|e| e.tags.iter().any(|t| t.starts_with("commit:")));
let evidence_mentions_test = evidence_text
.map(|ev| {
let low = ev.to_lowercase();
low.contains("test") || low.contains("spec") || low.contains("assert")
})
.unwrap_or(false);
let has_existing_validation = pre_existing_entries
.iter()
.any(|e| e.kind == LedgerKind::ValidationScenario);
let proof_has_tests = was_validated || evidence_mentions_test || has_existing_validation;
let proof_has_files = touched_count > 0;
let proof_has_manual_validation = was_validated;
let closure_summary_present = proof_was_explicit;
let touched_symbols_annotated = entries_written > 0;
let bool_count = [
proof_has_commit,
proof_has_tests,
proof_has_files,
proof_has_manual_validation,
closure_summary_present,
touched_symbols_annotated,
]
.iter()
.filter(|&&b| b)
.count();
let evidence_quality_score = (bool_count as f64 / 6.0 * 100.0).round() / 100.0;
EvidenceQuality {
proof_has_commit,
proof_has_tests,
proof_has_files,
proof_has_manual_validation,
closure_summary_present,
touched_symbols_annotated,
evidence_quality_score,
}
}
pub fn detect_workflow(
pre_existing_entries: &[LedgerEntry],
eq: &EvidenceQuality,
has_invariants: bool,
) -> (String, Vec<String>, Vec<String>) {
let mut steps: Vec<String> = Vec::new();
let did_annotate = eq.proof_has_commit;
if did_annotate {
steps.push("annotate_commit".to_string());
}
let did_invariant = pre_existing_entries
.iter()
.any(|e| e.kind == LedgerKind::Invariant);
if did_invariant {
steps.push("invariant_checked".to_string());
}
let did_test = eq.proof_has_tests
&& (eq.proof_has_manual_validation
|| pre_existing_entries
.iter()
.any(|e| e.kind == LedgerKind::ValidationScenario));
if did_test {
steps.push("tested".to_string());
}
steps.push("task_closed".to_string());
let workflow_type = match (did_annotate, did_test) {
(true, true) => "full",
(true, false) => "annotate-and-close",
(false, true) => "test-and-close",
(false, false) => "close-only",
};
let mut missing: Vec<String> = Vec::new();
if !did_annotate {
missing.push("annotate_commit".to_string());
}
if !did_test {
missing.push("test_or_validate".to_string());
}
if has_invariants && !did_invariant {
missing.push("check_invariants".to_string());
}
(workflow_type.to_string(), steps, missing)
}
pub fn append_workflow_session(db_path: &Path, summary: &WorkflowSummary) {
let dot_asd = match db_path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.join(".asd"),
_ => return,
};
let _ = std::fs::create_dir_all(&dot_asd);
let sessions_path = dot_asd.join("workflow-sessions.jsonl");
let line = serde_json::to_string(&json!({
"closed_at": summary.closed_at,
"task_id": if summary.task_id.is_empty() { Value::Null } else { json!(summary.task_id) },
"plan_id": if summary.plan_id.is_empty() { Value::Null } else { json!(summary.plan_id) },
"workflow_type": summary.workflow_type,
"steps_detected": summary.steps_detected,
"missing_steps": summary.missing_recommended_steps,
"evidence_score": summary.evidence_quality.evidence_quality_score,
"symbols_annotated": summary.symbols_annotated,
"entries_written": summary.ledger_entries_written,
}));
let line = match line {
Ok(l) => format!("{l}\n"),
Err(_) => return,
};
let existing: Vec<String> = std::fs::read_to_string(&sessions_path)
.unwrap_or_default()
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.to_string())
.collect();
let mut combined: Vec<String> = existing;
combined.push(line.trim_end_matches('\n').to_string());
if combined.len() > 500 {
let excess = combined.len() - 500;
combined.drain(0..excess);
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&sessions_path)
{
let _ = f.write_all(combined.join("\n").as_bytes());
let _ = f.write_all(b"\n");
}
}