1use std::collections::HashMap;
2use std::sync::Arc;
3
4use algocline_core::{EngineApi, QueryResponse};
5use algocline_engine::state::{ResetReport, StateError};
6use async_trait::async_trait;
7
8use super::list_opts::ListOpts;
9use super::AppService;
10
11#[async_trait]
18impl EngineApi for AppService {
19 async fn run(
22 &self,
23 code: Option<String>,
24 code_file: Option<String>,
25 ctx: Option<serde_json::Value>,
26 project_root: Option<String>,
27 host_mode: Option<bool>,
28 ) -> Result<String, String> {
29 AppService::run(self, code, code_file, ctx, project_root, host_mode).await
30 }
31
32 async fn advice(
33 &self,
34 strategy: &str,
35 task: Option<String>,
36 opts: Option<serde_json::Value>,
37 project_root: Option<String>,
38 ) -> Result<String, String> {
39 AppService::advice(self, strategy, task, opts, project_root).await
40 }
41
42 async fn continue_single(
43 &self,
44 session_id: &str,
45 response: String,
46 query_id: Option<&str>,
47 usage: Option<algocline_core::TokenUsage>,
48 ) -> Result<String, String> {
49 AppService::continue_single(self, session_id, response, query_id, usage).await
50 }
51
52 async fn continue_batch(
53 &self,
54 session_id: &str,
55 responses: Vec<QueryResponse>,
56 ) -> Result<String, String> {
57 AppService::continue_batch(self, session_id, responses).await
58 }
59
60 async fn status(
63 &self,
64 session_id: Option<&str>,
65 pending_filter: Option<serde_json::Value>,
66 include_history: bool,
67 ) -> Result<String, String> {
68 AppService::status(self, session_id, pending_filter, include_history).await
69 }
70
71 async fn eval(
74 &self,
75 scenario: Option<String>,
76 scenario_file: Option<String>,
77 scenario_name: Option<String>,
78 strategy: &str,
79 strategy_opts: Option<serde_json::Value>,
80 auto_card: bool,
81 ) -> Result<String, String> {
82 AppService::eval(
83 self,
84 scenario,
85 scenario_file,
86 scenario_name,
87 strategy,
88 strategy_opts,
89 auto_card,
90 )
91 .await
92 }
93
94 async fn eval_history(&self, strategy: Option<&str>, limit: usize) -> Result<String, String> {
95 AppService::eval_history(self, strategy, limit)
96 }
97
98 async fn eval_detail(&self, eval_id: &str) -> Result<String, String> {
99 AppService::eval_detail(self, eval_id)
100 }
101
102 async fn eval_compare(&self, eval_id_a: &str, eval_id_b: &str) -> Result<String, String> {
103 AppService::eval_compare(self, eval_id_a, eval_id_b).await
104 }
105
106 async fn scenario_list(&self) -> Result<String, String> {
109 AppService::scenario_list(self)
110 }
111
112 async fn scenario_show(&self, name: &str) -> Result<String, String> {
113 AppService::scenario_show(self, name)
114 }
115
116 async fn scenario_install(&self, url: String) -> Result<String, String> {
117 AppService::scenario_install(self, url).await
118 }
119
120 async fn pkg_link(
123 &self,
124 path: String,
125 name: Option<String>,
126 force: Option<bool>,
127 scope: Option<String>,
128 project_root: Option<String>,
129 ) -> Result<String, String> {
130 AppService::pkg_link(self, path, name, force, scope, project_root).await
131 }
132
133 async fn pkg_unlink(&self, name: String) -> Result<String, String> {
134 AppService::pkg_unlink(self, name).await
135 }
136
137 #[allow(clippy::too_many_arguments)]
138 async fn pkg_list(
139 &self,
140 project_root: Option<String>,
141 limit: Option<i32>,
142 sort: Option<String>,
143 filter: Option<serde_json::Value>,
144 fields: Option<Vec<String>>,
145 verbose: Option<String>,
146 ) -> Result<String, String> {
147 let filter_map = match filter {
153 None => None,
154 Some(v) => match serde_json::from_value::<HashMap<String, serde_json::Value>>(v) {
155 Ok(map) => Some(map),
156 Err(e) => {
157 tracing::warn!(error = %e, "pkg_list: filter value is not a JSON object — treating as no filter");
158 None
159 }
160 },
161 };
162
163 let opts = ListOpts {
168 limit: limit.map(|n| n.max(0) as usize),
169 sort,
170 filter: filter_map,
171 fields,
172 verbose,
173 };
174
175 AppService::pkg_list(self, project_root, opts)
176 .await
177 .map_err(|e| e.to_string())
178 }
179
180 async fn pkg_install(
181 &self,
182 url: String,
183 name: Option<String>,
184 force: Option<bool>,
185 ) -> Result<String, String> {
186 AppService::pkg_install(self, url, name, force).await
187 }
188
189 async fn pkg_remove(
190 &self,
191 name: &str,
192 project_root: Option<String>,
193 version: Option<String>,
194 scope: Option<String>,
195 ) -> Result<String, String> {
196 AppService::pkg_remove(self, name, project_root, version, scope).await
197 }
198
199 async fn pkg_repair(
200 &self,
201 name: Option<String>,
202 project_root: Option<String>,
203 ) -> Result<String, String> {
204 AppService::pkg_repair(self, name, project_root).await
205 }
206
207 async fn pkg_doctor(
208 &self,
209 name: Option<String>,
210 project_root: Option<String>,
211 ) -> Result<String, String> {
212 AppService::pkg_doctor(self, name, project_root).await
213 }
214
215 #[allow(clippy::too_many_arguments)]
219 async fn pkg_test(
220 &self,
221 pkg: Option<String>,
222 code_file: Option<String>,
223 code: Option<String>,
224 spec_dir: Option<String>,
225 filter: Option<String>,
226 search_paths: Option<Vec<String>>,
227 project_root: Option<String>,
228 auto_search_paths: Option<bool>,
229 ) -> Result<String, String> {
230 AppService::pkg_test(
231 self,
232 pkg,
233 code_file,
234 code,
235 spec_dir,
236 filter,
237 search_paths,
238 project_root,
239 auto_search_paths,
240 )
241 .await
242 }
243
244 async fn add_note(
247 &self,
248 session_id: &str,
249 content: &str,
250 title: Option<&str>,
251 ) -> Result<String, String> {
252 AppService::add_note(self, session_id, content, title).await
253 }
254
255 async fn log_view(
256 &self,
257 session_id: Option<&str>,
258 limit: Option<usize>,
259 max_chars: Option<usize>,
260 ) -> Result<String, String> {
261 AppService::log_view(self, session_id, limit, max_chars).await
262 }
263
264 async fn stats(
265 &self,
266 strategy_filter: Option<&str>,
267 days: Option<u64>,
268 ) -> Result<String, String> {
269 AppService::stats(self, strategy_filter, days)
270 }
271
272 async fn init(&self, project_root: Option<String>) -> Result<String, String> {
275 AppService::init(self, project_root).await
276 }
277
278 async fn update(&self, project_root: Option<String>) -> Result<String, String> {
279 AppService::update(self, project_root).await
280 }
281
282 async fn migrate(&self, project_root: Option<String>) -> Result<String, String> {
283 AppService::migrate(self, project_root).await
284 }
285
286 async fn session_new(
289 &self,
290 project_root: Option<String>,
291 mode: Option<String>,
292 ) -> Result<String, String> {
293 let session = self.activate_session(project_root.as_deref(), mode.as_deref())?;
294 let result = serde_json::json!({
295 "session_id": session.session_id,
296 "project_root": session
297 .project_root
298 .as_ref()
299 .map(|p| p.to_string_lossy().to_string()),
300 "mode": session.mode.as_str(),
301 });
302 serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
303 }
304
305 async fn card_list(&self, pkg: Option<String>) -> Result<String, String> {
308 AppService::card_list(self, pkg.as_deref())
309 }
310
311 async fn card_get(&self, card_id: &str) -> Result<String, String> {
312 AppService::card_get(self, card_id)
313 }
314
315 async fn card_find(
316 &self,
317 pkg: Option<String>,
318 where_: Option<serde_json::Value>,
319 order_by: Option<serde_json::Value>,
320 limit: Option<usize>,
321 offset: Option<usize>,
322 ) -> Result<String, String> {
323 AppService::card_find(self, pkg, where_, order_by, limit, offset)
324 }
325
326 async fn card_alias_list(&self, pkg: Option<String>) -> Result<String, String> {
327 AppService::card_alias_list(self, pkg.as_deref())
328 }
329
330 async fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
331 AppService::card_get_by_alias(self, name)
332 }
333
334 async fn card_alias_set(
335 &self,
336 name: &str,
337 card_id: &str,
338 pkg: Option<String>,
339 note: Option<String>,
340 ) -> Result<String, String> {
341 AppService::card_alias_set(self, name, card_id, pkg.as_deref(), note.as_deref())
342 }
343
344 async fn card_append(
345 &self,
346 card_id: &str,
347 fields: serde_json::Value,
348 ) -> Result<String, String> {
349 AppService::card_append(self, card_id, fields)
350 }
351
352 async fn card_install(&self, url: String) -> Result<String, String> {
353 AppService::card_install(self, url).await
354 }
355
356 async fn card_samples(
357 &self,
358 card_id: &str,
359 offset: Option<usize>,
360 limit: Option<usize>,
361 where_: Option<serde_json::Value>,
362 ) -> Result<String, String> {
363 AppService::card_samples(self, card_id, offset.unwrap_or(0), limit, where_)
364 }
365
366 async fn card_lineage(
367 &self,
368 card_id: &str,
369 direction: Option<String>,
370 depth: Option<usize>,
371 include_stats: Option<bool>,
372 relation_filter: Option<Vec<String>>,
373 ) -> Result<String, String> {
374 AppService::card_lineage(
375 self,
376 card_id,
377 direction.as_deref(),
378 depth,
379 include_stats,
380 relation_filter,
381 )
382 }
383
384 async fn card_sink_backfill(&self, sink: String, dry_run: bool) -> Result<String, String> {
385 AppService::card_sink_backfill(self, super::card::SinkBackfillParams { sink, dry_run })
386 }
387
388 async fn card_analyze(&self, card_id: &str, pkg: Option<String>) -> Result<String, String> {
389 AppService::card_analyze(self, card_id, pkg).await
390 }
391
392 async fn card_publish(
393 &self,
394 card_id: &str,
395 target_repo: &str,
396 commit_message: Option<&str>,
397 ) -> Result<String, String> {
398 AppService::card_publish(self, card_id, target_repo, commit_message).await
399 }
400
401 async fn hub_reindex(
404 &self,
405 output_path: Option<String>,
406 source_dir: Option<String>,
407 ) -> Result<String, String> {
408 self.hub_reindex(output_path.as_deref(), source_dir.as_deref())
409 .await
410 }
411
412 async fn hub_gendoc(
413 &self,
414 source_dir: String,
415 out_dir: Option<String>,
416 projections: Option<Vec<String>>,
417 config_path: Option<String>,
418 lint_strict: Option<bool>,
419 ) -> Result<String, String> {
420 let svc = self.clone();
421 tokio::task::spawn_blocking(move || {
422 crate::AppService::hub_gendoc(
423 &svc,
424 &source_dir,
425 out_dir.as_deref(),
426 projections.as_deref(),
427 config_path.as_deref(),
428 lint_strict,
429 )
430 })
431 .await
432 .map_err(|e| format!("hub_gendoc task panicked: {e}"))?
433 }
434
435 async fn hub_dist(
436 &self,
437 source_dir: String,
438 output_path: Option<String>,
439 out_dir: Option<String>,
440 preset: Option<String>,
441 project_root: Option<String>,
442 projections: Option<Vec<String>>,
443 config_path: Option<String>,
444 lint_strict: Option<bool>,
445 ) -> Result<String, String> {
446 self.hub_dist(
447 &source_dir,
448 output_path.as_deref(),
449 out_dir.as_deref(),
450 preset.as_deref(),
451 project_root.as_deref(),
452 projections.as_deref(),
453 config_path.as_deref(),
454 lint_strict,
455 )
456 .await
457 }
458
459 async fn hub_info(&self, pkg: String) -> Result<String, String> {
460 let svc = self.clone();
461 tokio::task::spawn_blocking(move || AppService::hub_info(&svc, &pkg))
462 .await
463 .map_err(|e| format!("hub_info task panicked: {e}"))?
464 }
465
466 #[allow(clippy::too_many_arguments)]
467 async fn hub_search(
468 &self,
469 query: Option<String>,
470 category: Option<String>,
471 installed_only: Option<bool>,
472 limit: Option<i32>,
473 sort: Option<String>,
474 filter: Option<serde_json::Value>,
475 fields: Option<Vec<String>>,
476 verbose: Option<String>,
477 local_indices: Option<Vec<String>>,
478 ) -> Result<String, String> {
479 let svc = self.clone();
480
481 let filter_map = match filter {
489 None => None,
490 Some(v) => match serde_json::from_value::<HashMap<String, serde_json::Value>>(v) {
491 Ok(map) => Some(map),
492 Err(e) => {
493 tracing::warn!(error = %e, "hub_search: filter value is not a JSON object — treating as no filter");
494 None
495 }
496 },
497 };
498
499 let opts = ListOpts {
504 limit: limit.map(|n| n.max(0) as usize),
505 sort,
506 filter: filter_map,
507 fields,
508 verbose,
509 };
510
511 tokio::task::spawn_blocking(move || {
512 AppService::hub_search(
513 &svc,
514 query.as_deref(),
515 category.as_deref(),
516 installed_only,
517 opts,
518 local_indices,
519 )
520 })
521 .await
522 .map_err(|e| format!("hub_search task panicked: {e}"))?
523 }
524
525 async fn pkg_read_init_lua(&self, name: &str) -> Result<String, String> {
528 AppService::pkg_read_init_lua(self, name, None)
529 }
530
531 async fn pkg_get_narrative_md(&self, name: &str) -> Result<Option<String>, String> {
532 AppService::pkg_get_narrative_md(self, name).await
533 }
534
535 async fn pkg_meta(&self, name: &str) -> Result<String, String> {
536 let filter = serde_json::json!({ "name": name });
537 let json_str = EngineApi::pkg_list(
538 self,
539 None,
540 None,
541 None,
542 Some(filter),
543 None,
544 Some("full".to_string()),
545 )
546 .await?;
547 let val: serde_json::Value = serde_json::from_str(&json_str)
548 .map_err(|e| format!("pkg_meta: failed to parse pkg_list response: {e}"))?;
549 let pkgs = val
550 .get("packages")
551 .and_then(|p| p.as_array())
552 .ok_or_else(|| "pkg_meta: pkg_list response missing 'packages' field".to_string())?;
553 if pkgs.is_empty() {
554 return Err(format!("pkg not found: {name}"));
555 }
556 serde_json::to_string(&pkgs[0]).map_err(|e| format!("pkg_meta: serialize entry: {e}"))
557 }
558
559 async fn pkg_scaffold(
562 &self,
563 name: String,
564 target_dir: Option<String>,
565 category: Option<String>,
566 description: Option<String>,
567 ) -> Result<String, String> {
568 let svc = self.clone();
569 tokio::task::spawn_blocking(move || {
570 AppService::pkg_scaffold(
571 &svc,
572 &name,
573 target_dir.as_deref(),
574 category.as_deref(),
575 description.as_deref(),
576 )
577 })
578 .await
579 .map_err(|e| format!("pkg_scaffold task panicked: {e}"))?
580 }
581
582 async fn hub_index_aggregate(&self) -> Result<String, String> {
592 let svc = self.clone();
593 let (index, warnings) = tokio::task::spawn_blocking(move || {
594 AppService::aggregate_index(&svc).map_err(|e| e.to_string())
595 })
596 .await
597 .map_err(|e| format!("hub_index_aggregate task panicked: {e}"))??;
598
599 let mut json = serde_json::to_value(&index)
600 .map_err(|e| format!("hub_index_aggregate: serialize index: {e}"))?;
601 if !warnings.is_empty() {
602 if let Some(obj) = json.as_object_mut() {
603 obj.insert("warnings".to_string(), serde_json::json!(warnings));
604 }
605 }
606 serde_json::to_string(&json)
607 .map_err(|e| format!("hub_index_aggregate: serialize final: {e}"))
608 }
609
610 async fn setting_resolve(&self, target: Option<String>) -> Result<String, String> {
613 let app_dir = self.log_config.app_dir();
614 let project_root = self.resolve_root(None);
615 tokio::task::spawn_blocking(move || {
616 crate::service::setting::resolve_setting(
617 &app_dir,
618 project_root.as_deref(),
619 target.as_deref(),
620 )
621 .map_err(|e| e.to_string())
622 .and_then(|r| {
623 serde_json::to_string(&r).map_err(|e| format!("setting_resolve: serialize: {e}"))
624 })
625 })
626 .await
627 .map_err(|e| format!("setting_resolve: task panicked: {e}"))?
628 }
629
630 async fn state_list(&self, namespace: String) -> Result<String, String> {
633 let store = Arc::clone(&self.state_store);
634 tokio::task::spawn_blocking(move || {
635 store
636 .list_dispatched(&namespace)
637 .map_err(AppService::state_err_to_wire)
638 .and_then(|keys| {
639 serde_json::to_string(&serde_json::json!({"keys": keys}))
643 .map_err(|e| format!("state_list: serialize: {e}"))
644 })
645 })
646 .await
647 .map_err(|e| format!("state_list: task panicked: {e}"))?
648 }
649
650 async fn state_show(&self, namespace: String, key: String) -> Result<String, String> {
651 let store = Arc::clone(&self.state_store);
652 tokio::task::spawn_blocking(move || {
653 store
654 .show_dispatched(&namespace, &key)
655 .map_err(AppService::state_err_to_wire)
656 .and_then(|value| {
657 serde_json::to_string(&value).map_err(|e| format!("state_show: serialize: {e}"))
658 })
659 })
660 .await
661 .map_err(|e| format!("state_show: task panicked: {e}"))?
662 }
663
664 async fn state_reset(
665 &self,
666 namespace: String,
667 key: String,
668 steps: Option<Vec<String>>,
669 fields: Option<Vec<String>>,
670 ) -> Result<String, String> {
671 let store = Arc::clone(&self.state_store);
672 let steps_input: Vec<String> = steps.clone().unwrap_or_default();
674 let fields_input: Vec<String> = fields.clone().unwrap_or_default();
675 tokio::task::spawn_blocking(move || {
676 let steps_slice: Vec<String> = steps.unwrap_or_default();
677 let fields_slice: Vec<String> = fields.unwrap_or_default();
678 store
679 .reset_dispatched_with_backup(&namespace, &key, &steps_slice, &fields_slice)
680 .map_err(AppService::state_err_to_wire)
681 .and_then(|report: ResetReport| {
682 let v = serde_json::json!({
683 "ok": true,
684 "backup_path": report.backup_path.to_string_lossy(),
685 "steps_removed": report.steps_removed,
686 "steps_input": steps_input,
687 "fields_removed": report.fields_removed,
688 "fields_input": fields_input,
689 });
690 serde_json::to_string(&v).map_err(|e| format!("state_reset: serialize: {e}"))
691 })
692 })
693 .await
694 .map_err(|e| format!("state_reset: task panicked: {e}"))?
695 }
696
697 async fn state_set(
698 &self,
699 namespace: String,
700 key: String,
701 value: serde_json::Value,
702 ) -> Result<String, String> {
703 let store = Arc::clone(&self.state_store);
704 tokio::task::spawn_blocking(move || {
705 store
706 .set_dispatched(&namespace, &key, &value)
707 .map_err(AppService::state_err_to_wire)
708 .map(|_| r#"{"ok":true}"#.to_string())
709 })
710 .await
711 .map_err(|e| format!("state_set: task panicked: {e}"))?
712 }
713
714 async fn state_delete(&self, namespace: String, key: String) -> Result<String, String> {
715 let store = Arc::clone(&self.state_store);
716 tokio::task::spawn_blocking(move || {
717 store
718 .delete_dispatched(&namespace, &key)
719 .map_err(AppService::state_err_to_wire)
720 .and_then(|existed| {
721 serde_json::to_string(&serde_json::json!({"ok": true, "existed": existed}))
722 .map_err(|e| format!("state_delete: serialize: {e}"))
723 })
724 })
725 .await
726 .map_err(|e| format!("state_delete: task panicked: {e}"))?
727 }
728
729 async fn info(&self) -> String {
732 let svc = self.clone();
733 tokio::task::spawn_blocking(move || AppService::info(&svc))
734 .await
735 .unwrap_or_else(|e| format!("{{\"error\": \"info: task panicked: {e}\"}}"))
736 }
737
738 async fn pool_ensure(&self) -> Result<String, String> {
741 AppService::pool_ensure_impl(self).await
742 }
743
744 async fn pool_status(&self, sid: Option<String>) -> Result<String, String> {
745 AppService::pool_status_impl(self, sid).await
746 }
747
748 async fn pool_stop(&self, sid: Option<String>) -> Result<String, String> {
749 AppService::pool_stop_impl(self, sid).await
750 }
751}
752
753impl AppService {
756 fn state_err_to_wire(e: StateError) -> String {
768 let v = match e {
769 StateError::KeyNotFound { namespace, key } => {
770 serde_json::json!({"error": "NOT_FOUND", "namespace": namespace, "key": key})
771 }
772 StateError::UnsafeSegment { which, value } => {
773 serde_json::json!({"error": "UNSAFE_SEGMENT", "which": which, "value": value})
774 }
775 StateError::IoBackup(io_err) => {
776 serde_json::json!({"error": "IO_BACKUP", "message": io_err.to_string()})
777 }
778 StateError::IoRead(io_err) => {
779 serde_json::json!({"error": "IO_READ", "message": io_err.to_string()})
780 }
781 StateError::IoWrite(io_err) => {
782 serde_json::json!({"error": "IO_WRITE", "message": io_err.to_string()})
783 }
784 StateError::Serde(serde_err) => {
785 serde_json::json!({"error": "SERDE", "message": serde_err.to_string()})
786 }
787 StateError::ShapeInvalid { reason } => {
788 serde_json::json!({"error": "SHAPE_INVALID", "reason": reason})
789 }
790 };
791 serde_json::to_string(&v).unwrap_or_else(|e| {
792 format!("{{\"error\":\"INTERNAL\",\"message\":\"serialize failed: {e}\"}}")
796 })
797 }
798}