agentstatedeveloper_cli/commands/
invariant.rs1use anyhow::Result;
9use clap::{Args, Subcommand};
10use serde_json::json;
11
12use agentstatedeveloper_core::{
13 AsgIndexStore, AsgLedgerStore, Author, AuthorKind, Engine, IndexStore, LedgerEntry, LedgerKind,
14 LedgerStore,
15};
16
17use crate::commands::{graph::build_id_map, ledger::open_engine_public};
18use crate::config::Config;
19
20#[derive(Debug, Subcommand)]
21pub enum InvariantCmd {
22 Add(AddArgs),
24
25 List(ListArgs),
27
28 Rm(RmArgs),
30}
31
32#[derive(Debug, Args)]
33pub struct AddArgs {
34 pub qname: String,
36
37 pub summary: String,
39
40 #[arg(long, default_value = "asd-cli-user")]
42 pub author_id: String,
43}
44
45#[derive(Debug, Args)]
46pub struct ListArgs {
47 pub qname: Option<String>,
49}
50
51#[derive(Debug, Args)]
52pub struct RmArgs {
53 pub entry_id: String,
55
56 #[arg(long, default_value = "asd-cli-user")]
58 pub author_id: String,
59}
60
61pub fn run(cfg: &Config, cmd: InvariantCmd) -> Result<()> {
62 match cmd {
63 InvariantCmd::Add(args) => add(cfg, args),
64 InvariantCmd::List(args) => list(cfg, args),
65 InvariantCmd::Rm(args) => rm(cfg, args),
66 }
67}
68
69fn add(cfg: &Config, args: AddArgs) -> Result<()> {
70 let engine = open_engine_public(cfg)?;
71 let index_store = AsgIndexStore::from_engine(&engine);
72 let symbol = index_store
73 .get_symbol_by_qname(&engine.ref_name, &args.qname)?
74 .ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
75
76 let author = Author {
77 kind: AuthorKind::Agent,
78 id: args.author_id.clone(),
79 };
80 let entry = LedgerEntry::new(
81 &symbol.symbol_id,
82 LedgerKind::Invariant,
83 args.summary.clone(),
84 author,
85 );
86
87 let ledger_store = AsgLedgerStore::from_engine(&engine);
88 ledger_store.append_entry(&engine.ref_name, &entry, &cfg.agent_id)?;
89
90 println!(
91 "{}",
92 serde_json::to_string_pretty(&json!({
93 "status": "added",
94 "entry_id": entry.entry_id,
95 "symbol_id": entry.symbol_id,
96 "qname": args.qname,
97 "summary": args.summary,
98 }))?
99 );
100 Ok(())
101}
102
103fn list(cfg: &Config, args: ListArgs) -> Result<()> {
104 let engine = Engine::open_sqlite(&cfg.db_path)?;
105 let id_map = build_id_map(&engine);
106
107 let sym_to_qname: std::collections::HashMap<&str, &str> = id_map
109 .iter()
110 .map(|(id, sym)| (id.as_str(), sym.qname.as_str()))
111 .collect();
112
113 let rows: Vec<serde_json::Value> = if let Some(ref qname) = args.qname {
114 let index_store = AsgIndexStore::from_engine(&engine);
115 let ledger_store = AsgLedgerStore::from_engine(&engine);
116 let symbol = index_store
117 .get_symbol_by_qname(&engine.ref_name, qname)?
118 .ok_or_else(|| anyhow::anyhow!("symbol not found: {qname}"))?;
119 ledger_store
120 .list_entries(&engine.ref_name, &symbol.symbol_id)
121 .unwrap_or_default()
122 .into_iter()
123 .filter(|e| e.kind == LedgerKind::Invariant)
124 .map(|e| entry_to_json(e, qname))
125 .collect()
126 } else {
127 let tree = match engine.repo.get_tree(&engine.ref_name, "/asd/v1/ledger") {
129 Ok(v) => v,
130 _ => serde_json::Value::Null,
131 };
132 let mut rows = Vec::new();
133 if let Some(sym_map) = tree.as_object() {
134 for per_symbol in sym_map.values() {
135 if let Some(entry_map) = per_symbol.as_object() {
136 for entry_val in entry_map.values() {
137 if let Ok(e) = serde_json::from_value::<LedgerEntry>(entry_val.clone()) {
138 if e.kind == LedgerKind::Invariant {
139 let qname = sym_to_qname
140 .get(e.symbol_id.as_str())
141 .copied()
142 .unwrap_or("");
143 rows.push(entry_to_json(e, qname));
144 }
145 }
146 }
147 }
148 }
149 }
150 rows.sort_by(|a, b| {
151 a.get("qname")
152 .and_then(serde_json::Value::as_str)
153 .cmp(&b.get("qname").and_then(serde_json::Value::as_str))
154 });
155 rows
156 };
157
158 println!(
159 "{}",
160 serde_json::to_string_pretty(&json!({ "invariants": rows }))?
161 );
162 Ok(())
163}
164
165fn rm(cfg: &Config, args: RmArgs) -> Result<()> {
166 let engine = open_engine_public(cfg)?;
167 let ledger_store = AsgLedgerStore::from_engine(&engine);
168 let outcome = ledger_store.withdraw_entry(
169 &engine.ref_name,
170 &args.entry_id,
171 &args.author_id,
172 &cfg.agent_id,
173 )?;
174 let status = if outcome.already_resolved {
175 "already-withdrawn"
176 } else {
177 "withdrawn"
178 };
179 println!(
180 "{}",
181 serde_json::to_string_pretty(&json!({
182 "status": status,
183 "entry_id": outcome.entry.entry_id,
184 "symbol_id": outcome.entry.symbol_id,
185 }))?
186 );
187 Ok(())
188}
189
190fn entry_to_json(entry: agentstatedeveloper_core::LedgerEntry, qname: &str) -> serde_json::Value {
191 json!({
192 "entry_id": entry.entry_id,
193 "qname": qname,
194 "symbol_id": entry.symbol_id,
195 "summary": entry.summary,
196 "author": { "kind": format!("{:?}", entry.author.kind), "id": entry.author.id },
197 "created_at": entry.created_at,
198 "tags": entry.tags,
199 })
200}