Skip to main content

agentstatedeveloper_cli/commands/
verify_effects.rs

1//! `asd verify-effects <qname>` — compare declared effects against what the
2//! static checker infers from the current source, write mismatches back to
3//! the EffectDecl, and report verification status.
4//!
5//! Status values:
6//!   ok        — declared effects match inferred (or inferred is empty)
7//!   mismatch  — at least one declared effect not inferred, or vice versa
8//!   unverified — source file unreadable or no adapter for the language
9
10use anyhow::Result;
11use clap::Args;
12use serde_json::json;
13
14use agentstatedeveloper_adapters::default_adapters;
15use agentstatedeveloper_core::schema::{
16    EffectCategory, EffectDecl, Mismatch, Verification, VerificationSource, VerificationStatus,
17};
18use agentstatedeveloper_core::{
19    AsgEffectStore, AsgIndexStore, EffectStore, Engine, IndexStore, ParsedSymbol,
20};
21
22use crate::config::Config;
23
24#[derive(Debug, Args)]
25pub struct VerifyEffectsArgs {
26    /// Fully-qualified symbol name.
27    pub qname: String,
28
29    /// Write verification result back to the EffectDecl store.
30    #[arg(long)]
31    pub write: bool,
32}
33
34pub fn run(cfg: &Config, args: VerifyEffectsArgs) -> Result<()> {
35    let engine = Engine::open_sqlite(&cfg.db_path)?;
36    let index_store = AsgIndexStore::from_engine(&engine);
37    let effect_store = AsgEffectStore::from_engine(&engine);
38    let adapters = default_adapters();
39
40    let symbol = index_store
41        .get_symbol_by_qname(&engine.ref_name, &args.qname)?
42        .ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
43
44    let mut effect_decl = effect_store
45        .get_effects(&engine.ref_name, &symbol.symbol_id)?
46        .unwrap_or_else(|| EffectDecl {
47            symbol_id: symbol.symbol_id.clone(),
48            declared: Vec::new(),
49            transitive: Vec::new(),
50            verification: None,
51            confidence: None,
52            runtime: None,
53            matched_policy: None,
54        });
55
56    // Find an adapter for the symbol's language.
57    let adapter = adapters
58        .iter()
59        .find(|a| a.language() == symbol.language.as_str());
60
61    let (status, mismatches, inferred_strs) = if let Some(adapter) = adapter {
62        match std::fs::read_to_string(&symbol.file) {
63            Ok(source) => {
64                // Build a ParsedSymbol stub sufficient for infer_effects.
65                let stub = ParsedSymbol {
66                    qname: symbol.qname.clone(),
67                    kind: symbol.kind,
68                    start_line: symbol.start.line,
69                    start_col: symbol.start.col,
70                    end_line: symbol.end.line,
71                    end_col: symbol.end.col,
72                    body: String::new(),
73                    signature: symbol.signature.clone(),
74                    doc: symbol.doc.clone(),
75                };
76                let inferred: Vec<EffectCategory> = adapter
77                    .infer_effects(&source, &stub)
78                    .into_iter()
79                    .map(|e| e.effect)
80                    .collect();
81
82                let declared_cats: Vec<EffectCategory> = effect_decl
83                    .declared
84                    .iter()
85                    .map(|e| e.effect.clone())
86                    .collect();
87
88                let mut mismatches: Vec<Mismatch> = Vec::new();
89
90                // Declared but not inferred = possible over-declaration.
91                for cat in &declared_cats {
92                    if !inferred.contains(cat) {
93                        mismatches.push(Mismatch {
94                            kind: "declared_not_inferred".to_string(),
95                            effect: cat.clone(),
96                            detected_in: Some(symbol.file.clone()),
97                            note: Some("declared but not found by static checker".to_string()),
98                        });
99                    }
100                }
101
102                // Inferred but not declared = possible under-declaration.
103                for cat in &inferred {
104                    if !declared_cats.contains(cat) {
105                        mismatches.push(Mismatch {
106                            kind: "inferred_not_declared".to_string(),
107                            effect: cat.clone(),
108                            detected_in: Some(symbol.file.clone()),
109                            note: Some(
110                                "found by static checker but not in declared effects".to_string(),
111                            ),
112                        });
113                    }
114                }
115
116                let status = if mismatches.is_empty() {
117                    VerificationStatus::Ok
118                } else {
119                    VerificationStatus::Mismatch
120                };
121                let inferred_strs: Vec<String> =
122                    inferred.iter().map(|e| e.as_str().to_string()).collect();
123                (status, mismatches, inferred_strs)
124            }
125            Err(e) => {
126                eprintln!("asd: cannot read {}: {e}", symbol.file);
127                (VerificationStatus::Unverified, Vec::new(), Vec::new())
128            }
129        }
130    } else {
131        (VerificationStatus::Unverified, Vec::new(), Vec::new())
132    };
133
134    let verification = Verification {
135        by: VerificationSource::StaticChecker,
136        at: chrono::Utc::now(),
137        status,
138        mismatches: mismatches.clone(),
139    };
140
141    if args.write {
142        effect_decl.verification = Some(verification);
143        effect_store.put_effects(
144            &engine.ref_name,
145            &effect_decl.symbol_id,
146            &effect_decl,
147            "asd-verify-effects",
148        )?;
149    }
150
151    let out = json!({
152        "qname": symbol.qname,
153        "symbol_id": symbol.symbol_id,
154        "status": match status {
155            VerificationStatus::Ok => "ok",
156            VerificationStatus::Mismatch => "mismatch",
157            VerificationStatus::Unverified => "unverified",
158        },
159        "declared": effect_decl.declared.iter().map(|e| e.effect.as_str()).collect::<Vec<_>>(),
160        "inferred": inferred_strs,
161        "mismatches": mismatches.iter().map(|m| json!({
162            "kind": m.kind,
163            "effect": m.effect.as_str(),
164            "note": m.note,
165        })).collect::<Vec<_>>(),
166        "written": args.write,
167    });
168    println!("{}", serde_json::to_string_pretty(&out)?);
169    Ok(())
170}