Skip to main content

algocline_app/service/
card.rs

1//! Card service layer — MCP-facing read/write operations.
2//!
3//! Thin adapter between MCP tool handlers and [`algocline_engine::card`].
4//! All data flows through the engine; this layer handles JSON
5//! serialization for the MCP transport.
6
7use algocline_engine::card;
8
9use super::AppService;
10
11impl AppService {
12    /// List Cards as JSON summaries, optionally filtered by package.
13    pub fn card_list(&self, pkg: Option<&str>) -> Result<String, String> {
14        let rows = card::list(pkg)?;
15        Ok(card::summaries_to_json(&rows).to_string())
16    }
17
18    /// Fetch full Card body (Tier 1) by id.
19    pub fn card_get(&self, card_id: &str) -> Result<String, String> {
20        match card::get(card_id)? {
21            Some(v) => Ok(v.to_string()),
22            None => Err(format!("card '{card_id}' not found")),
23        }
24    }
25
26    /// Query Cards with sort, filter, and limit.
27    #[allow(clippy::too_many_arguments)]
28    pub fn card_find(
29        &self,
30        pkg: Option<String>,
31        scenario: Option<String>,
32        model: Option<String>,
33        sort: Option<String>,
34        limit: Option<usize>,
35        min_pass_rate: Option<f64>,
36    ) -> Result<String, String> {
37        let q = card::FindQuery {
38            pkg,
39            scenario,
40            model,
41            sort,
42            limit,
43            min_pass_rate,
44        };
45        let rows = card::find(q)?;
46        Ok(card::summaries_to_json(&rows).to_string())
47    }
48
49    /// Resolve alias then fetch the full Card.
50    pub fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
51        match card::get_by_alias(name)? {
52            Some(v) => Ok(v.to_string()),
53            None => Err(format!("alias '{name}' not found")),
54        }
55    }
56
57    /// List aliases, optionally filtered by package.
58    pub fn card_alias_list(&self, pkg: Option<&str>) -> Result<String, String> {
59        let rows = card::alias_list(pkg)?;
60        Ok(card::aliases_to_json(&rows).to_string())
61    }
62
63    /// Pin or rebind a mutable alias to a Card.
64    pub fn card_alias_set(
65        &self,
66        name: &str,
67        card_id: &str,
68        pkg: Option<&str>,
69        note: Option<&str>,
70    ) -> Result<String, String> {
71        let alias = card::alias_set(name, card_id, pkg, note)?;
72        let arr = card::aliases_to_json(std::slice::from_ref(&alias));
73        let single = arr
74            .as_array()
75            .and_then(|a| a.first().cloned())
76            .unwrap_or(serde_json::Value::Null);
77        Ok(single.to_string())
78    }
79
80    /// Additive-only annotation — new top-level keys only.
81    pub fn card_append(
82        &self,
83        card_id: &str,
84        fields: serde_json::Value,
85    ) -> Result<String, String> {
86        let merged = card::append(card_id, fields)?;
87        Ok(merged.to_string())
88    }
89
90    /// Read per-case sidecar rows (Tier 2) with offset/limit paging.
91    pub fn card_samples(
92        &self,
93        card_id: &str,
94        offset: usize,
95        limit: Option<usize>,
96    ) -> Result<String, String> {
97        let rows = card::read_samples(card_id, offset, limit)?;
98        Ok(serde_json::Value::Array(rows).to_string())
99    }
100}