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        scope: Option<String>,
184    ) -> Result<String, String> {
185        AppService::pkg_remove(self, name, project_root, version, scope).await
186    }
187
188    async fn pkg_repair(
189        &self,
190        name: Option<String>,
191        project_root: Option<String>,
192    ) -> Result<String, String> {
193        AppService::pkg_repair(self, name, project_root).await
194    }
195
196    async fn pkg_doctor(
197        &self,
198        name: Option<String>,
199        project_root: Option<String>,
200    ) -> Result<String, String> {
201        AppService::pkg_doctor(self, name, project_root).await
202    }
203
204    // ─── Logging ─────────────────────────────────────────────
205
206    async fn add_note(
207        &self,
208        session_id: &str,
209        content: &str,
210        title: Option<&str>,
211    ) -> Result<String, String> {
212        AppService::add_note(self, session_id, content, title).await
213    }
214
215    async fn log_view(
216        &self,
217        session_id: Option<&str>,
218        limit: Option<usize>,
219        max_chars: Option<usize>,
220    ) -> Result<String, String> {
221        AppService::log_view(self, session_id, limit, max_chars).await
222    }
223
224    async fn stats(
225        &self,
226        strategy_filter: Option<&str>,
227        days: Option<u64>,
228    ) -> Result<String, String> {
229        AppService::stats(self, strategy_filter, days)
230    }
231
232    // ─── Project lifecycle ────────────────────────────────────
233
234    async fn init(&self, project_root: Option<String>) -> Result<String, String> {
235        AppService::init(self, project_root).await
236    }
237
238    async fn update(&self, project_root: Option<String>) -> Result<String, String> {
239        AppService::update(self, project_root).await
240    }
241
242    async fn migrate(&self, project_root: Option<String>) -> Result<String, String> {
243        AppService::migrate(self, project_root).await
244    }
245
246    // ─── Cards ───────────────────────────────────────────────
247
248    async fn card_list(&self, pkg: Option<String>) -> Result<String, String> {
249        AppService::card_list(self, pkg.as_deref())
250    }
251
252    async fn card_get(&self, card_id: &str) -> Result<String, String> {
253        AppService::card_get(self, card_id)
254    }
255
256    async fn card_find(
257        &self,
258        pkg: Option<String>,
259        where_: Option<serde_json::Value>,
260        order_by: Option<serde_json::Value>,
261        limit: Option<usize>,
262        offset: Option<usize>,
263    ) -> Result<String, String> {
264        AppService::card_find(self, pkg, where_, order_by, limit, offset)
265    }
266
267    async fn card_alias_list(&self, pkg: Option<String>) -> Result<String, String> {
268        AppService::card_alias_list(self, pkg.as_deref())
269    }
270
271    async fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
272        AppService::card_get_by_alias(self, name)
273    }
274
275    async fn card_alias_set(
276        &self,
277        name: &str,
278        card_id: &str,
279        pkg: Option<String>,
280        note: Option<String>,
281    ) -> Result<String, String> {
282        AppService::card_alias_set(self, name, card_id, pkg.as_deref(), note.as_deref())
283    }
284
285    async fn card_append(
286        &self,
287        card_id: &str,
288        fields: serde_json::Value,
289    ) -> Result<String, String> {
290        AppService::card_append(self, card_id, fields)
291    }
292
293    async fn card_install(&self, url: String) -> Result<String, String> {
294        AppService::card_install(self, url).await
295    }
296
297    async fn card_samples(
298        &self,
299        card_id: &str,
300        offset: Option<usize>,
301        limit: Option<usize>,
302        where_: Option<serde_json::Value>,
303    ) -> Result<String, String> {
304        AppService::card_samples(self, card_id, offset.unwrap_or(0), limit, where_)
305    }
306
307    async fn card_lineage(
308        &self,
309        card_id: &str,
310        direction: Option<String>,
311        depth: Option<usize>,
312        include_stats: Option<bool>,
313        relation_filter: Option<Vec<String>>,
314    ) -> Result<String, String> {
315        AppService::card_lineage(
316            self,
317            card_id,
318            direction.as_deref(),
319            depth,
320            include_stats,
321            relation_filter,
322        )
323    }
324
325    async fn card_sink_backfill(&self, sink: String, dry_run: bool) -> Result<String, String> {
326        AppService::card_sink_backfill(self, super::card::SinkBackfillParams { sink, dry_run })
327    }
328
329    // ─── Hub ─────────────────────────────────────────────────
330
331    async fn hub_reindex(
332        &self,
333        output_path: Option<String>,
334        source_dir: Option<String>,
335    ) -> Result<String, String> {
336        let svc = self.clone();
337        tokio::task::spawn_blocking(move || {
338            AppService::hub_reindex(&svc, output_path.as_deref(), source_dir.as_deref())
339        })
340        .await
341        .map_err(|e| format!("hub_reindex task panicked: {e}"))?
342    }
343
344    async fn hub_gendoc(
345        &self,
346        source_dir: String,
347        out_dir: Option<String>,
348        projections: Option<Vec<String>>,
349        config_path: Option<String>,
350        lint_strict: Option<bool>,
351    ) -> Result<String, String> {
352        let svc = self.clone();
353        tokio::task::spawn_blocking(move || {
354            crate::AppService::hub_gendoc(
355                &svc,
356                &source_dir,
357                out_dir.as_deref(),
358                projections.as_deref(),
359                config_path.as_deref(),
360                lint_strict,
361            )
362        })
363        .await
364        .map_err(|e| format!("hub_gendoc task panicked: {e}"))?
365    }
366
367    async fn hub_dist(
368        &self,
369        source_dir: String,
370        output_path: Option<String>,
371        out_dir: Option<String>,
372        preset: Option<String>,
373        project_root: Option<String>,
374        projections: Option<Vec<String>>,
375        config_path: Option<String>,
376        lint_strict: Option<bool>,
377    ) -> Result<String, String> {
378        let svc = self.clone();
379        tokio::task::spawn_blocking(move || {
380            AppService::hub_dist(
381                &svc,
382                &source_dir,
383                output_path.as_deref(),
384                out_dir.as_deref(),
385                preset.as_deref(),
386                project_root.as_deref(),
387                projections.as_deref(),
388                config_path.as_deref(),
389                lint_strict,
390            )
391        })
392        .await
393        .map_err(|e| format!("hub_dist task panicked: {e}"))?
394    }
395
396    async fn hub_info(&self, pkg: String) -> Result<String, String> {
397        let svc = self.clone();
398        tokio::task::spawn_blocking(move || AppService::hub_info(&svc, &pkg))
399            .await
400            .map_err(|e| format!("hub_info task panicked: {e}"))?
401    }
402
403    #[allow(clippy::too_many_arguments)]
404    async fn hub_search(
405        &self,
406        query: Option<String>,
407        category: Option<String>,
408        installed_only: Option<bool>,
409        limit: Option<i32>,
410        sort: Option<String>,
411        filter: Option<serde_json::Value>,
412        fields: Option<Vec<String>>,
413        verbose: Option<String>,
414    ) -> Result<String, String> {
415        let svc = self.clone();
416
417        // `filter` is a free-form JSON Value at the MCP boundary (so the
418        // trait stays core-crate-pure). If the caller sends something
419        // that is not a JSON object we treat it as "no filter" — the
420        // explicit category/installed_only params still cover the common
421        // cases. The MCP `JsonSchema` layer will have already flagged
422        // hard type errors. We log the drop so operators can diagnose
423        // unexpected filter shapes in production.
424        let filter_map = match filter {
425            None => None,
426            Some(v) => match serde_json::from_value::<HashMap<String, serde_json::Value>>(v) {
427                Ok(map) => Some(map),
428                Err(e) => {
429                    tracing::warn!(error = %e, "hub_search: filter value is not a JSON object — treating as no filter");
430                    None
431                }
432            },
433        };
434
435        // Negative limit values from MCP callers are clamped to 0 rather
436        // than wrapping to a huge usize (unchecked-user-bound-input pattern).
437        // Downstream semantics: `Some(0)` means "no limit" (return all) —
438        // the truncate path in `AppService::hub_search` short-circuits on 0.
439        let opts = ListOpts {
440            limit: limit.map(|n| n.max(0) as usize),
441            sort,
442            filter: filter_map,
443            fields,
444            verbose,
445        };
446
447        tokio::task::spawn_blocking(move || {
448            AppService::hub_search(
449                &svc,
450                query.as_deref(),
451                category.as_deref(),
452                installed_only,
453                opts,
454            )
455        })
456        .await
457        .map_err(|e| format!("hub_search task panicked: {e}"))?
458    }
459
460    // ─── Diagnostics ─────────────────────────────────────────
461
462    async fn info(&self) -> String {
463        AppService::info(self)
464    }
465}