agentstatedeveloper_cli/commands/
verify_effects.rs1use 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 pub qname: String,
28
29 #[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 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 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 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 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}