use anyhow::Result;
use clap::Args;
use serde_json::json;
use agentstatedeveloper_core::{
AsgIndexStore, AsgLedgerStore, Engine, IndexStore, LedgerKind, LedgerStore, Symbol,
WorkflowSummary, append_workflow_session, compute_trust_score, detect_workflow,
schema::{Author, AuthorKind, LedgerEntry},
score_evidence_quality,
};
use crate::config::Config;
#[derive(Debug, Args)]
pub struct TaskCloseArgs {
pub proof: Option<String>,
#[arg(long)]
pub symbols: Option<String>,
#[arg(long)]
pub validated: bool,
#[arg(long)]
pub validation_note: Option<String>,
#[arg(long)]
pub evidence: Option<String>,
#[arg(long)]
pub plan: Option<String>,
#[arg(long)]
pub task: Option<String>,
#[arg(long, default_value = "asd-task-close")]
pub author: String,
#[arg(long)]
pub quiet: bool,
#[arg(long, default_value_t = true)]
pub json: bool,
}
pub fn run(cfg: &Config, args: TaskCloseArgs) -> Result<()> {
let engine = Engine::open_sqlite(&cfg.db_path)?;
let index_store = AsgIndexStore::from_engine(&engine);
let ledger_store = AsgLedgerStore::from_engine(&engine);
let plan_id = args
.plan
.clone()
.or_else(|| std::env::var("CTXONE_PLAN").ok())
.unwrap_or_default();
let task_id = args
.task
.clone()
.or_else(|| std::env::var("CTXONE_TASK").ok())
.unwrap_or_default();
let mut ctx_tags: Vec<String> = Vec::new();
if !plan_id.is_empty() {
ctx_tags.push(format!("ctx:plan:{}", plan_id));
}
if !task_id.is_empty() {
ctx_tags.push(format!("ctx:task:{}", task_id));
}
let target_symbols: Vec<Symbol> = if let Some(ref sym_list) = args.symbols {
sym_list
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter_map(|q| {
index_store
.get_symbol_by_qname(&engine.ref_name, q)
.ok()
.flatten()
})
.collect()
} else {
let out = std::process::Command::new("git")
.args(["diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD"])
.output()
.unwrap_or_else(|_| std::process::Output {
status: std::process::ExitStatus::default(),
stdout: vec![],
stderr: vec![],
});
let changed: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let tree = engine
.repo
.get_tree(&engine.ref_name, "/asd/v1/index/by-qname")
.unwrap_or(serde_json::Value::Object(Default::default()));
let mut syms: Vec<Symbol> = tree
.as_object()
.map(|m| {
m.values()
.filter_map(|v| serde_json::from_value::<Symbol>(v.clone()).ok())
.filter(|s| {
changed
.iter()
.any(|f| s.file.ends_with(f.as_str()) || s.file == *f)
})
.collect()
})
.unwrap_or_default();
syms.truncate(20);
syms
};
if target_symbols.is_empty() {
eprintln!("asd: no symbols resolved — pass --symbols or ensure HEAD has changed files");
println!(
"{}",
json!({"written": [], "ctx": {"plan": plan_id, "task": task_id}})
);
return Ok(());
}
let proof_base = args
.proof
.clone()
.unwrap_or_else(|| "task completed".to_string());
let proof_text = if let Some(ref ev) = args.evidence {
format!("{} [evidence: {}]", proof_base, ev)
} else {
proof_base.clone()
};
let author = Author {
kind: AuthorKind::Human,
id: args.author.clone(),
};
let mut written: Vec<serde_json::Value> = Vec::new();
let closed_at = chrono::Utc::now().to_rfc3339();
for sym in &target_symbols {
let mut proof_entry = LedgerEntry::new(
&sym.symbol_id,
LedgerKind::Proof,
&proof_text,
author.clone(),
);
proof_entry.tags.extend(ctx_tags.iter().cloned());
if let Some(ref ev) = args.evidence {
proof_entry.tags.push(format!("evidence:{}", ev));
}
ledger_store.append_entry(&engine.ref_name, &proof_entry, &args.author)?;
written.push(json!({"symbol": sym.qname, "kind": "proof", "summary": proof_text}));
if args.validated {
let validation_text = args
.validation_note
.clone()
.unwrap_or_else(|| format!("validated: {}", proof_base));
let mut vs_entry = LedgerEntry::new(
&sym.symbol_id,
LedgerKind::ValidationScenario,
&validation_text,
author.clone(),
);
vs_entry.tags.extend(ctx_tags.iter().cloned());
ledger_store.append_entry(&engine.ref_name, &vs_entry, &args.author)?;
written.push(json!({"symbol": sym.qname, "kind": "validation_scenario", "summary": validation_text}));
}
}
if !args.quiet {
eprintln!(
"asd: wrote {} ledger entries across {} symbols",
written.len(),
target_symbols.len()
);
if !ctx_tags.is_empty() {
eprintln!("asd: provenance: {}", ctx_tags.join(", "));
}
}
let pre_existing: Vec<LedgerEntry> = target_symbols
.iter()
.flat_map(|sym| {
ledger_store
.list_entries(&engine.ref_name, &sym.symbol_id)
.unwrap_or_default()
})
.filter(|e| {
!written.iter().any(|w| {
w.get("summary").and_then(|s| s.as_str()) == Some(e.summary.as_str())
&& w.get("kind")
.and_then(|k| k.as_str())
.map(|k| k == format!("{:?}", e.kind).to_lowercase())
.unwrap_or(false)
})
})
.collect();
let proof_was_explicit = args.proof.is_some();
let eq = score_evidence_quality(
&pre_existing,
args.validated,
args.evidence.as_deref(),
proof_was_explicit,
target_symbols.len(),
written.len(),
);
let has_invariants = pre_existing.iter().any(|e| e.kind == LedgerKind::Invariant);
let (wf_type, steps_detected, missing_steps) =
detect_workflow(&pre_existing, &eq, has_invariants);
let trust = compute_trust_score(&cfg.db_path);
let db_state = trust.data_quality.state.clone();
let db_state_note = match db_state.as_str() {
"clean_room" => "fresh workspace — low evidence score is expected before annotations are written".to_string(),
"unannotated" => "index built but no prior annotations — low evidence score is expected; run `asd annotate-commit` to enrich".to_string(),
"degraded" => "warning: sparse ledger despite prior activity — possible state loss or DB reset".to_string(),
_ => String::new(),
};
let workflow_summary = WorkflowSummary {
workflow_type: wf_type,
steps_detected,
missing_recommended_steps: missing_steps,
evidence_quality: eq,
task_id: task_id.clone(),
plan_id: plan_id.clone(),
closed_at: closed_at.clone(),
symbols_annotated: target_symbols.len(),
ledger_entries_written: written.len(),
db_state,
db_state_note,
};
append_workflow_session(&cfg.db_path, &workflow_summary);
let closure_summary = json!({
"status": "closed",
"closed_at": closed_at,
"proof": proof_text,
"validated": args.validated,
"evidence": args.evidence,
"symbols_annotated": target_symbols.len(),
"ledger_entries_written": written.len(),
"ctx": {
"plan": if plan_id.is_empty() { serde_json::Value::Null } else { json!(plan_id) },
"task": if task_id.is_empty() { serde_json::Value::Null } else { json!(task_id) },
},
"workflow": workflow_summary.to_json(),
"written": written,
});
if args.json {
println!("{}", serde_json::to_string_pretty(&closure_summary)?);
}
Ok(())
}