Skip to main content

algocline_app/service/
engine_api_impl.rs

1use std::collections::HashMap;
2
3use algocline_core::{EngineApi, QueryResponse};
4use async_trait::async_trait;
5
6use super::list_opts::ListOpts;
7use super::AppService;
8
9/// Delegates each [`EngineApi`] method to the corresponding `AppService`
10/// inherent method via fully-qualified syntax (`AppService::method(self, …)`).
11///
12/// This avoids ambiguity between the trait method and the inherent method
13/// of the same name, preventing accidental infinite recursion if the
14/// inherent method is ever removed or renamed.
15#[async_trait]
16impl EngineApi for AppService {
17    // ─── Core execution ──────────────────────────────────────
18
19    async fn run(
20        &self,
21        code: Option<String>,
22        code_file: Option<String>,
23        ctx: Option<serde_json::Value>,
24        project_root: Option<String>,
25    ) -> Result<String, String> {
26        AppService::run(self, code, code_file, ctx, project_root).await
27    }
28
29    async fn advice(
30        &self,
31        strategy: &str,
32        task: Option<String>,
33        opts: Option<serde_json::Value>,
34        project_root: Option<String>,
35    ) -> Result<String, String> {
36        AppService::advice(self, strategy, task, opts, project_root).await
37    }
38
39    async fn continue_single(
40        &self,
41        session_id: &str,
42        response: String,
43        query_id: Option<&str>,
44        usage: Option<algocline_core::TokenUsage>,
45    ) -> Result<String, String> {
46        AppService::continue_single(self, session_id, response, query_id, usage).await
47    }
48
49    async fn continue_batch(
50        &self,
51        session_id: &str,
52        responses: Vec<QueryResponse>,
53    ) -> Result<String, String> {
54        AppService::continue_batch(self, session_id, responses).await
55    }
56
57    // ─── Session status ──────────────────────────────────────
58
59    async fn status(
60        &self,
61        session_id: Option<&str>,
62        pending_filter: Option<serde_json::Value>,
63    ) -> Result<String, String> {
64        AppService::status(self, session_id, pending_filter).await
65    }
66
67    // ─── Evaluation ──────────────────────────────────────────
68
69    async fn eval(
70        &self,
71        scenario: Option<String>,
72        scenario_file: Option<String>,
73        scenario_name: Option<String>,
74        strategy: &str,
75        strategy_opts: Option<serde_json::Value>,
76        auto_card: bool,
77    ) -> Result<String, String> {
78        AppService::eval(
79            self,
80            scenario,
81            scenario_file,
82            scenario_name,
83            strategy,
84            strategy_opts,
85            auto_card,
86        )
87        .await
88    }
89
90    async fn eval_history(&self, strategy: Option<&str>, limit: usize) -> Result<String, String> {
91        AppService::eval_history(self, strategy, limit)
92    }
93
94    async fn eval_detail(&self, eval_id: &str) -> Result<String, String> {
95        AppService::eval_detail(self, eval_id)
96    }
97
98    async fn eval_compare(&self, eval_id_a: &str, eval_id_b: &str) -> Result<String, String> {
99        AppService::eval_compare(self, eval_id_a, eval_id_b).await
100    }
101
102    // ─── Scenarios ───────────────────────────────────────────
103
104    async fn scenario_list(&self) -> Result<String, String> {
105        AppService::scenario_list(self)
106    }
107
108    async fn scenario_show(&self, name: &str) -> Result<String, String> {
109        AppService::scenario_show(self, name)
110    }
111
112    async fn scenario_install(&self, url: String) -> Result<String, String> {
113        AppService::scenario_install(self, url).await
114    }
115
116    // ─── Packages ────────────────────────────────────────────
117
118    async fn pkg_link(
119        &self,
120        path: String,
121        name: Option<String>,
122        force: Option<bool>,
123        scope: Option<String>,
124        project_root: Option<String>,
125    ) -> Result<String, String> {
126        AppService::pkg_link(self, path, name, force, scope, project_root).await
127    }
128
129    async fn pkg_unlink(&self, name: String) -> Result<String, String> {
130        AppService::pkg_unlink(self, name).await
131    }
132
133    #[allow(clippy::too_many_arguments)]
134    async fn pkg_list(
135        &self,
136        project_root: Option<String>,
137        limit: Option<i32>,
138        sort: Option<String>,
139        filter: Option<serde_json::Value>,
140        fields: Option<Vec<String>>,
141        verbose: Option<String>,
142    ) -> Result<String, String> {
143        // `filter` is a free-form JSON Value at the MCP boundary (so the
144        // trait stays core-crate-pure). If the caller sends something
145        // that is not a JSON object we treat it as "no filter" and log
146        // the drop so operators can diagnose unexpected filter shapes
147        // in production.
148        let filter_map = match filter {
149            None => None,
150            Some(v) => match serde_json::from_value::<HashMap<String, serde_json::Value>>(v) {
151                Ok(map) => Some(map),
152                Err(e) => {
153                    tracing::warn!(error = %e, "pkg_list: filter value is not a JSON object — treating as no filter");
154                    None
155                }
156            },
157        };
158
159        // Negative limit values from MCP callers are clamped to 0 rather
160        // than wrapping to a huge usize (unchecked-user-bound-input pattern).
161        // Downstream semantics: `Some(0)` means "no limit" (return all) —
162        // the truncate path in `AppService::pkg_list` short-circuits on 0.
163        let opts = ListOpts {
164            limit: limit.map(|n| n.max(0) as usize),
165            sort,
166            filter: filter_map,
167            fields,
168            verbose,
169        };
170
171        AppService::pkg_list(self, project_root, opts).await
172    }
173
174    async fn pkg_install(&self, url: String, name: Option<String>) -> Result<String, String> {
175        AppService::pkg_install(self, url, name).await
176    }
177
178    async fn pkg_remove(
179        &self,
180        name: &str,
181        project_root: Option<String>,
182        version: Option<String>,
183    ) -> Result<String, String> {
184        AppService::pkg_remove(self, name, project_root, version).await
185    }
186
187    async fn pkg_repair(
188        &self,
189        name: Option<String>,
190        project_root: Option<String>,
191    ) -> Result<String, String> {
192        AppService::pkg_repair(self, name, project_root).await
193    }
194
195    // ─── Logging ─────────────────────────────────────────────
196
197    async fn add_note(
198        &self,
199        session_id: &str,
200        content: &str,
201        title: Option<&str>,
202    ) -> Result<String, String> {
203        AppService::add_note(self, session_id, content, title).await
204    }
205
206    async fn log_view(
207        &self,
208        session_id: Option<&str>,
209        limit: Option<usize>,
210        max_chars: Option<usize>,
211    ) -> Result<String, String> {
212        AppService::log_view(self, session_id, limit, max_chars).await
213    }
214
215    async fn stats(
216        &self,
217        strategy_filter: Option<&str>,
218        days: Option<u64>,
219    ) -> Result<String, String> {
220        AppService::stats(self, strategy_filter, days)
221    }
222
223    // ─── Project lifecycle ────────────────────────────────────
224
225    async fn init(&self, project_root: Option<String>) -> Result<String, String> {
226        AppService::init(self, project_root).await
227    }
228
229    async fn update(&self, project_root: Option<String>) -> Result<String, String> {
230        AppService::update(self, project_root).await
231    }
232
233    async fn migrate(&self, project_root: Option<String>) -> Result<String, String> {
234        AppService::migrate(self, project_root).await
235    }
236
237    // ─── Cards ───────────────────────────────────────────────
238
239    async fn card_list(&self, pkg: Option<String>) -> Result<String, String> {
240        AppService::card_list(self, pkg.as_deref())
241    }
242
243    async fn card_get(&self, card_id: &str) -> Result<String, String> {
244        AppService::card_get(self, card_id)
245    }
246
247    async fn card_find(
248        &self,
249        pkg: Option<String>,
250        where_: Option<serde_json::Value>,
251        order_by: Option<serde_json::Value>,
252        limit: Option<usize>,
253        offset: Option<usize>,
254    ) -> Result<String, String> {
255        AppService::card_find(self, pkg, where_, order_by, limit, offset)
256    }
257
258    async fn card_alias_list(&self, pkg: Option<String>) -> Result<String, String> {
259        AppService::card_alias_list(self, pkg.as_deref())
260    }
261
262    async fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
263        AppService::card_get_by_alias(self, name)
264    }
265
266    async fn card_alias_set(
267        &self,
268        name: &str,
269        card_id: &str,
270        pkg: Option<String>,
271        note: Option<String>,
272    ) -> Result<String, String> {
273        AppService::card_alias_set(self, name, card_id, pkg.as_deref(), note.as_deref())
274    }
275
276    async fn card_append(
277        &self,
278        card_id: &str,
279        fields: serde_json::Value,
280    ) -> Result<String, String> {
281        AppService::card_append(self, card_id, fields)
282    }
283
284    async fn card_install(&self, url: String) -> Result<String, String> {
285        AppService::card_install(self, url).await
286    }
287
288    async fn card_samples(
289        &self,
290        card_id: &str,
291        offset: Option<usize>,
292        limit: Option<usize>,
293        where_: Option<serde_json::Value>,
294    ) -> Result<String, String> {
295        AppService::card_samples(self, card_id, offset.unwrap_or(0), limit, where_)
296    }
297
298    async fn card_lineage(
299        &self,
300        card_id: &str,
301        direction: Option<String>,
302        depth: Option<usize>,
303        include_stats: Option<bool>,
304        relation_filter: Option<Vec<String>>,
305    ) -> Result<String, String> {
306        AppService::card_lineage(
307            self,
308            card_id,
309            direction.as_deref(),
310            depth,
311            include_stats,
312            relation_filter,
313        )
314    }
315
316    async fn card_sink_backfill(&self, sink: String, dry_run: bool) -> Result<String, String> {
317        AppService::card_sink_backfill(self, super::card::SinkBackfillParams { sink, dry_run })
318    }
319
320    // ─── Hub ─────────────────────────────────────────────────
321
322    async fn hub_reindex(
323        &self,
324        output_path: Option<String>,
325        source_dir: Option<String>,
326    ) -> Result<String, String> {
327        let svc = self.clone();
328        tokio::task::spawn_blocking(move || {
329            AppService::hub_reindex(&svc, output_path.as_deref(), source_dir.as_deref())
330        })
331        .await
332        .map_err(|e| format!("hub_reindex task panicked: {e}"))?
333    }
334
335    async fn hub_info(&self, pkg: String) -> Result<String, String> {
336        let svc = self.clone();
337        tokio::task::spawn_blocking(move || AppService::hub_info(&svc, &pkg))
338            .await
339            .map_err(|e| format!("hub_info task panicked: {e}"))?
340    }
341
342    #[allow(clippy::too_many_arguments)]
343    async fn hub_search(
344        &self,
345        query: Option<String>,
346        category: Option<String>,
347        installed_only: Option<bool>,
348        limit: Option<i32>,
349        sort: Option<String>,
350        filter: Option<serde_json::Value>,
351        fields: Option<Vec<String>>,
352        verbose: Option<String>,
353    ) -> Result<String, String> {
354        let svc = self.clone();
355
356        // `filter` is a free-form JSON Value at the MCP boundary (so the
357        // trait stays core-crate-pure). If the caller sends something
358        // that is not a JSON object we treat it as "no filter" — the
359        // explicit category/installed_only params still cover the common
360        // cases. The MCP `JsonSchema` layer will have already flagged
361        // hard type errors. We log the drop so operators can diagnose
362        // unexpected filter shapes in production.
363        let filter_map = match filter {
364            None => None,
365            Some(v) => match serde_json::from_value::<HashMap<String, serde_json::Value>>(v) {
366                Ok(map) => Some(map),
367                Err(e) => {
368                    tracing::warn!(error = %e, "hub_search: filter value is not a JSON object — treating as no filter");
369                    None
370                }
371            },
372        };
373
374        // Negative limit values from MCP callers are clamped to 0 rather
375        // than wrapping to a huge usize (unchecked-user-bound-input pattern).
376        // Downstream semantics: `Some(0)` means "no limit" (return all) —
377        // the truncate path in `AppService::hub_search` short-circuits on 0.
378        let opts = ListOpts {
379            limit: limit.map(|n| n.max(0) as usize),
380            sort,
381            filter: filter_map,
382            fields,
383            verbose,
384        };
385
386        tokio::task::spawn_blocking(move || {
387            AppService::hub_search(
388                &svc,
389                query.as_deref(),
390                category.as_deref(),
391                installed_only,
392                opts,
393            )
394        })
395        .await
396        .map_err(|e| format!("hub_search task panicked: {e}"))?
397    }
398
399    // ─── Diagnostics ─────────────────────────────────────────
400
401    async fn info(&self) -> String {
402        AppService::info(self)
403    }
404}