use anyhow::Result;
use clap::{Args, Subcommand};
use serde_json::json;
use agentstatedeveloper_core::{
AsgIndexStore, AsgLedgerStore, Author, AuthorKind, Engine, IndexStore, LedgerEntry, LedgerKind,
LedgerStore,
};
use crate::commands::{graph::build_id_map, ledger::open_engine_public};
use crate::config::Config;
#[derive(Debug, Subcommand)]
pub enum InvariantCmd {
Add(AddArgs),
List(ListArgs),
Rm(RmArgs),
}
#[derive(Debug, Args)]
pub struct AddArgs {
pub qname: String,
pub summary: String,
#[arg(long, default_value = "asd-cli-user")]
pub author_id: String,
}
#[derive(Debug, Args)]
pub struct ListArgs {
pub qname: Option<String>,
}
#[derive(Debug, Args)]
pub struct RmArgs {
pub entry_id: String,
#[arg(long, default_value = "asd-cli-user")]
pub author_id: String,
}
pub fn run(cfg: &Config, cmd: InvariantCmd) -> Result<()> {
match cmd {
InvariantCmd::Add(args) => add(cfg, args),
InvariantCmd::List(args) => list(cfg, args),
InvariantCmd::Rm(args) => rm(cfg, args),
}
}
fn add(cfg: &Config, args: AddArgs) -> Result<()> {
let engine = open_engine_public(cfg)?;
let index_store = AsgIndexStore::from_engine(&engine);
let symbol = index_store
.get_symbol_by_qname(&engine.ref_name, &args.qname)?
.ok_or_else(|| anyhow::anyhow!("symbol not found: {}", args.qname))?;
let author = Author {
kind: AuthorKind::Agent,
id: args.author_id.clone(),
};
let entry = LedgerEntry::new(
&symbol.symbol_id,
LedgerKind::Invariant,
args.summary.clone(),
author,
);
let ledger_store = AsgLedgerStore::from_engine(&engine);
ledger_store.append_entry(&engine.ref_name, &entry, &cfg.agent_id)?;
println!(
"{}",
serde_json::to_string_pretty(&json!({
"status": "added",
"entry_id": entry.entry_id,
"symbol_id": entry.symbol_id,
"qname": args.qname,
"summary": args.summary,
}))?
);
Ok(())
}
fn list(cfg: &Config, args: ListArgs) -> Result<()> {
let engine = Engine::open_sqlite(&cfg.db_path)?;
let id_map = build_id_map(&engine);
let sym_to_qname: std::collections::HashMap<&str, &str> = id_map
.iter()
.map(|(id, sym)| (id.as_str(), sym.qname.as_str()))
.collect();
let rows: Vec<serde_json::Value> = if let Some(ref qname) = args.qname {
let index_store = AsgIndexStore::from_engine(&engine);
let ledger_store = AsgLedgerStore::from_engine(&engine);
let symbol = index_store
.get_symbol_by_qname(&engine.ref_name, qname)?
.ok_or_else(|| anyhow::anyhow!("symbol not found: {qname}"))?;
ledger_store
.list_entries(&engine.ref_name, &symbol.symbol_id)
.unwrap_or_default()
.into_iter()
.filter(|e| e.kind == LedgerKind::Invariant)
.map(|e| entry_to_json(e, qname))
.collect()
} else {
let tree = match engine.repo.get_tree(&engine.ref_name, "/asd/v1/ledger") {
Ok(v) => v,
_ => serde_json::Value::Null,
};
let mut rows = Vec::new();
if let Some(sym_map) = tree.as_object() {
for per_symbol in sym_map.values() {
if let Some(entry_map) = per_symbol.as_object() {
for entry_val in entry_map.values() {
if let Ok(e) = serde_json::from_value::<LedgerEntry>(entry_val.clone()) {
if e.kind == LedgerKind::Invariant {
let qname = sym_to_qname
.get(e.symbol_id.as_str())
.copied()
.unwrap_or("");
rows.push(entry_to_json(e, qname));
}
}
}
}
}
}
rows.sort_by(|a, b| {
a.get("qname")
.and_then(serde_json::Value::as_str)
.cmp(&b.get("qname").and_then(serde_json::Value::as_str))
});
rows
};
println!(
"{}",
serde_json::to_string_pretty(&json!({ "invariants": rows }))?
);
Ok(())
}
fn rm(cfg: &Config, args: RmArgs) -> Result<()> {
let engine = open_engine_public(cfg)?;
let ledger_store = AsgLedgerStore::from_engine(&engine);
let outcome = ledger_store.withdraw_entry(
&engine.ref_name,
&args.entry_id,
&args.author_id,
&cfg.agent_id,
)?;
let status = if outcome.already_resolved {
"already-withdrawn"
} else {
"withdrawn"
};
println!(
"{}",
serde_json::to_string_pretty(&json!({
"status": status,
"entry_id": outcome.entry.entry_id,
"symbol_id": outcome.entry.symbol_id,
}))?
);
Ok(())
}
fn entry_to_json(entry: agentstatedeveloper_core::LedgerEntry, qname: &str) -> serde_json::Value {
json!({
"entry_id": entry.entry_id,
"qname": qname,
"symbol_id": entry.symbol_id,
"summary": entry.summary,
"author": { "kind": format!("{:?}", entry.author.kind), "id": entry.author.id },
"created_at": entry.created_at,
"tags": entry.tags,
})
}