Skip to main content

ryo_app/api/
mod.rs

1//! Api - External interface for RYO operations.
2//!
3//! This module provides the main entry point for RYO operations.
4//! It orchestrates Intent → Mutation → Execution flow.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │  Api (Application Facade)                                        │
11//! │  ├── discover(...)  → DiscoverResponse                          │
12//! │  ├── suggest(...)   → SuggestResponse                           │
13//! │  ├── run(...)       → RunResponse                               │
14//! │  ├── graph_cascade(...) → CascadeResponse                       │
15//! │  └── status()       → StatusResponse                            │
16//! └─────────────────────────────────────────────────────────────────┘
17//!                     │
18//!                     ▼
19//! ┌─────────────────────────────────────────────────────────────────┐
20//! │  Internal Services (non-public)                                  │
21//! │  ├── DiscoverService                                             │
22//! │  ├── SuggestionEngine                                            │
23//! │  ├── GraphApi                                                    │
24//! │  └── SpecService                                                 │
25//! └─────────────────────────────────────────────────────────────────┘
26//! ```
27//!
28//! # Example
29//!
30//! ```ignore
31//! use ryo_app::{Api, DiscoverRequest};
32//!
33//! // Create API from project path
34//! let api = Api::from_path("/path/to/project")?;
35//!
36//! // Discover symbols
37//! let response = api.discover(DiscoverRequest {
38//!     pattern: "Config*".to_string(),
39//!     kind: None,
40//!     sort: None,
41//!     limit: Some(10),
42//! })?;
43//!
44//! for sym in response.results {
45//!     println!("{}: {}", sym.name, sym.path.display());
46//! }
47//! ```
48
49mod types;
50
51pub use types::*;
52
53use crate::config::{RyoConfig, SuggestConfig};
54use crate::intent::{ConflictStrategy, Goal, Intent};
55use crate::planner::{PlanError, Planner};
56use crate::project::Project;
57use crate::storage::Storage;
58use ryo_analysis::{
59    AnalysisContext, DiscoveryEngine, DiscoveryQuery, SymbolId, SymbolKind, UuidPersistence,
60};
61use ryo_executor::{collect_affected_ids, BlueprintExecutor, Conflict, ParallelBlueprint};
62use ryo_query_language::{MatchResult, MatchView, QueryExecutor};
63use ryo_storage::{FileUuidStorage, TxAction, TxLog};
64use ryo_suggest::{
65    EnhancedSuggestion, RuleStore, SafetyLevel, StringErrorType, SuggestId, SuggestQuery,
66    SuggestRegistry, SuggestService, UnnecessaryClone, UnwrapToExpect,
67};
68use ryo_symbol::{ResolveError, WorkspaceType};
69use ryo_verification::{
70    CargoVerifier, FileChange, GraphVerifier, InMemoryVerifier, VerificationInput,
71    VerificationPipeline,
72};
73use std::cell::RefCell;
74use std::path::{Path, PathBuf};
75use std::time::Instant;
76
77type DefinitionDetails = (
78    Vec<types::TypeFieldInfo>,
79    Vec<types::TypeVariantInfo>,
80    Vec<types::TypeParamInfo>,
81    Option<String>,
82    Vec<String>,
83    Option<String>,
84    Vec<String>,
85);
86
87/// Convert QueryExecutor's MatchResult to tarpc-compatible DiscoveredSymbol
88fn convert_match_result_to_discovered_symbol(m: MatchResult) -> types::DiscoveredSymbol {
89    let (view_mode, definition, doc, body, snippet) = match m.view {
90        MatchView::Snippet { text } => ("snippet".to_string(), None, None, None, Some(text)),
91        MatchView::Precise => ("precise".to_string(), None, None, None, None),
92        MatchView::Def {
93            module_path: _,
94            definition,
95            doc,
96        } => ("def".to_string(), Some(definition), doc, None, None),
97        MatchView::Full {
98            module_path: _,
99            definition,
100            doc,
101            body,
102        } => ("full".to_string(), Some(definition), doc, Some(body), None),
103    };
104
105    types::DiscoveredSymbol {
106        id: m.id,
107        uuid: m.uuid,
108        path: m.path,
109        name: m.name,
110        kind: m.node_kind,
111        view_mode,
112        definition,
113        doc,
114        body,
115        snippet,
116    }
117}
118
119/// Extract parent path string from an Intent (if it has one).
120///
121/// Returns the path string for intents that have a parent field (like AddCode).
122/// Used for validating that `crate::` prefixed paths are unambiguous in workspaces.
123fn format_generics(g: &ryo_analysis::detail_store::GenericInfo) -> String {
124    let mut parts = Vec::new();
125    for lt in &g.lifetimes {
126        parts.push(lt.clone());
127    }
128    for tp in &g.type_params {
129        parts.push(tp.clone());
130    }
131    for (name, ty) in &g.const_params {
132        parts.push(format!("const {}: {}", name, ty));
133    }
134    format!("<{}>", parts.join(", "))
135}
136
137fn extract_parent_path_string(intent: &Intent) -> Option<String> {
138    match intent {
139        Intent::AddCode { symbol_path, .. } => {
140            // symbol_pathが"::"を含む場合のみ検証対象
141            symbol_path.as_ref().filter(|p| p.contains("::")).cloned()
142        }
143        // Other intents don't have parent paths that need validation
144        _ => None,
145    }
146}
147
148/// Error type for API operations.
149#[derive(Debug, thiserror::Error)]
150pub enum ApiError {
151    #[error("Project error: {0}")]
152    Project(#[from] crate::project::ProjectError),
153
154    #[error("Planning error: {0}")]
155    Planning(#[from] PlanError),
156
157    #[error("Execution error: {0}")]
158    Execution(String),
159
160    #[error("Storage error: {0}")]
161    Storage(#[from] ryo_storage::StorageError),
162
163    #[error("Invalid goal: {0}")]
164    InvalidGoal(String),
165
166    #[error("Conflicts detected: {0} conflicts require resolution")]
167    Conflicts(usize),
168
169    #[error("Syntax error after mutation: {0}")]
170    SyntaxError(String),
171
172    #[error("Not found: {0}")]
173    NotFound(String),
174
175    #[error("Graph error: {0}")]
176    Graph(#[from] crate::graph::GraphError),
177
178    #[error("Discover error: {0}")]
179    Discover(#[from] crate::discover::DiscoverError),
180
181    #[error("Spec error: {0}")]
182    Spec(#[from] crate::spec::SpecError),
183
184    #[error("{0}")]
185    Resolve(#[from] ResolveError),
186
187    #[error("File sync error: {0}")]
188    Sync(#[from] ryo_executor::SyncError),
189}
190
191/// Result type for API operations.
192pub type ApiResult<T> = Result<T, ApiError>;
193
194/// Options for execution.
195#[derive(Debug, Clone, Default)]
196pub struct ExecuteOptions {
197    /// Skip actual mutation execution (plan and validate only).
198    /// When true, planning and conflict detection are performed but no changes are applied.
199    pub dry_run: bool,
200
201    /// Verify syntax after mutations using syn parser.
202    /// When true, all modified files are parsed to ensure valid Rust syntax.
203    pub check_syntax: bool,
204}
205
206impl ExecuteOptions {
207    /// Create new options with defaults.
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Enable dry-run mode.
213    pub fn dry_run(mut self) -> Self {
214        self.dry_run = true;
215        self
216    }
217
218    /// Enable syntax checking.
219    pub fn check_syntax(mut self) -> Self {
220        self.check_syntax = true;
221        self
222    }
223}
224
225/// Execution result from API.
226#[derive(Debug, Clone)]
227pub struct ExecutionResult {
228    /// Structured status with code and detailed reason.
229    /// This is the primary way to understand the execution outcome.
230    pub status: ExecutionStatus,
231    /// Number of files modified
232    pub files_modified: usize,
233    /// Total changes made
234    pub total_changes: usize,
235    /// Session ID (if logged)
236    pub session_id: Option<String>,
237    /// Transaction log with mutation history
238    pub log: TxLog,
239    /// Paths of modified files
240    pub modified_files: Vec<PathBuf>,
241    /// Detected conflicts (if any)
242    pub conflicts: Vec<Conflict>,
243    /// Syntax errors found during verification (if check_syntax enabled)
244    pub syntax_errors: Vec<String>,
245    /// Whether execution completed successfully (derived from status.is_success())
246    pub success: bool,
247}
248
249impl ExecutionResult {
250    /// Create a new ExecutionResult with the given status.
251    pub fn with_status(status: ExecutionStatus) -> Self {
252        let success = status.is_success();
253        Self {
254            status,
255            files_modified: 0,
256            total_changes: 0,
257            session_id: None,
258            log: TxLog::new(),
259            modified_files: vec![],
260            conflicts: vec![],
261            syntax_errors: vec![],
262            success,
263        }
264    }
265
266    /// Create a successful result with OK status.
267    pub fn ok(changes: usize, files: usize) -> Self {
268        let reason = format!("{} changes in {} files", changes, files);
269        Self {
270            status: ExecutionStatus::ok(reason),
271            files_modified: files,
272            total_changes: changes,
273            session_id: None,
274            log: TxLog::new(),
275            modified_files: vec![],
276            conflicts: vec![],
277            syntax_errors: vec![],
278            success: true,
279        }
280    }
281
282    /// Create a no-change result.
283    pub fn no_change(reason: impl Into<String>) -> Self {
284        Self {
285            status: ExecutionStatus::no_change(reason),
286            files_modified: 0,
287            total_changes: 0,
288            session_id: None,
289            log: TxLog::new(),
290            modified_files: vec![],
291            conflicts: vec![],
292            syntax_errors: vec![],
293            success: true,
294        }
295    }
296}
297
298/// Main API for RYO operations.
299///
300/// This is the primary entry point for external consumers (CLI, UI, Agent).
301/// All operations go through this facade.
302pub struct Api {
303    /// Analysis context - owned directly for mutable access during execution.
304    /// The context maintains SymbolRegistry and FileRegistry which must persist
305    /// across Discover → Run sequences to keep SymbolId/FileId valid.
306    context: AnalysisContext,
307    /// Storage backend for persistence
308    storage: Box<dyn Storage>,
309    /// UUID persistence backend
310    uuid_storage: Box<dyn UuidPersistence>,
311    /// Project for file operations
312    project: Project,
313    /// Cached SpecFlowData (invalidated on context.files change)
314    spec_cache: Option<crate::spec::SpecFlowData>,
315    /// Suggestion service for code improvement suggestions
316    suggest: SuggestService,
317    /// Suggestion configuration
318    suggest_config: SuggestConfig,
319    /// Generator templates for parameterized code generation
320    generator_store: ryo_suggest::GeneratorStore,
321}
322
323impl Api {
324    /// Create a new API from a project path.
325    ///
326    /// This loads the project and builds the analysis context.
327    /// Uses `from_workspace_root` for full workspace analysis (not just entry points).
328    pub fn from_path(path: impl AsRef<Path>) -> ApiResult<Self> {
329        let project = Project::load(path.as_ref())?;
330        // Use from_workspace_root for full workspace analysis
331        // (Project::load only traverses mod declarations from entry points,
332        // which misses files not reachable via mod tree)
333        let context = AnalysisContext::from_workspace_root(project.workspace_root())
334            .map_err(|e| ApiError::Execution(format!("Context build failed: {}", e)))?;
335
336        // Load suggest config from project's ryo.toml
337        let config = RyoConfig::load_or_default(path.as_ref());
338        let suggest_config = config.suggest.clone();
339
340        // Create SuggestService with configured strategy
341        let registry = Self::create_default_registry(project.root());
342        let suggest = SuggestService::with_strategy(registry, suggest_config.to_strategy());
343
344        // Load generator templates
345        let generator_store = ryo_suggest::GeneratorStore::load(project.root())
346            .unwrap_or_else(|_| ryo_suggest::GeneratorStore::new());
347
348        // Create UUID storage
349        let uuid_storage = Box::new(FileUuidStorage::new(project.root()));
350
351        Ok(Self {
352            context,
353            storage: Box::new(crate::storage::InMemoryStorage::new()),
354            uuid_storage,
355            project,
356            spec_cache: None,
357            suggest,
358            suggest_config,
359            generator_store,
360        })
361    }
362
363    /// Create a new API with a pre-built context.
364    ///
365    /// This is useful for server mode where the context is already loaded.
366    /// Loads suggest configuration from ryo.toml via the Project.
367    pub fn with_context(
368        context: AnalysisContext,
369        project: Project,
370        storage: Box<dyn Storage>,
371    ) -> Self {
372        // Load suggest config from ryo.toml
373        let suggest_config = project.config().suggest.clone();
374        let registry = Self::create_default_registry(project.root());
375        let suggest = SuggestService::with_strategy(registry, suggest_config.to_strategy());
376
377        // Load generator templates
378        let generator_store = ryo_suggest::GeneratorStore::load(project.root())
379            .unwrap_or_else(|_| ryo_suggest::GeneratorStore::new());
380
381        // Create UUID storage
382        let uuid_storage = Box::new(FileUuidStorage::new(project.root()));
383
384        Self {
385            context,
386            storage,
387            uuid_storage,
388            project,
389            spec_cache: None,
390            suggest,
391            suggest_config,
392            generator_store,
393        }
394    }
395
396    /// Create a new API with the given storage backend.
397    ///
398    /// This is the legacy constructor for backward compatibility.
399    /// Uses current directory to load project.
400    /// Loads suggest configuration from ryo.toml.
401    ///
402    /// # Panics
403    ///
404    /// Panics if project cannot be loaded from current directory.
405    /// Consider using `from_path()` instead.
406    pub fn new(storage: Box<dyn Storage>) -> Self {
407        let cwd = std::env::current_dir().unwrap_or_default();
408        let project = Project::load(&cwd).unwrap_or_else(|e| {
409            panic!(
410                "Api::new(): failed to load project from {:?}: {}. \
411                 Use Api::from_path(path, storage) to avoid relying on the current directory.",
412                cwd, e
413            )
414        });
415        let context = AnalysisContext::from_workspace_root(project.root()).unwrap_or_else(|e| {
416            panic!(
417                "Api::new(): failed to create analysis context from {:?}: {}",
418                project.root(),
419                e
420            )
421        });
422
423        // Load suggest config from ryo.toml
424        let suggest_config = project.config().suggest.clone();
425        let registry = Self::create_default_registry(project.root());
426        let suggest = SuggestService::with_strategy(registry, suggest_config.to_strategy());
427
428        // Load generator templates
429        let generator_store = ryo_suggest::GeneratorStore::load(project.root())
430            .unwrap_or_else(|_| ryo_suggest::GeneratorStore::new());
431
432        // Create UUID storage
433        let uuid_storage = Box::new(FileUuidStorage::new(project.root()));
434
435        Self {
436            context,
437            storage,
438            uuid_storage,
439            project,
440            spec_cache: None,
441            suggest,
442            suggest_config,
443            generator_store,
444        }
445    }
446
447    /// Create the default SuggestRegistry with built-in patterns.
448    ///
449    /// Loads rules from all sources (builtin, global, project) and registers
450    /// them as PatternBasedSuggest in the registry.
451    fn create_default_registry(project_path: &Path) -> SuggestRegistry {
452        let mut registry = SuggestRegistry::new();
453
454        // Register performance suggests
455        registry.register(UnnecessaryClone::new());
456        tracing::debug!("Registered performance suggests: UnnecessaryClone");
457
458        // Register safety suggests
459        registry.register(UnwrapToExpect::new());
460        registry.register(StringErrorType::new());
461        tracing::debug!("Registered safety suggests: UnwrapToExpect, StringErrorType");
462
463        // Load rules from all sources (builtin + global + project)
464        match RuleStore::load(project_path) {
465            Ok(store) => {
466                let count = registry.register_from_rule_store(&store);
467                tracing::debug!("Registered {} lint rules from RuleStore", count);
468            }
469            Err(e) => {
470                tracing::warn!("Failed to load RuleStore: {}", e);
471                // Fall back to builtin rules only
472                if let Ok(store) = RuleStore::builtin_only() {
473                    let count = registry.register_from_rule_store(&store);
474                    tracing::debug!("Registered {} builtin lint rules (fallback)", count);
475                }
476            }
477        }
478
479        registry
480    }
481
482    // ========================================================================
483    // UUID Resolution Helpers
484    // ========================================================================
485
486    /// Resolve a SymbolId from UUID, SymbolId string, or name.
487    ///
488    /// Resolution priority:
489    /// 1. `uuid` - Persistent cross-session identifier (highest priority)
490    /// 2. `id` - Session-volatile SymbolId string (e.g., "165v1")
491    /// 3. `name` - Symbol name pattern (lowest priority)
492    ///
493    /// # Returns
494    /// - `Ok(Some(SymbolId))` if resolved successfully
495    /// - `Ok(None)` if no identifier provided
496    /// - `Err(ApiError::NotFound)` if identifier provided but not found
497    fn resolve_symbol_id(
498        &self,
499        uuid: Option<&str>,
500        id: Option<&str>,
501        name: Option<&str>,
502    ) -> ApiResult<Option<SymbolId>> {
503        // Priority 1: UUID (persistent)
504        if let Some(uuid_str) = uuid {
505            let uuid = uuid::Uuid::parse_str(uuid_str).map_err(|_| {
506                ApiError::InvalidGoal(format!("Invalid UUID format: '{}'", uuid_str))
507            })?;
508            return self
509                .context
510                .registry
511                .lookup_by_uuid(uuid)
512                .map(Some)
513                .ok_or_else(|| {
514                    ApiError::NotFound(format!("UUID '{}' not found in registry", uuid_str))
515                });
516        }
517
518        // Priority 2: SymbolId string (session-volatile)
519        if let Some(id_str) = id {
520            let symbol_id = SymbolId::parse(id_str).ok_or_else(|| {
521                ApiError::InvalidGoal(format!(
522                    "Invalid SymbolId format: '{}'. Expected format: '165v1'",
523                    id_str
524                ))
525            })?;
526            // Verify symbol exists
527            if self.context.registry.resolve(symbol_id).is_none() {
528                let info = self
529                    .context
530                    .registry
531                    .resolve(symbol_id)
532                    .map(|m| format!(" (symbol: {})", m.name()))
533                    .unwrap_or_default();
534                return Err(ApiError::NotFound(format!("'{}'{}", id_str, info)));
535            }
536            return Ok(Some(symbol_id));
537        }
538
539        // Priority 3: Name pattern (requires search)
540        if let Some(_name_pattern) = name {
541            // Name resolution is handled by the calling method
542            // since it may need fuzzy matching or glob patterns
543            return Ok(None);
544        }
545
546        Ok(None)
547    }
548
549    /// Resolve a SymbolId from UUID or SymbolId string (required).
550    ///
551    /// This is a stricter version for APIs that require an identifier.
552    /// Unlike `resolve_symbol_id`, this returns an error if no identifier provided.
553    fn resolve_symbol_id_required(&self, uuid: Option<&str>, id: &str) -> ApiResult<SymbolId> {
554        // Try UUID first
555        if let Some(uuid_str) = uuid {
556            let uuid = uuid::Uuid::parse_str(uuid_str).map_err(|_| {
557                ApiError::InvalidGoal(format!("Invalid UUID format: '{}'", uuid_str))
558            })?;
559            return self.context.registry.lookup_by_uuid(uuid).ok_or_else(|| {
560                ApiError::NotFound(format!("UUID '{}' not found in registry", uuid_str))
561            });
562        }
563
564        // Fall back to SymbolId string
565        let symbol_id = SymbolId::parse(id).ok_or_else(|| {
566            ApiError::InvalidGoal(format!(
567                "Invalid SymbolId format: '{}'. Expected format: '165v1'",
568                id
569            ))
570        })?;
571
572        // Verify symbol exists and provide better error message
573        if self.context.registry.resolve(symbol_id).is_none() {
574            let info = self
575                .context
576                .registry
577                .resolve(symbol_id)
578                .map(|m| format!(" (symbol: {})", m.name()))
579                .unwrap_or_default();
580            return Err(ApiError::NotFound(format!("'{}'{}", id, info)));
581        }
582
583        Ok(symbol_id)
584    }
585
586    /// Build a helpful error when no variables are found in the dataflow graph.
587    ///
588    /// Detects if the user passed a type name (Struct/Enum/Trait/etc.)
589    /// and provides a targeted suggestion.
590    fn no_vars_error(
591        &self,
592        name: Option<&str>,
593        display_name: &str,
594        registry: &ryo_symbol::SymbolRegistry,
595    ) -> ApiError {
596        if let Some(name) = name {
597            let type_kind = registry.find_by_name(name).into_iter().find_map(|sid| {
598                let kind = registry.kind(sid)?;
599                matches!(
600                    kind,
601                    ryo_symbol::SymbolKind::Struct
602                        | ryo_symbol::SymbolKind::Enum
603                        | ryo_symbol::SymbolKind::Trait
604                        | ryo_symbol::SymbolKind::TypeAlias
605                        | ryo_symbol::SymbolKind::Union
606                )
607                .then_some(kind)
608            });
609            if let Some(kind) = type_kind {
610                return ApiError::InvalidGoal(format!(
611                    "'{}' is a {:?}. This command analyzes variables within functions. \
612                     Pass a function/method name that uses '{}' instead.",
613                    name, kind, name
614                ));
615            }
616        }
617        ApiError::NotFound(format!(
618            "'{}': no variables found in dataflow graph. Pass a function/method name.",
619            display_name
620        ))
621    }
622
623    // ========================================================================
624    // Read Operations
625    // ========================================================================
626
627    /// Discover symbols by pattern.
628    ///
629    /// # Example
630    ///
631    /// ```ignore
632    /// let response = api.discover(DiscoverRequest {
633    ///     pattern: "Config*".to_string(),
634    ///     kind: Some(ItemKind::Struct),
635    ///     sort: Some(SortOrder::Refs),
636    ///     limit: Some(10),
637    /// })?;
638    /// ```
639    pub fn discover(&mut self, request: DiscoverRequest) -> ApiResult<DiscoverResponse> {
640        let start = Instant::now();
641
642        // Handle --id: direct lookup by SymbolId
643        if request.is_id {
644            return self.discover_by_id(&request, start);
645        }
646
647        let engine = DiscoveryEngine::new(&self.context.code_graph, &self.context.registry, None);
648
649        let sort = request.sort.map(|s| match s {
650            SortOrder::Alpha => ryo_analysis::SortOrder::Alpha,
651            SortOrder::Refs => ryo_analysis::SortOrder::Refs,
652            SortOrder::Impls => ryo_analysis::SortOrder::Impls,
653        });
654
655        // Don't apply limit to query - we apply it after filtering
656        let mut query = DiscoveryQuery::symbol(&request.pattern);
657
658        if let Some(kind) = request.kind {
659            query = query.kind(kind);
660        }
661        if let Some(s) = sort {
662            query = query.sort(s);
663        }
664        if request.ignore_case {
665            query = query.ignore_case();
666        }
667        if request.ignore_word_separate {
668            query = query.ignore_word_separate();
669        }
670
671        let result = engine.execute(&query);
672
673        // Apply RyoQL-style post-filters
674        let filtered_symbols = self.apply_discover_filters(
675            result.symbols,
676            request.is_async,
677            request.is_unsafe,
678            request.scope_path.as_deref(),
679            request.attr.as_deref(),
680        );
681
682        // Apply limit after filtering
683        let (symbols_to_convert, truncated) = if let Some(limit) = request.limit {
684            if filtered_symbols.len() > limit {
685                (
686                    filtered_symbols.into_iter().take(limit).collect::<Vec<_>>(),
687                    true,
688                )
689            } else {
690                (filtered_symbols, result.truncated)
691            }
692        } else {
693            (filtered_symbols, result.truncated)
694        };
695
696        let total = symbols_to_convert.len();
697
698        // Use QueryExecutor to convert symbols with view mode
699        let view_mode = request.view.unwrap_or_default();
700        let executor = QueryExecutor::new(&self.context);
701
702        // Convert to tarpc-compatible DiscoveredSymbol
703        let symbols: Vec<types::DiscoveredSymbol> = symbols_to_convert
704            .iter()
705            .map(|sym| {
706                let match_result = executor.to_match_result(sym, view_mode);
707                convert_match_result_to_discovered_symbol(match_result)
708            })
709            .collect();
710
711        let status = if total == 0 {
712            "not_found"
713        } else if truncated {
714            "partial"
715        } else {
716            "found"
717        };
718
719        // Generate hint when 0 results with kind filter
720        let hint = if total == 0 && request.kind.is_some() {
721            self.generate_discover_hint(&request.pattern, &engine)
722        } else {
723            None
724        };
725
726        Ok(DiscoverResponse {
727            status: status.to_string(),
728            symbols,
729            total,
730            elapsed_ms: start.elapsed().as_millis() as u32,
731            hint,
732        })
733    }
734
735    /// Discover a single symbol by SymbolId.
736    ///
737    /// Used when `request.is_id` is true. Performs direct registry lookup
738    /// instead of pattern matching.
739    fn discover_by_id(
740        &self,
741        request: &DiscoverRequest,
742        start: Instant,
743    ) -> ApiResult<DiscoverResponse> {
744        use ryo_analysis::DiscoveredSymbol;
745
746        if request.pattern.contains('*') || request.pattern.contains('?') {
747            return Err(ApiError::Execution(
748                "--id does not support glob patterns (* or ?)".to_string(),
749            ));
750        }
751
752        let symbol_id = SymbolId::parse(&request.pattern).ok_or_else(|| {
753            ApiError::Execution(format!(
754                "Invalid SymbolId format: '{}'. Expected format: '165v1' or 'SymbolId(165v1)'",
755                request.pattern
756            ))
757        })?;
758
759        let mut discovered = Vec::new();
760        if let Some(path) = self.context.registry.resolve(symbol_id) {
761            let kind = self
762                .context
763                .registry
764                .kind(symbol_id)
765                .unwrap_or(ryo_analysis::SymbolKind::Any);
766            let mut symbol = DiscoveredSymbol::new(symbol_id, path.clone(), kind);
767            if let Some(span) = self.context.registry.span(symbol_id) {
768                symbol = symbol.with_span(span.clone());
769            }
770            if let Some(vis) = self.context.registry.visibility(symbol_id) {
771                symbol = symbol.with_visibility(vis.clone());
772            }
773            discovered.push(symbol);
774        }
775
776        let total = discovered.len();
777        let view_mode = request.view.unwrap_or_default();
778        let executor = QueryExecutor::new(&self.context);
779
780        let symbols: Vec<types::DiscoveredSymbol> = discovered
781            .iter()
782            .map(|sym| {
783                let match_result = executor.to_match_result(sym, view_mode);
784                convert_match_result_to_discovered_symbol(match_result)
785            })
786            .collect();
787
788        let status = if total == 0 { "not_found" } else { "found" };
789
790        Ok(DiscoverResponse {
791            status: status.to_string(),
792            symbols,
793            total,
794            elapsed_ms: start.elapsed().as_millis() as u32,
795            hint: None,
796        })
797    }
798
799    /// Get codebase overview (module tree, key types, statistics).
800    pub fn overview(&self, _request: types::OverviewRequest) -> ApiResult<types::OverviewResponse> {
801        use std::collections::BTreeMap;
802
803        let start = Instant::now();
804
805        // 1. Crate/Module structure (iter_by_kind avoids full registry scan)
806        let mut crate_modules: BTreeMap<String, Vec<String>> = BTreeMap::new();
807        for symbol_id in self
808            .context
809            .registry
810            .iter_by_kind(ryo_analysis::SymbolKind::Mod)
811        {
812            let Some(path) = self.context.registry.resolve(symbol_id) else {
813                continue;
814            };
815            let path_str = path.to_string();
816            let parts: Vec<&str> = path_str.split("::").collect();
817            if let Some(crate_name) = parts.first() {
818                let module_path = parts[1..].join("::");
819                if !module_path.is_empty() {
820                    crate_modules
821                        .entry(crate_name.to_string())
822                        .or_default()
823                        .push(module_path);
824                } else {
825                    crate_modules.entry(crate_name.to_string()).or_default();
826                }
827            }
828        }
829
830        let crates: Vec<types::CrateOverview> = crate_modules
831            .into_iter()
832            .map(|(name, modules)| types::CrateOverview { name, modules })
833            .collect();
834
835        // 2. Top structs by ref_count
836        let engine = DiscoveryEngine::new(&self.context.code_graph, &self.context.registry, None);
837        let struct_query = DiscoveryQuery::symbol("*")
838            .kind(ryo_analysis::SymbolKind::Struct)
839            .sort(ryo_analysis::SortOrder::Refs);
840        let struct_result = engine.execute(&struct_query);
841        let top_structs: Vec<types::OverviewSymbol> = struct_result
842            .symbols
843            .iter()
844            .take(10)
845            .map(|s| types::OverviewSymbol {
846                path: s.path.to_string(),
847                count: s.ref_count,
848            })
849            .collect();
850
851        // 3. Top traits by impl_count
852        let trait_query = DiscoveryQuery::symbol("*")
853            .kind(ryo_analysis::SymbolKind::Trait)
854            .sort(ryo_analysis::SortOrder::Impls);
855        let trait_result = engine.execute(&trait_query);
856        let top_traits: Vec<types::OverviewSymbol> = trait_result
857            .symbols
858            .iter()
859            .take(10)
860            .map(|s| types::OverviewSymbol {
861                path: s.path.to_string(),
862                count: s.impl_count,
863            })
864            .collect();
865
866        // 4. Statistics
867        let crate_count = crates.len();
868        let kinds = [
869            (ryo_analysis::SymbolKind::Function, "Functions"),
870            (ryo_analysis::SymbolKind::Struct, "Structs"),
871            (ryo_analysis::SymbolKind::Enum, "Enums"),
872            (ryo_analysis::SymbolKind::Trait, "Traits"),
873            (ryo_analysis::SymbolKind::Impl, "Impls"),
874            (ryo_analysis::SymbolKind::Mod, "Mods"),
875            (ryo_analysis::SymbolKind::Const, "Consts"),
876            (ryo_analysis::SymbolKind::Static, "Statics"),
877            (ryo_analysis::SymbolKind::TypeAlias, "TypeAliases"),
878        ];
879        let by_kind: Vec<(String, usize)> = kinds
880            .iter()
881            .filter_map(|(kind, name)| {
882                let count = self.context.registry.iter_by_kind(*kind).count();
883                if count > 0 {
884                    Some((name.to_string(), count))
885                } else {
886                    None
887                }
888            })
889            .collect();
890
891        Ok(types::OverviewResponse {
892            crates,
893            top_structs,
894            top_traits,
895            stats: types::OverviewStats {
896                crate_count,
897                by_kind,
898            },
899            elapsed_ms: start.elapsed().as_millis() as u32,
900        })
901    }
902
903    /// Execute a RyoQL query and return structured results.
904    ///
905    /// Parses the query string (YAML/JSON), determines view mode, and executes
906    /// against the analysis context.
907    pub fn query_ryoql(&self, request: types::RyoqlRequest) -> ApiResult<types::QueryResponse> {
908        use ryo_query_language::{QueryExecutor, QueryParser};
909
910        let query = QueryParser::parse(&request.query)
911            .map_err(|e| ApiError::SyntaxError(format!("RyoQL parse error: {}", e)))?;
912
913        let view_mode = query.view.or(request.default_view).unwrap_or_default();
914        let executor = QueryExecutor::new(&self.context).with_view_mode(view_mode);
915
916        executor
917            .execute(&query)
918            .map_err(|e| ApiError::Execution(format!("RyoQL execution failed: {}", e)))
919    }
920
921    /// Search literals in source code using the LiteralIndex.
922    #[cfg(feature = "literal-search")]
923    pub fn search_literal(
924        &self,
925        request: types::LiteralSearchRequest,
926    ) -> ApiResult<types::LiteralSearchResponse> {
927        use ryo_analysis::literal::{LiteralKind, LiteralQuery};
928
929        let start = Instant::now();
930
931        let literal_index = self
932            .context
933            .literal_index
934            .as_ref()
935            .ok_or_else(|| ApiError::Execution("Literal index not available".to_string()))?;
936
937        let mut query = LiteralQuery::new(&request.pattern).with_limit(request.limit);
938        if let Some(ref kind_str) = request.kind {
939            if let Some(kind) = LiteralKind::parse_kind(kind_str) {
940                query = query.with_kind(kind);
941            }
942        }
943
944        let matches = literal_index
945            .search(&query)
946            .map_err(|e| ApiError::Execution(format!("Literal search failed: {}", e)))?;
947
948        let total = matches.len();
949        let results: Vec<types::LiteralMatchResult> = matches
950            .into_iter()
951            .map(|m| {
952                let symbol_name = self
953                    .context
954                    .registry
955                    .path(m.symbol_id)
956                    .map(|p| p.to_string())
957                    .unwrap_or_else(|| m.symbol_id.to_string());
958                types::LiteralMatchResult {
959                    value: m.value,
960                    kind: m.kind.to_string(),
961                    symbol_id: symbol_name,
962                    file_path: m.file_path,
963                    score: m.score,
964                }
965            })
966            .collect();
967
968        Ok(types::LiteralSearchResponse {
969            matches: results,
970            total,
971            elapsed_ms: start.elapsed().as_millis() as u32,
972        })
973    }
974
975    /// Search literals - stub when feature is disabled.
976    #[cfg(not(feature = "literal-search"))]
977    pub fn search_literal(
978        &self,
979        _request: types::LiteralSearchRequest,
980    ) -> ApiResult<types::LiteralSearchResponse> {
981        Err(ApiError::Execution(
982            "Literal search not available. Rebuild with `literal-search` feature.".to_string(),
983        ))
984    }
985
986    /// Apply RyoQL-style filters to discovered symbols.
987    fn apply_discover_filters(
988        &self,
989        symbols: Vec<ryo_analysis::DiscoveredSymbol>,
990        is_async: Option<bool>,
991        is_unsafe: Option<bool>,
992        scope_path: Option<&str>,
993        attr: Option<&str>,
994    ) -> Vec<ryo_analysis::DiscoveredSymbol> {
995        use glob::Pattern as GlobPattern;
996
997        let mut filtered = symbols;
998
999        // Filter by is_async
1000        if let Some(expected_async) = is_async {
1001            filtered.retain(|sym| {
1002                if let Some(detail) = self.context.detail_store.function(sym.id) {
1003                    detail.is_async == expected_async
1004                } else {
1005                    // Non-function symbols: async=false
1006                    !expected_async
1007                }
1008            });
1009        }
1010
1011        // Filter by is_unsafe
1012        if let Some(expected_unsafe) = is_unsafe {
1013            filtered.retain(|sym| {
1014                // Check function
1015                if let Some(detail) = self.context.detail_store.function(sym.id) {
1016                    return detail.is_unsafe == expected_unsafe;
1017                }
1018                // Check trait
1019                if let Some(detail) = self.context.detail_store.trait_(sym.id) {
1020                    return detail.is_unsafe == expected_unsafe;
1021                }
1022                // Check impl
1023                if let Some(detail) = self.context.detail_store.impl_(sym.id) {
1024                    return detail.is_unsafe == expected_unsafe;
1025                }
1026                // Other symbols: unsafe=false
1027                !expected_unsafe
1028            });
1029        }
1030
1031        // Filter by scope_path (glob pattern)
1032        if let Some(pattern) = scope_path {
1033            if let Ok(glob) = GlobPattern::new(pattern) {
1034                filtered.retain(|sym| {
1035                    if let Some(ref span) = sym.span {
1036                        glob.matches(span.file.as_relative().to_string_lossy().as_ref())
1037                    } else {
1038                        false // No span = excluded
1039                    }
1040                });
1041            }
1042        }
1043
1044        // Filter by attribute pattern (glob-style)
1045        if let Some(pattern) = attr {
1046            if let Ok(glob) = GlobPattern::new(pattern) {
1047                filtered.retain(|sym| {
1048                    // Collect attrs from any detail type that matches this symbol
1049                    let attrs = self.get_symbol_attrs(sym.id);
1050                    attrs.iter().any(|a| glob.matches(a))
1051                });
1052            }
1053        }
1054
1055        filtered
1056    }
1057
1058    /// Get attributes for a symbol from any matching detail type.
1059    fn get_symbol_attrs(&self, id: ryo_analysis::SymbolId) -> Vec<&str> {
1060        let detail_store = &self.context.detail_store;
1061
1062        // Check function
1063        if let Some(detail) = detail_store.function(id) {
1064            return detail.attrs.iter().map(|s| s.as_str()).collect();
1065        }
1066        // Check struct
1067        if let Some(detail) = detail_store.struct_(id) {
1068            return detail.attrs.iter().map(|s| s.as_str()).collect();
1069        }
1070        // Check enum
1071        if let Some(detail) = detail_store.enum_(id) {
1072            return detail.attrs.iter().map(|s| s.as_str()).collect();
1073        }
1074        // Check trait
1075        if let Some(detail) = detail_store.trait_(id) {
1076            return detail.attrs.iter().map(|s| s.as_str()).collect();
1077        }
1078        // Check impl
1079        if let Some(detail) = detail_store.impl_(id) {
1080            return detail.attrs.iter().map(|s| s.as_str()).collect();
1081        }
1082
1083        Vec::new()
1084    }
1085
1086    /// Generate alternative search hint when 0 results with kind filter.
1087    ///
1088    /// Searches with `*pattern*`, `kind=None`, `ignore_case=true`, `ignore_word_separate=true`
1089    /// and returns a hint message if matches are found.
1090    fn generate_discover_hint(&self, pattern: &str, engine: &DiscoveryEngine) -> Option<String> {
1091        // Build glob pattern: *Word* for contains search
1092        let glob_pattern = if pattern.contains('*') {
1093            pattern.to_string()
1094        } else {
1095            format!("*{}*", pattern)
1096        };
1097
1098        // Re-search with kind=None (any), ignore_case=true, ignore_word_separate=true
1099        let alt_query = DiscoveryQuery::symbol(&glob_pattern)
1100            .ignore_case()
1101            .ignore_word_separate();
1102        let alt_result = engine.execute(&alt_query);
1103
1104        if alt_result.symbols.is_empty() {
1105            return None;
1106        }
1107
1108        // Show up to 5 unique matches as hints
1109        let samples: Vec<_> = alt_result
1110            .symbols
1111            .iter()
1112            .take(5)
1113            .map(|s| format!("  - {} [{}]", s.path, s.kind))
1114            .collect();
1115
1116        let more = if alt_result.symbols.len() > 5 {
1117            format!("\n  ... and {} more", alt_result.symbols.len() - 5)
1118        } else {
1119            String::new()
1120        };
1121
1122        Some(format!(
1123            "Hint: {} matches found with \"{}\" --kind any -i -w:\n{}{}",
1124            alt_result.symbols.len(),
1125            glob_pattern,
1126            samples.join("\n"),
1127            more
1128        ))
1129    }
1130
1131    /// Get improvement suggestions.
1132    ///
1133    /// Uses the integrated SuggestService to query cached suggestions.
1134    /// Call `suggest_scan()` first to detect new suggestions.
1135    ///
1136    /// # Example
1137    ///
1138    /// ```ignore
1139    /// let response = api.suggest(SuggestRequest {
1140    ///     pattern_filter: Some("*Service*".to_string()),
1141    ///     high_impact: true,
1142    ///     quick: false,
1143    /// })?;
1144    /// ```
1145    pub fn suggest(&self, request: SuggestRequest) -> ApiResult<SuggestResponse> {
1146        // Parse scope filter from request
1147        let scope_filter: Vec<ryo_suggest::SymbolScope> = request
1148            .scope_filter
1149            .iter()
1150            .filter_map(|s| s.parse().ok())
1151            .collect();
1152
1153        // Run scan if requested
1154        if request.scan {
1155            if request.precheck {
1156                let _ = self.suggest_scan_with_precheck();
1157            } else {
1158                let _ = self.suggest_scan_with_scope(&scope_filter);
1159            }
1160        }
1161
1162        // Build query from request
1163        let mut query = SuggestQuery::all();
1164
1165        // Apply filters
1166        if request.high_impact {
1167            query = query.with_max_safety(SafetyLevel::Auto);
1168        }
1169
1170        // Query the suggest service
1171        let results = self.suggest.query(&query);
1172
1173        // Convert to response format with exclusion filter
1174        let exclude_rules = &request.exclude_rules;
1175        let suggestions: Vec<Suggestion> = results
1176            .iter()
1177            .filter_map(|(id, category, safety)| {
1178                // Filter out excluded rules
1179                if !exclude_rules.is_empty() {
1180                    if let Some(rule_id) = self.suggest.rule_id(*id) {
1181                        if exclude_rules.iter().any(|ex| ex == rule_id) {
1182                            return None;
1183                        }
1184                    }
1185                }
1186
1187                let stored = self.suggest.get(*id)?;
1188
1189                // Filter by scope
1190                if !scope_filter.is_empty() && !scope_filter.contains(&stored.opportunity.scope) {
1191                    return None;
1192                }
1193
1194                let pattern_name = self.suggest.pattern_name(*id)?;
1195                let rule_id = self.suggest.rule_id(*id).map(|s| s.to_string());
1196
1197                Some(Suggestion {
1198                    id: id.to_string(),
1199                    rule_id,
1200                    title: pattern_name.to_string(),
1201                    description: stored.opportunity.message.clone(),
1202                    category: format!("{:?}", category),
1203                    impact: format!("{:?}", safety),
1204                    file: PathBuf::from(&stored.opportunity.location.file),
1205                    symbol_path: Some(stored.opportunity.location.symbol_path.to_string()),
1206                    fix_intent: None, // TODO: Generate intent from MutationSpec
1207                })
1208            })
1209            .collect();
1210
1211        // Sort suggestions by ID for deterministic output
1212        let mut suggestions = suggestions;
1213        suggestions.sort_by(|a, b| a.id.cmp(&b.id));
1214
1215        // Build summary
1216        let total = suggestions.len();
1217        let high_impact = suggestions.iter().filter(|s| s.impact == "Auto").count();
1218
1219        // Count by category
1220        let mut by_category = std::collections::HashMap::new();
1221        for s in &suggestions {
1222            *by_category.entry(s.category.clone()).or_insert(0) += 1;
1223        }
1224
1225        // Build enhanced suggestions if requested
1226        let enhanced = if request.enhanced {
1227            results
1228                .iter()
1229                .filter_map(|(id, _category, _safety)| {
1230                    // Filter out excluded rules
1231                    if !exclude_rules.is_empty() {
1232                        if let Some(rule_id) = self.suggest.rule_id(*id) {
1233                            if exclude_rules.iter().any(|ex| ex == rule_id) {
1234                                return None;
1235                            }
1236                        }
1237                    }
1238
1239                    let stored = self.suggest.get(*id)?;
1240
1241                    // Filter by scope
1242                    if !scope_filter.is_empty() && !scope_filter.contains(&stored.opportunity.scope)
1243                    {
1244                        return None;
1245                    }
1246
1247                    Some(
1248                        EnhancedSuggestion::from_opportunity(&stored.opportunity, *id)
1249                            .with_description(stored.opportunity.message.clone()),
1250                    )
1251                })
1252                .collect()
1253        } else {
1254            Vec::new()
1255        };
1256
1257        Ok(SuggestResponse {
1258            suggestions,
1259            summary: SuggestionSummary {
1260                total,
1261                high_impact,
1262                by_category: by_category.into_iter().collect(),
1263            },
1264            enhanced,
1265        })
1266    }
1267
1268    /// Scan the codebase and detect new suggestions.
1269    ///
1270    /// This runs all registered patterns against the current context
1271    /// and populates the suggestion cache.
1272    ///
1273    /// Respects `disabled_rules`, `enabled_rules`, and `severity_overrides` from ryo.toml.
1274    pub fn suggest_scan(&self) -> usize {
1275        self.suggest_scan_with_scope(&[])
1276    }
1277
1278    /// Scan with scope filtering.
1279    ///
1280    /// If `scope_filter` is empty, all scopes are included.
1281    /// Otherwise, only suggestions matching one of the specified scopes are stored.
1282    pub fn suggest_scan_with_scope(&self, scope_filter: &[ryo_suggest::SymbolScope]) -> usize {
1283        use ryo_suggest::LintSeverity;
1284
1285        // Collect all symbol IDs from the registry
1286        let all_symbols: Vec<SymbolId> = self.context.registry.iter().map(|(id, _)| id).collect();
1287
1288        // Build allow store
1289        let allow_store = ryo_suggest::AllowStore::from_context(&self.context);
1290
1291        // Run detection with full configuration
1292        // Use project config for module-level rule filtering
1293        let config = self.project.config();
1294        self.suggest.detect_with_config_and_scope(
1295            &self.context,
1296            &all_symbols,
1297            &allow_store,
1298            |rule_id, file_path| config.is_rule_enabled_for_file(file_path, rule_id),
1299            |rule_id| {
1300                self.suggest_config
1301                    .get_severity_override(rule_id)
1302                    .and_then(|s| s.parse::<LintSeverity>().ok())
1303            },
1304            scope_filter,
1305        )
1306    }
1307
1308    /// Scan the codebase with pre-check verification.
1309    ///
1310    /// This runs all registered patterns against the current context,
1311    /// verifies each suggestion speculatively, and only stores suggestions
1312    /// that pass the pre-check. This ensures that Apply will succeed.
1313    ///
1314    /// # Example
1315    ///
1316    /// ```ignore
1317    /// let result = api.suggest_scan_with_precheck();
1318    /// println!("Passed: {}, Failed: {}", result.passed, result.failed_precheck);
1319    /// ```
1320    pub fn suggest_scan_with_precheck(&self) -> ryo_suggest::DetectWithPrecheckResult {
1321        use std::sync::atomic::{AtomicU64, Ordering};
1322
1323        // Timing accumulators (in microseconds)
1324        static FORK_TIME: AtomicU64 = AtomicU64::new(0);
1325        static EXEC_TIME: AtomicU64 = AtomicU64::new(0);
1326        static GRAPH_REBUILD_TIME: AtomicU64 = AtomicU64::new(0);
1327        static VERIFY_TIME: AtomicU64 = AtomicU64::new(0);
1328        static COUNT: AtomicU64 = AtomicU64::new(0);
1329
1330        // Reset counters
1331        FORK_TIME.store(0, Ordering::Relaxed);
1332        EXEC_TIME.store(0, Ordering::Relaxed);
1333        GRAPH_REBUILD_TIME.store(0, Ordering::Relaxed);
1334        VERIFY_TIME.store(0, Ordering::Relaxed);
1335        COUNT.store(0, Ordering::Relaxed);
1336
1337        // Collect all symbol IDs from the registry
1338        let all_symbols: Vec<SymbolId> = self.context.registry.iter().map(|(id, _)| id).collect();
1339
1340        // Measure wall-clock time for the parallel phase
1341        let wall_start = Instant::now();
1342
1343        // Run detection with parallel pre-check (rayon-based)
1344        // OPTIMIZATION: Use thread_local to cache forked context per thread.
1345        // - First use on thread: fork_clone (expensive, ~78ms)
1346        // - Subsequent uses: snapshot → apply → verify → rollback (cheap)
1347        // This reduces fork_clone calls from N (=suggestions) to T (=thread count).
1348        thread_local! {
1349            static CACHED_CTX: RefCell<Option<AnalysisContext>> = const { RefCell::new(None) };
1350        }
1351
1352        // Limit to top 100 candidates by priority to keep precheck fast
1353        const PRECHECK_LIMIT: usize = 100;
1354
1355        // Use rule filter from config for project-level disabled_rules/enabled_rules
1356        let result = self.suggest.detect_with_parallel_precheck_and_rule_filter(
1357            &self.context,
1358            &all_symbols,
1359            Some(PRECHECK_LIMIT),
1360            |rule_id| self.suggest_config.is_rule_enabled(rule_id),
1361            |opp, specs, ctx| {
1362                COUNT.fetch_add(1, Ordering::Relaxed);
1363
1364                CACHED_CTX.with(|cell| {
1365                    let mut borrow = cell.borrow_mut();
1366
1367                    // First use on this thread: fork_clone (expensive, but only once per thread)
1368                    let forked_ctx = borrow.get_or_insert_with(|| {
1369                        let t0 = Instant::now();
1370                        let cloned = ctx.fork_clone();
1371                        FORK_TIME.fetch_add(t0.elapsed().as_micros() as u64, Ordering::Relaxed);
1372                        cloned
1373                    });
1374
1375                    // Get target symbols from opportunity for snapshot
1376                    let target_ids = &opp.targets;
1377
1378                    // Take snapshot before mutation (cheap: just copies affected AST items)
1379                    let t_snapshot = Instant::now();
1380                    let snapshot = forked_ctx.snapshot_symbols(target_ids);
1381                    // Include snapshot time in FORK_TIME for comparison
1382                    FORK_TIME.fetch_add(t_snapshot.elapsed().as_micros() as u64, Ordering::Relaxed);
1383
1384                    // Apply mutations speculatively
1385                    let t1 = Instant::now();
1386                    let blueprint = ParallelBlueprint::from_mutations(specs.to_vec());
1387                    let executor = BlueprintExecutor::default();
1388                    let exec_result = executor.execute_v2(&blueprint, forked_ctx);
1389                    EXEC_TIME.fetch_add(t1.elapsed().as_micros() as u64, Ordering::Relaxed);
1390
1391                    if !exec_result.success {
1392                        // Rollback and return false
1393                        forked_ctx.rollback(snapshot, target_ids);
1394                        return false;
1395                    }
1396
1397                    // Rebuild analysis graphs from MutationEvents (symbol-based, O(S))
1398                    // CRITICAL: Without this, GraphVerifier would check against stale indices
1399                    let t_rebuild = Instant::now();
1400                    let all_events: Vec<_> = exec_result
1401                        .results
1402                        .iter()
1403                        .flat_map(|r| r.events.clone())
1404                        .collect();
1405                    let affected_ids: Vec<SymbolId> = if !all_events.is_empty() {
1406                        let ids = collect_affected_ids(&all_events, forked_ctx.registry());
1407                        forked_ctx.rebuild_after_mutation_by_symbols(&ids);
1408                        ids
1409                    } else {
1410                        target_ids.clone()
1411                    };
1412                    GRAPH_REBUILD_TIME
1413                        .fetch_add(t_rebuild.elapsed().as_micros() as u64, Ordering::Relaxed);
1414
1415                    // Verify
1416                    let t2 = Instant::now();
1417                    let verifier = GraphVerifier::new();
1418                    let vresult = verifier.verify_precheck(forked_ctx);
1419                    VERIFY_TIME.fetch_add(t2.elapsed().as_micros() as u64, Ordering::Relaxed);
1420
1421                    // Rollback to original state for next iteration
1422                    let t_rollback = Instant::now();
1423                    forked_ctx.rollback(snapshot, &affected_ids);
1424                    // Include rollback time in GRAPH_REBUILD_TIME
1425                    GRAPH_REBUILD_TIME
1426                        .fetch_add(t_rollback.elapsed().as_micros() as u64, Ordering::Relaxed);
1427
1428                    vresult.is_success()
1429                })
1430            },
1431        );
1432
1433        // Measure wall-clock time
1434        let wall_elapsed = wall_start.elapsed();
1435
1436        // Write timing summary to file
1437        let count = COUNT.load(Ordering::Relaxed);
1438        if count > 0 {
1439            let cpu_total = (FORK_TIME.load(Ordering::Relaxed)
1440                + EXEC_TIME.load(Ordering::Relaxed)
1441                + GRAPH_REBUILD_TIME.load(Ordering::Relaxed)
1442                + VERIFY_TIME.load(Ordering::Relaxed)) as f64
1443                / 1000.0;
1444            let wall_ms = wall_elapsed.as_millis() as f64;
1445            let parallelism = if wall_ms > 0.0 {
1446                cpu_total / wall_ms
1447            } else {
1448                0.0
1449            };
1450
1451            let timing = format!(
1452                "\n=== Precheck Timing ({} suggestions) ===\n\
1453                 fork_clone:    {:>8.2}ms total, {:>6.2}ms avg\n\
1454                 execute_v2:    {:>8.2}ms total, {:>6.2}ms avg\n\
1455                 graph_rebuild: {:>8.2}ms total, {:>6.2}ms avg\n\
1456                 verify:        {:>8.2}ms total, {:>6.2}ms avg\n\
1457                 ---\n\
1458                 CPU TOTAL:     {:>8.2}ms\n\
1459                 WALL CLOCK:    {:>8.2}ms\n\
1460                 PARALLELISM:   {:>8.2}x (effective threads)\n\
1461                 =====================================\n",
1462                count,
1463                FORK_TIME.load(Ordering::Relaxed) as f64 / 1000.0,
1464                FORK_TIME.load(Ordering::Relaxed) as f64 / 1000.0 / count as f64,
1465                EXEC_TIME.load(Ordering::Relaxed) as f64 / 1000.0,
1466                EXEC_TIME.load(Ordering::Relaxed) as f64 / 1000.0 / count as f64,
1467                GRAPH_REBUILD_TIME.load(Ordering::Relaxed) as f64 / 1000.0,
1468                GRAPH_REBUILD_TIME.load(Ordering::Relaxed) as f64 / 1000.0 / count as f64,
1469                VERIFY_TIME.load(Ordering::Relaxed) as f64 / 1000.0,
1470                VERIFY_TIME.load(Ordering::Relaxed) as f64 / 1000.0 / count as f64,
1471                cpu_total,
1472                wall_ms,
1473                parallelism,
1474            );
1475            let _ = std::fs::write("/tmp/precheck_timing.txt", &timing);
1476        }
1477
1478        result
1479    }
1480
1481    /// Get the SuggestService for direct access.
1482    pub fn suggest_service(&self) -> &SuggestService {
1483        &self.suggest
1484    }
1485
1486    /// Get the SuggestConfig.
1487    pub fn suggest_config(&self) -> &SuggestConfig {
1488        &self.suggest_config
1489    }
1490
1491    /// Take the suggestion store contents for preservation across reload.
1492    ///
1493    /// Returns the current store, leaving an empty store in place.
1494    pub fn take_suggest_store(&self) -> ryo_suggest::SuggestStore {
1495        self.suggest.take_store()
1496    }
1497
1498    /// Restore suggestion store contents after reload.
1499    pub fn restore_suggest_store(&self, store: ryo_suggest::SuggestStore) {
1500        self.suggest.restore_store(store);
1501    }
1502
1503    /// Apply suggestions by ID.
1504    ///
1505    /// Takes suggestion IDs, generates MutationSpecs, and executes them.
1506    /// Returns information about which suggestions were successfully applied.
1507    ///
1508    /// # Example
1509    ///
1510    /// ```ignore
1511    /// let response = api.suggest_apply(SuggestApplyRequest {
1512    ///     ids: vec!["S001g0".to_string(), "S002g0".to_string()],
1513    ///     dry_run: false,
1514    ///     check_syntax: true,
1515    /// })?;
1516    ///
1517    /// if response.success {
1518    ///     println!("Applied {} suggestions", response.applied_ids.len());
1519    /// }
1520    /// ```
1521    pub fn suggest_apply(
1522        &mut self,
1523        request: SuggestApplyRequest,
1524    ) -> ApiResult<SuggestApplyResponse> {
1525        let mut response = SuggestApplyResponse::default();
1526        let mut all_specs = Vec::new();
1527        let mut valid_ids = Vec::new();
1528
1529        // Phase 1: Parse IDs and collect MutationSpecs
1530        for id_str in &request.ids {
1531            // Parse the suggestion ID
1532            let suggest_id: SuggestId = match id_str.parse() {
1533                Ok(id) => id,
1534                Err(e) => {
1535                    response
1536                        .failed
1537                        .push((id_str.clone(), format!("Invalid ID format: {}", e)));
1538                    continue;
1539                }
1540            };
1541
1542            // Check if suggestion exists and is valid
1543            if !self.suggest.is_valid(suggest_id) {
1544                response.failed.push((
1545                    id_str.clone(),
1546                    "Suggestion not found or expired".to_string(),
1547                ));
1548                continue;
1549            }
1550
1551            // Generate MutationSpecs for this suggestion
1552            match self.suggest.to_mutation_specs(suggest_id, &self.context) {
1553                Some(specs) if !specs.is_empty() => {
1554                    all_specs.extend(specs);
1555                    valid_ids.push((id_str.clone(), suggest_id));
1556                }
1557                Some(_) => {
1558                    response
1559                        .failed
1560                        .push((id_str.clone(), "No mutations generated".to_string()));
1561                }
1562                None => {
1563                    response
1564                        .failed
1565                        .push((id_str.clone(), "Failed to generate mutations".to_string()));
1566                }
1567            }
1568        }
1569
1570        // If no valid specs, return early
1571        if all_specs.is_empty() {
1572            if response.failed.is_empty() {
1573                response.error = Some("No suggestions to apply".to_string());
1574            }
1575            return Ok(response);
1576        }
1577
1578        // Phase 2: Dry run — execute on forked context, report real changes
1579        if request.dry_run {
1580            let mut forked_ctx = self.context.fork_clone();
1581            let blueprint = ParallelBlueprint::from_mutations(all_specs);
1582            let executor = BlueprintExecutor::default();
1583            let exec_result = executor.execute_v2(&blueprint, &mut forked_ctx);
1584
1585            if !exec_result.success {
1586                response.error = exec_result.error.clone();
1587                for (id_str, _) in &valid_ids {
1588                    response.failed.push((
1589                        id_str.clone(),
1590                        exec_result
1591                            .error
1592                            .clone()
1593                            .unwrap_or_else(|| "Execution failed".to_string()),
1594                    ));
1595                }
1596                return Ok(response);
1597            }
1598
1599            response.success = true;
1600            response.total_changes = exec_result.total_changes;
1601            response.files_modified = exec_result.modified_files.len();
1602            response.modified_files = exec_result
1603                .modified_files
1604                .iter()
1605                .map(|p| p.to_absolute())
1606                .collect();
1607            for (id_str, _) in &valid_ids {
1608                response.applied_ids.push(id_str.clone());
1609            }
1610            return Ok(response);
1611        }
1612
1613        // Phase 3: Real apply — execute on self.context
1614        let blueprint = ParallelBlueprint::from_mutations(all_specs);
1615        let executor = BlueprintExecutor::default();
1616        let exec_result = executor.execute_v2(&blueprint, &mut self.context);
1617
1618        if !exec_result.success {
1619            response.error = exec_result.error.clone();
1620            for (id_str, _) in &valid_ids {
1621                response.failed.push((
1622                    id_str.clone(),
1623                    exec_result
1624                        .error
1625                        .clone()
1626                        .unwrap_or_else(|| "Execution failed".to_string()),
1627                ));
1628            }
1629            return Ok(response);
1630        }
1631
1632        // Phase 3.5: Sync files and rebuild analysis graphs
1633        let modified_workspace_paths =
1634            BlueprintExecutor::sync_files_and_rebuild(&exec_result, &mut self.context)?;
1635
1636        // Phase 4: Collect results
1637        response.success = true;
1638        response.total_changes = exec_result.total_changes;
1639
1640        // Collect modified files (convert WorkspaceFilePath to PathBuf)
1641        let modified_files: Vec<PathBuf> = modified_workspace_paths
1642            .iter()
1643            .map(|p| p.to_absolute())
1644            .collect();
1645        response.files_modified = modified_files.len();
1646        response.modified_files = modified_files.clone();
1647
1648        // Mark successfully applied IDs
1649        for (id_str, _) in &valid_ids {
1650            response.applied_ids.push(id_str.clone());
1651        }
1652
1653        // Phase 5: Write to disk
1654        // INVARIANT: Only write when actual mutations occurred.
1655        // total_changes == 0 means no AST was modified → no file should be touched.
1656        if exec_result.total_changes > 0 {
1657            for workspace_path in &modified_workspace_paths {
1658                if let Some(file) = self.context.files.get(workspace_path) {
1659                    let source = match file.to_source() {
1660                        Ok(s) => s,
1661                        Err(e) => {
1662                            response.syntax_errors.push(format!(
1663                                "Failed to generate source for {}: {}",
1664                                workspace_path.to_absolute().display(),
1665                                e
1666                            ));
1667                            continue;
1668                        }
1669                    };
1670                    // Use WorkspaceFilePath::write() to ensure parent dirs are created
1671                    if let Err(e) = workspace_path.write(&source) {
1672                        response.syntax_errors.push(format!(
1673                            "Failed to write {}: {}",
1674                            workspace_path.to_absolute().display(),
1675                            e
1676                        ));
1677                    }
1678                }
1679            }
1680        }
1681
1682        // Close the applied suggestions
1683        for (_, suggest_id) in &valid_ids {
1684            self.suggest.close(*suggest_id, "Applied via suggest_apply");
1685        }
1686
1687        // Phase 5: Syntax check (if requested)
1688        if request.check_syntax {
1689            for workspace_path in &modified_workspace_paths {
1690                if let Some(file) = self.context.files.get(workspace_path) {
1691                    let source = match file.to_source() {
1692                        Ok(s) => s,
1693                        Err(e) => {
1694                            response.syntax_errors.push(format!(
1695                                "{}: source generation failed: {}",
1696                                workspace_path.to_absolute().display(),
1697                                e
1698                            ));
1699                            continue;
1700                        }
1701                    };
1702                    if let Err(e) = syn::parse_file(&source) {
1703                        let abs_path = workspace_path.to_absolute();
1704                        response
1705                            .syntax_errors
1706                            .push(format!("{}: {}", abs_path.display(), e));
1707                    }
1708                }
1709            }
1710        }
1711
1712        Ok(response)
1713    }
1714
1715    /// Generate code graph summary.
1716    ///
1717    /// # Example
1718    ///
1719    /// ```ignore
1720    /// let response = api.graph_summary(GraphSummaryRequest::new())?;
1721    /// println!("{}", response.content);
1722    /// ```
1723    pub fn graph_summary(&self, request: GraphSummaryRequest) -> ApiResult<GraphSummaryResponse> {
1724        use ryo_analysis::SymbolKind;
1725
1726        let registry = &self.context.registry;
1727        let max_items = request.max_items.unwrap_or(50);
1728
1729        // Collect statistics
1730        let mut functions = 0usize;
1731        let mut structs = 0usize;
1732        let mut enums = 0usize;
1733        let mut traits = 0usize;
1734        let mut impls = 0usize;
1735
1736        for (id, _) in registry.iter() {
1737            if let Some(kind) = registry.kind(id) {
1738                match kind {
1739                    SymbolKind::Function => functions += 1,
1740                    SymbolKind::Struct => structs += 1,
1741                    SymbolKind::Enum => enums += 1,
1742                    SymbolKind::Trait => traits += 1,
1743                    SymbolKind::Impl => impls += 1,
1744                    _ => {}
1745                }
1746            }
1747        }
1748
1749        let node_count = self.context.code_graph.node_count();
1750        let edge_count = self.context.code_graph.edge_count();
1751        let file_count = self.context.files.len();
1752
1753        // Build summary string
1754        let mut out = String::new();
1755        out.push_str("=== CodeGraph ===\n");
1756
1757        if request.detailed {
1758            out.push_str(&format!("Nodes: {}\n", node_count));
1759            out.push_str(&format!("Edges: {}\n", edge_count));
1760            out.push_str(&format!(
1761                "Types: {} functions, {} structs, {} enums, {} traits, {} impls\n",
1762                functions, structs, enums, traits, impls
1763            ));
1764            out.push_str(&format!("Files: {}\n", file_count));
1765        } else if !request.compact {
1766            out.push_str(&format!(
1767                "Nodes: {}, Edges: {}, Files: {}\n",
1768                node_count, edge_count, file_count
1769            ));
1770        }
1771
1772        out.push('\n');
1773
1774        // Functions section
1775        let fn_iter: Vec<_> = registry
1776            .iter_by_kind(SymbolKind::Function)
1777            .take(max_items)
1778            .collect();
1779        if !fn_iter.is_empty() {
1780            out.push_str("[Functions]\n");
1781            for id in &fn_iter {
1782                let vis = registry.visibility(*id).map_or("", |v| {
1783                    if matches!(v, ryo_analysis::Visibility::Public) {
1784                        "pub "
1785                    } else {
1786                        ""
1787                    }
1788                });
1789                let name = registry
1790                    .resolve(*id)
1791                    .map(|p| p.name().to_string())
1792                    .unwrap_or_default();
1793                if request.compact {
1794                    out.push_str(&format!("  {}fn {}\n", vis, name));
1795                } else {
1796                    out.push_str(&format!("  {}fn {} [{:?}]\n", vis, name, id));
1797                }
1798            }
1799            if functions > max_items {
1800                out.push_str(&format!("  ... and {} more\n", functions - max_items));
1801            }
1802            out.push('\n');
1803        }
1804
1805        // Structs section
1806        let struct_iter: Vec<_> = registry
1807            .iter_by_kind(SymbolKind::Struct)
1808            .take(max_items)
1809            .collect();
1810        if !struct_iter.is_empty() {
1811            out.push_str("[Structs]\n");
1812            for id in &struct_iter {
1813                let vis = registry.visibility(*id).map_or("", |v| {
1814                    if matches!(v, ryo_analysis::Visibility::Public) {
1815                        "pub "
1816                    } else {
1817                        ""
1818                    }
1819                });
1820                let name = registry
1821                    .resolve(*id)
1822                    .map(|p| p.name().to_string())
1823                    .unwrap_or_default();
1824                if request.compact {
1825                    out.push_str(&format!("  {}struct {}\n", vis, name));
1826                } else {
1827                    out.push_str(&format!("  {}struct {} [{:?}]\n", vis, name, id));
1828                }
1829            }
1830            if structs > max_items {
1831                out.push_str(&format!("  ... and {} more\n", structs - max_items));
1832            }
1833            out.push('\n');
1834        }
1835
1836        // Enums section
1837        let enum_iter: Vec<_> = registry
1838            .iter_by_kind(SymbolKind::Enum)
1839            .take(max_items)
1840            .collect();
1841        if !enum_iter.is_empty() {
1842            out.push_str("[Enums]\n");
1843            for id in &enum_iter {
1844                let vis = registry.visibility(*id).map_or("", |v| {
1845                    if matches!(v, ryo_analysis::Visibility::Public) {
1846                        "pub "
1847                    } else {
1848                        ""
1849                    }
1850                });
1851                let name = registry
1852                    .resolve(*id)
1853                    .map(|p| p.name().to_string())
1854                    .unwrap_or_default();
1855                if request.compact {
1856                    out.push_str(&format!("  {}enum {}\n", vis, name));
1857                } else {
1858                    out.push_str(&format!("  {}enum {} [{:?}]\n", vis, name, id));
1859                }
1860            }
1861            out.push('\n');
1862        }
1863
1864        // Traits section
1865        let trait_iter: Vec<_> = registry
1866            .iter_by_kind(SymbolKind::Trait)
1867            .take(max_items)
1868            .collect();
1869        if !trait_iter.is_empty() {
1870            out.push_str("[Traits]\n");
1871            for id in &trait_iter {
1872                let vis = registry.visibility(*id).map_or("", |v| {
1873                    if matches!(v, ryo_analysis::Visibility::Public) {
1874                        "pub "
1875                    } else {
1876                        ""
1877                    }
1878                });
1879                let name = registry
1880                    .resolve(*id)
1881                    .map(|p| p.name().to_string())
1882                    .unwrap_or_default();
1883                if request.compact {
1884                    out.push_str(&format!("  {}trait {}\n", vis, name));
1885                } else {
1886                    out.push_str(&format!("  {}trait {} [{:?}]\n", vis, name, id));
1887                }
1888            }
1889            out.push('\n');
1890        }
1891
1892        Ok(GraphSummaryResponse {
1893            content: out,
1894            build_time_ms: 0, // Server already has context loaded
1895            node_count,
1896            edge_count,
1897            file_count,
1898        })
1899    }
1900
1901    /// Analyze cascade effects of changing a symbol.
1902    ///
1903    /// # Example
1904    ///
1905    /// ```ignore
1906    /// let response = api.graph_cascade(CascadeRequest {
1907    ///     id: "165v1".to_string(),
1908    ///     uuid: None, // or Some("550e8400-e29b-41d4-a716-446655440000".to_string())
1909    ///     depth: Some(3),
1910    /// })?;
1911    /// ```
1912    pub fn graph_cascade(&self, request: CascadeRequest) -> ApiResult<CascadeResponse> {
1913        let _depth = request.depth.unwrap_or(3);
1914
1915        // Resolve SymbolId (UUID takes precedence over id)
1916        let symbol_id = self.resolve_symbol_id_required(request.uuid.as_deref(), &request.id)?;
1917
1918        let symbol_kind = self.context.registry.kind(symbol_id);
1919
1920        // Get callers from code graph
1921        let callers: Vec<String> = self
1922            .context
1923            .code_graph
1924            .callers_of(symbol_id)
1925            .filter_map(|id| {
1926                self.context
1927                    .registry
1928                    .resolve(id)
1929                    .map(|path| format!("{} [{:?}]", path, id))
1930            })
1931            .collect();
1932
1933        // Get type users (types that reference this symbol, via TypeFlow)
1934        let users: Vec<String> = self
1935            .context
1936            .typeflow_graph
1937            .type_users(symbol_id)
1938            .filter_map(|id| {
1939                self.context
1940                    .registry
1941                    .resolve(id)
1942                    .map(|path| format!("{} [{:?}]", path, id))
1943            })
1944            .collect();
1945
1946        // For enums: scan PureFiles for functions containing match expressions
1947        // on this enum. The match_expr_index in CodeGraphV2 is not yet populated
1948        // by the analysis pipeline, so we scan at query time.
1949        let match_functions: Vec<String> = if symbol_kind == Some(SymbolKind::Enum) {
1950            let enum_name = self
1951                .context
1952                .registry
1953                .resolve(symbol_id)
1954                .map(|p| p.name().to_string())
1955                .unwrap_or_default();
1956            if enum_name.is_empty() {
1957                Vec::new()
1958            } else {
1959                self.find_match_functions_for_enum(&enum_name)
1960            }
1961        } else {
1962            Vec::new()
1963        };
1964
1965        // For types with TypeFlow definitions: find containing types
1966        let containing_types: Vec<String> = {
1967            let impact = self.context.typeflow_graph.impact(symbol_id);
1968            impact
1969                .containing_types
1970                .iter()
1971                .filter_map(|id| {
1972                    self.context
1973                        .registry
1974                        .resolve(*id)
1975                        .map(|path| format!("{} [{:?}]", path, id))
1976                })
1977                .collect()
1978        };
1979
1980        let display_name = self
1981            .context
1982            .registry
1983            .resolve(symbol_id)
1984            .map(|p| p.name().to_string())
1985            .unwrap_or_else(|| request.id.clone());
1986
1987        Ok(CascadeResponse {
1988            display_name,
1989            callers,
1990            users,
1991            match_functions,
1992            containing_types,
1993        })
1994    }
1995
1996    /// Scan PureFiles for functions/methods that contain match expressions on a given enum.
1997    fn find_match_functions_for_enum(&self, enum_name: &str) -> Vec<String> {
1998        use crate::discover::pattern_contains_enum;
1999        use ryo_source::pure::{PureExpr, PureImplItem, PureItem, PureStmt};
2000
2001        let mut results = Vec::new();
2002
2003        for (_path, file) in self.context.files().iter() {
2004            let wfp = _path;
2005            let crate_name = wfp.crate_name();
2006            let resolver = ryo_symbol::SymbolPathResolver::new(crate_name.as_str());
2007            let mod_path = resolver.module_path_str(wfp);
2008
2009            for item in &file.items {
2010                match item {
2011                    PureItem::Fn(func) if block_has_match_on_enum(&func.body, enum_name) => {
2012                        let fn_path = format!("{}::{}", mod_path, func.name);
2013                        results.push(fn_path);
2014                    }
2015                    PureItem::Impl(impl_block) => {
2016                        let type_name = &impl_block.self_ty;
2017                        for impl_item in &impl_block.items {
2018                            if let PureImplItem::Fn(func) = impl_item {
2019                                if block_has_match_on_enum(&func.body, enum_name) {
2020                                    let fn_path =
2021                                        format!("{}::{}::{}", mod_path, type_name, func.name);
2022                                    results.push(fn_path);
2023                                }
2024                            }
2025                        }
2026                    }
2027                    _ => {}
2028                }
2029            }
2030        }
2031
2032        /// Check if a PureBlock contains a match expression on the given enum.
2033        fn block_has_match_on_enum(block: &ryo_source::pure::PureBlock, enum_name: &str) -> bool {
2034            for stmt in &block.stmts {
2035                let expr = match stmt {
2036                    PureStmt::Expr(e) | PureStmt::Semi(e) => Some(e),
2037                    PureStmt::Local { init: Some(e), .. } => Some(e),
2038                    _ => None,
2039                };
2040                if let Some(expr) = expr {
2041                    if expr_has_match_on_enum(expr, enum_name) {
2042                        return true;
2043                    }
2044                }
2045            }
2046            false
2047        }
2048
2049        /// Recursively check if an expression contains a match on the given enum.
2050        fn expr_has_match_on_enum(expr: &PureExpr, enum_name: &str) -> bool {
2051            match expr {
2052                PureExpr::Match { arms, expr: inner } => {
2053                    let has_enum = arms
2054                        .iter()
2055                        .any(|arm| pattern_contains_enum(&arm.pattern, enum_name));
2056                    if has_enum {
2057                        return true;
2058                    }
2059                    expr_has_match_on_enum(inner, enum_name)
2060                }
2061                PureExpr::Block { block, .. }
2062                | PureExpr::Unsafe(block)
2063                | PureExpr::Async { body: block, .. }
2064                | PureExpr::Loop { body: block, .. } => block_has_match_on_enum(block, enum_name),
2065                PureExpr::If {
2066                    cond,
2067                    then_branch,
2068                    else_branch,
2069                } => {
2070                    expr_has_match_on_enum(cond, enum_name)
2071                        || block_has_match_on_enum(then_branch, enum_name)
2072                        || else_branch
2073                            .as_ref()
2074                            .is_some_and(|e| expr_has_match_on_enum(e, enum_name))
2075                }
2076                _ => false,
2077            }
2078        }
2079
2080        results
2081    }
2082
2083    /// Type relationship analysis using TypeFlowGraph.
2084    ///
2085    /// Analyzes type definitions → usage relationships with O(1) impact analysis
2086    /// via inverted index.
2087    pub fn graph_type(&self, request: TypeAnalysisRequest) -> ApiResult<TypeAnalysisResponse> {
2088        use types::{TypeAnalysisMode as ApiMode, TypeImpactInfo, TypeUsageInfo};
2089
2090        // Resolve symbol: UUID > SymbolId > name
2091        let (symbol_id, display_name) = if let Some(sid) =
2092            self.resolve_symbol_id(request.uuid.as_deref(), request.id.as_deref(), None)?
2093        {
2094            let name = self
2095                .context
2096                .registry
2097                .resolve(sid)
2098                .map(|p| p.name().to_string())
2099                .unwrap_or_else(|| format!("{:?}", sid));
2100            (sid, name)
2101        } else if let Some(ref name) = request.name {
2102            let symbol_ids = self.context.registry.find_by_name(name);
2103            match symbol_ids.len() {
2104                0 => return Err(ApiError::NotFound(name.clone())),
2105                1 => (symbol_ids[0], name.clone()),
2106                _ => {
2107                    // Multiple symbols with the same name — report all candidates
2108                    let candidates: Vec<String> = symbol_ids
2109                        .iter()
2110                        .filter_map(|&sid| {
2111                            let path = self.context.registry.resolve(sid)?;
2112                            let kind = self.context.registry.kind(sid)?;
2113                            Some(format!("  {} ({:?}) --id {}", path, kind, sid))
2114                        })
2115                        .collect();
2116                    return Err(ApiError::InvalidGoal(format!(
2117                        "Ambiguous name '{}': {} symbols found. Use --id to specify:\n{}",
2118                        name,
2119                        symbol_ids.len(),
2120                        candidates.join("\n")
2121                    )));
2122                }
2123            }
2124        } else {
2125            return Err(ApiError::InvalidGoal(
2126                "Either 'name', 'id', or 'uuid' must be provided".to_string(),
2127            ));
2128        };
2129
2130        let typeflow = &self.context.typeflow_graph;
2131        let registry = &self.context.registry;
2132
2133        // Get basic info (always)
2134        let mod_path = registry.resolve(symbol_id).map(|p| p.to_string());
2135        let kind = typeflow.definition(symbol_id).and_then(|def_node| {
2136            typeflow
2137                .get_definition(def_node)
2138                .map(|d| format!("{:?}", d.kind))
2139        });
2140
2141        // Usage count (always needed for summary)
2142        let usage_count = typeflow.usages(symbol_id).count();
2143
2144        // Collect usages (Usage/Impact modes only)
2145        let usage_infos: Vec<TypeUsageInfo> = match request.mode {
2146            ApiMode::Usage | ApiMode::Impact => typeflow
2147                .usages(symbol_id)
2148                .take(20)
2149                .filter_map(|usage_node| {
2150                    typeflow.get_usage(usage_node).map(|u| TypeUsageInfo {
2151                        context: format!("{:?}", u.context),
2152                        ref_kind: format!("{:?}", u.ref_kind),
2153                        container: u.container.and_then(|cid| {
2154                            self.context.registry.resolve(cid).map(|p| p.to_string())
2155                        }),
2156                    })
2157                })
2158                .collect(),
2159            ApiMode::Definition => Vec::new(),
2160        };
2161
2162        // Get impact info (Impact mode only)
2163        let impact = match request.mode {
2164            ApiMode::Impact => {
2165                let impact = typeflow.impact(symbol_id);
2166                let containing: Vec<String> = impact
2167                    .containing_types
2168                    .iter()
2169                    .take(10)
2170                    .filter_map(|&id| registry.resolve(id).map(|p| format!("{} [{:?}]", p, id)))
2171                    .collect();
2172                Some(TypeImpactInfo {
2173                    direct_usages: impact.direct_usages.len(),
2174                    bound_usages: impact.bound_usages.len(),
2175                    containing_types: containing,
2176                })
2177            }
2178            _ => None,
2179        };
2180
2181        // Definition details (Definition mode only)
2182        let (fields, variants, params, return_type, methods, generics, attrs) = match request.mode {
2183            ApiMode::Definition => self.collect_definition_details(symbol_id),
2184            _ => Default::default(),
2185        };
2186
2187        // Get supertraits (for traits only, Definition mode)
2188        let supertraits = match request.mode {
2189            ApiMode::Definition => self
2190                .context
2191                .detail_store
2192                .trait_(symbol_id)
2193                .map(|t| t.supertraits.clone())
2194                .unwrap_or_default(),
2195            _ => Vec::new(),
2196        };
2197
2198        // Get implementors (for traits only, Definition mode)
2199        let implementors: Vec<String> = match request.mode {
2200            ApiMode::Definition => self
2201                .context
2202                .code_graph
2203                .implementors_of(symbol_id)
2204                .take(20)
2205                .filter_map(|impl_id| {
2206                    registry
2207                        .resolve(impl_id)
2208                        .map(|p| format!("{} [{:?}]", p, impl_id))
2209                })
2210                .collect(),
2211            _ => Vec::new(),
2212        };
2213
2214        Ok(TypeAnalysisResponse {
2215            symbol_id: format!("{:?}", symbol_id),
2216            display_name,
2217            mod_path,
2218            kind,
2219            usage_count,
2220            usages: usage_infos,
2221            impact,
2222            supertraits,
2223            implementors,
2224            fields,
2225            variants,
2226            params,
2227            return_type,
2228            methods,
2229            generics,
2230            attrs,
2231        })
2232    }
2233
2234    /// Collect definition details from DetailStore for Definition mode.
2235    fn collect_definition_details(&self, symbol_id: ryo_analysis::SymbolId) -> DefinitionDetails {
2236        let ds = &self.context.detail_store;
2237
2238        // Struct
2239        if let Some(detail) = ds.struct_(symbol_id) {
2240            let fields = detail
2241                .fields
2242                .iter()
2243                .map(|f| types::TypeFieldInfo {
2244                    name: f.name.clone(),
2245                    ty: f.ty.clone(),
2246                    is_public: f.is_public,
2247                })
2248                .collect();
2249            let generics = if detail.generics.is_empty() {
2250                None
2251            } else {
2252                Some(format_generics(&detail.generics))
2253            };
2254            return (
2255                fields,
2256                Vec::new(),
2257                Vec::new(),
2258                None,
2259                Vec::new(),
2260                generics,
2261                detail
2262                    .attrs
2263                    .iter()
2264                    .filter(|a| !a.starts_with("doc"))
2265                    .cloned()
2266                    .collect(),
2267            );
2268        }
2269
2270        // Enum
2271        if let Some(detail) = ds.enum_(symbol_id) {
2272            let variants = detail
2273                .variants
2274                .iter()
2275                .map(|v| types::TypeVariantInfo {
2276                    name: v.name.clone(),
2277                    fields: v
2278                        .fields
2279                        .iter()
2280                        .map(|f| types::TypeFieldInfo {
2281                            name: f.name.clone(),
2282                            ty: f.ty.clone(),
2283                            is_public: f.is_public,
2284                        })
2285                        .collect(),
2286                })
2287                .collect();
2288            let generics = if detail.generics.is_empty() {
2289                None
2290            } else {
2291                Some(format_generics(&detail.generics))
2292            };
2293            return (
2294                Vec::new(),
2295                variants,
2296                Vec::new(),
2297                None,
2298                Vec::new(),
2299                generics,
2300                detail
2301                    .attrs
2302                    .iter()
2303                    .filter(|a| !a.starts_with("doc"))
2304                    .cloned()
2305                    .collect(),
2306            );
2307        }
2308
2309        // Function/Method
2310        if let Some(detail) = ds.function(symbol_id) {
2311            let params = detail
2312                .params
2313                .iter()
2314                .filter(|p| !p.is_self)
2315                .map(|p| types::TypeParamInfo {
2316                    name: p.name.clone(),
2317                    ty: p.ty.clone(),
2318                })
2319                .collect();
2320            let generics = if detail.generics.is_empty() {
2321                None
2322            } else {
2323                Some(format_generics(&detail.generics))
2324            };
2325            return (
2326                Vec::new(),
2327                Vec::new(),
2328                params,
2329                detail.return_type.clone(),
2330                Vec::new(),
2331                generics,
2332                detail
2333                    .attrs
2334                    .iter()
2335                    .filter(|a| !a.starts_with("doc"))
2336                    .cloned()
2337                    .collect(),
2338            );
2339        }
2340
2341        // Trait
2342        if let Some(detail) = ds.trait_(symbol_id) {
2343            let generics = if detail.generics.is_empty() {
2344                None
2345            } else {
2346                Some(format_generics(&detail.generics))
2347            };
2348            return (
2349                Vec::new(),
2350                Vec::new(),
2351                Vec::new(),
2352                None,
2353                detail.methods.clone(),
2354                generics,
2355                detail
2356                    .attrs
2357                    .iter()
2358                    .filter(|a| !a.starts_with("doc"))
2359                    .cloned()
2360                    .collect(),
2361            );
2362        }
2363
2364        Default::default()
2365    }
2366
2367    /// Data flow analysis (provenance and impact tracking).
2368    ///
2369    /// Tracks data provenance (where values come from) and impact
2370    /// (where values flow to). Identifies sources and sinks.
2371    pub fn graph_flow(&self, request: FlowAnalysisRequest) -> ApiResult<FlowAnalysisResponse> {
2372        use ryo_analysis::VarId;
2373        use types::{FlowAnalysisMode as ApiMode, VarInfo};
2374
2375        let dataflow = &self.context.dataflow_graph;
2376        let registry = &self.context.registry;
2377
2378        // Helper to convert VarId to VarInfo
2379        let var_to_info = |var_id: VarId| -> VarInfo {
2380            let name = dataflow.var_name(var_id).unwrap_or("unknown").to_string();
2381            let (kind, parent) = dataflow
2382                .var(var_id)
2383                .map(|d| {
2384                    let kind = format!("{:?}", d.kind);
2385                    let parent = registry
2386                        .resolve(d.parent)
2387                        .map(|p| format!("{} ({:?})", p.name(), d.parent))
2388                        .unwrap_or_else(|| format!("{:?}", d.parent));
2389                    (kind, parent)
2390                })
2391                .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
2392            let symbol_path = dataflow
2393                .var_to_symbol(var_id)
2394                .and_then(|s| registry.resolve(s))
2395                .map(|p| p.to_string());
2396            VarInfo {
2397                name,
2398                kind,
2399                parent,
2400                symbol_path,
2401            }
2402        };
2403
2404        let symbol_id =
2405            self.resolve_symbol_id(request.uuid.as_deref(), request.id.as_deref(), None)?;
2406        let (matching_vars, display_name) = dataflow
2407            .resolve_vars(symbol_id, request.name.as_deref(), registry)
2408            .ok_or_else(|| {
2409                ApiError::InvalidGoal("Either 'name', 'id', or 'uuid' must be provided".to_string())
2410            })?;
2411
2412        if matching_vars.is_empty() {
2413            return Err(self.no_vars_error(request.name.as_deref(), &display_name, registry));
2414        }
2415
2416        let found_vars: Vec<VarInfo> = matching_vars
2417            .iter()
2418            .take(5)
2419            .map(|&v| var_to_info(v))
2420            .collect();
2421
2422        let mut provenance = Vec::new();
2423        let mut impact = Vec::new();
2424        let mut sources = Vec::new();
2425        let mut sinks = Vec::new();
2426
2427        match request.mode {
2428            ApiMode::Provenance | ApiMode::Chain => {
2429                for &var_id in matching_vars.iter().take(3) {
2430                    for prov_id in dataflow.provenance(var_id).into_iter().take(5) {
2431                        provenance.push(var_to_info(prov_id));
2432                    }
2433                }
2434            }
2435            _ => {}
2436        }
2437
2438        match request.mode {
2439            ApiMode::Impact | ApiMode::Chain => {
2440                for &var_id in matching_vars.iter().take(3) {
2441                    for impact_id in dataflow.impact(var_id).into_iter().take(5) {
2442                        impact.push(var_to_info(impact_id));
2443                    }
2444                }
2445            }
2446            _ => {}
2447        }
2448
2449        if matches!(request.mode, ApiMode::Sources) {
2450            sources = dataflow
2451                .iter_vars()
2452                .filter(|(var_id, _)| dataflow.incoming(*var_id).is_empty())
2453                .take(20)
2454                .map(|(var_id, _)| var_to_info(var_id))
2455                .collect();
2456        }
2457
2458        if matches!(request.mode, ApiMode::Sinks) {
2459            sinks = dataflow
2460                .iter_vars()
2461                .filter(|(var_id, _)| dataflow.outgoing(*var_id).is_empty())
2462                .take(20)
2463                .map(|(var_id, _)| var_to_info(var_id))
2464                .collect();
2465        }
2466
2467        Ok(FlowAnalysisResponse {
2468            display_name,
2469            found_vars,
2470            provenance,
2471            impact,
2472            sources,
2473            sinks,
2474        })
2475    }
2476
2477    /// Borrow analysis (conflict detection, use-after-move).
2478    ///
2479    /// Tracks active borrows and detects potential conflicts or
2480    /// use-after-move errors.
2481    pub fn graph_borrow(
2482        &self,
2483        request: BorrowAnalysisRequest,
2484    ) -> ApiResult<BorrowAnalysisResponse> {
2485        use ryo_analysis::BorrowKind;
2486        use types::BorrowStatus;
2487
2488        let dataflow = &self.context.dataflow_graph;
2489        let registry = &self.context.registry;
2490        let tracker = dataflow.borrow_tracker();
2491
2492        let symbol_id =
2493            self.resolve_symbol_id(request.uuid.as_deref(), request.id.as_deref(), None)?;
2494        let (matching_vars, display_name) = dataflow
2495            .resolve_vars(symbol_id, request.name.as_deref(), registry)
2496            .ok_or_else(|| {
2497                ApiError::InvalidGoal("Either 'name', 'id', or 'uuid' must be provided".to_string())
2498            })?;
2499
2500        if matching_vars.is_empty() {
2501            return Err(self.no_vars_error(request.name.as_deref(), &display_name, registry));
2502        }
2503
2504        let mut statuses = Vec::new();
2505
2506        for &var_id in matching_vars.iter().take(10) {
2507            let var_data = dataflow.var(var_id);
2508            let var_name = dataflow.var_name(var_id).unwrap_or("unknown").to_string();
2509            let line = var_data.map(|d| d.line).unwrap_or(0);
2510
2511            let parent_info = var_data
2512                .and_then(|d| {
2513                    registry
2514                        .resolve(d.parent)
2515                        .map(|p| format!("{} ({:?})", p.name(), d.parent))
2516                })
2517                .unwrap_or_default();
2518
2519            let mut_conflicts = tracker.conflicts(var_id, BorrowKind::Mutable, line);
2520            let shared_conflicts = tracker.conflicts(var_id, BorrowKind::Shared, line);
2521
2522            let has_conflict = !mut_conflicts.is_empty() || !shared_conflicts.is_empty();
2523            let errors: Vec<String> = mut_conflicts
2524                .iter()
2525                .chain(shared_conflicts.iter())
2526                .map(|c| format!("`{}`: {}", var_name, c))
2527                .collect();
2528
2529            if !request.conflicts_only || has_conflict {
2530                statuses.push(BorrowStatus {
2531                    var_name,
2532                    line,
2533                    parent_info,
2534                    has_conflict,
2535                    errors,
2536                });
2537            }
2538        }
2539
2540        Ok(BorrowAnalysisResponse {
2541            display_name,
2542            found_count: matching_vars.len(),
2543            statuses,
2544        })
2545    }
2546
2547    /// Lock analysis (critical sections and optimization).
2548    ///
2549    /// Analyzes lock usage patterns, critical section sizes,
2550    /// and provides optimization suggestions.
2551    pub fn graph_lock(&self, request: LockAnalysisRequest) -> ApiResult<LockAnalysisResponse> {
2552        use ryo_analysis::LockGranularityAnalyzerV2;
2553        use types::{LockAcquisition, LockStats, LockSuggestionInfo};
2554
2555        let dataflow = &self.context.dataflow_graph;
2556        let registry = &self.context.registry;
2557        let tracker = dataflow.lock_tracker();
2558        let analyzer = LockGranularityAnalyzerV2::new(tracker);
2559
2560        // Resolve symbol_id and display_name
2561        let resolved_symbol_id =
2562            self.resolve_symbol_id(request.uuid.as_deref(), request.id.as_deref(), None)?;
2563
2564        let display_name = if let Some(symbol_id) = resolved_symbol_id {
2565            registry
2566                .resolve(symbol_id)
2567                .map(|p| p.name().to_string())
2568                .unwrap_or_else(|| format!("{:?}", symbol_id))
2569        } else if let Some(ref name) = request.name {
2570            name.clone()
2571        } else {
2572            return Err(ApiError::InvalidGoal(
2573                "Either 'name', 'id', or 'uuid' must be provided".to_string(),
2574            ));
2575        };
2576
2577        // Get statistics
2578        let stats_data = analyzer.stats();
2579        let stats = LockStats {
2580            total_locks: stats_data.total_locks as u32,
2581            mutex_count: stats_data.mutex_count as u32,
2582            rwlock_count: stats_data.rwlock_count as u32,
2583            refcell_count: stats_data.refcell_count as u32,
2584            total_field_accesses: stats_data.total_field_accesses as u32,
2585            max_cs_span: stats_data.max_cs_span,
2586        };
2587
2588        // Get acquisitions: --id mode filters by owner function, name mode uses pattern match
2589        let acquisitions: Vec<LockAcquisition> = if let Some(symbol_id) = resolved_symbol_id {
2590            tracker
2591                .acquisitions_by_owner(symbol_id)
2592                .into_iter()
2593                .take(10)
2594                .map(|acq| LockAcquisition {
2595                    lock_name: acq.lock_name.clone(),
2596                    lock_type: format!("{:?}", acq.lock_type),
2597                    line: acq.line,
2598                })
2599                .collect()
2600        } else {
2601            tracker
2602                .acquisitions()
2603                .iter()
2604                .filter(|acq| matches_lock_pattern_server(acq, &display_name))
2605                .take(10)
2606                .map(|acq| LockAcquisition {
2607                    lock_name: acq.lock_name.clone(),
2608                    lock_type: format!("{:?}", acq.lock_type),
2609                    line: acq.line,
2610                })
2611                .collect()
2612        };
2613
2614        // Get suggestions if requested
2615        let suggestions = if request.suggest {
2616            analyzer
2617                .analyze()
2618                .into_iter()
2619                .take(10)
2620                .map(|s| {
2621                    use ryo_analysis::LockSuggestion;
2622                    match s {
2623                        LockSuggestion::UseAtomic {
2624                            field,
2625                            suggested_type,
2626                            line,
2627                            ..
2628                        } => LockSuggestionInfo {
2629                            kind: "UseAtomic".to_string(),
2630                            target: field,
2631                            description: suggested_type,
2632                            line,
2633                        },
2634                        LockSuggestion::SplitLock {
2635                            lock_name,
2636                            suggested_splits,
2637                            line,
2638                        } => LockSuggestionInfo {
2639                            kind: "SplitLock".to_string(),
2640                            target: lock_name,
2641                            description: format!("Split into {} parts", suggested_splits.len()),
2642                            line,
2643                        },
2644                        LockSuggestion::ReduceScope {
2645                            guard_name,
2646                            current_span,
2647                            ..
2648                        } => LockSuggestionInfo {
2649                            kind: "ReduceScope".to_string(),
2650                            target: guard_name,
2651                            description: format!("lines {}-{}", current_span.0, current_span.1),
2652                            line: current_span.0,
2653                        },
2654                        LockSuggestion::UseRwLock {
2655                            lock_name, line, ..
2656                        } => LockSuggestionInfo {
2657                            kind: "UseRwLock".to_string(),
2658                            target: lock_name,
2659                            description: "Consider RwLock".to_string(),
2660                            line,
2661                        },
2662                        LockSuggestion::LockAcrossAwait {
2663                            guard_name,
2664                            lock_line,
2665                            await_line,
2666                            ..
2667                        } => LockSuggestionInfo {
2668                            kind: "LockAcrossAwait".to_string(),
2669                            target: guard_name,
2670                            description: format!("await at line {}", await_line),
2671                            line: lock_line,
2672                        },
2673                        LockSuggestion::RemoveLock {
2674                            lock_name,
2675                            reason,
2676                            line,
2677                        } => LockSuggestionInfo {
2678                            kind: "RemoveLock".to_string(),
2679                            target: lock_name,
2680                            description: reason,
2681                            line,
2682                        },
2683                    }
2684                })
2685                .collect()
2686        } else {
2687            Vec::new()
2688        };
2689
2690        Ok(LockAnalysisResponse {
2691            display_name,
2692            stats,
2693            acquisitions,
2694            suggestions,
2695        })
2696    }
2697
2698    /// Transitive call chain traversal.
2699    ///
2700    /// Analyzes the transitive closure of call/use relationships up to a specified depth.
2701    /// Uses BFS for level-order traversal with accurate depth tracking.
2702    ///
2703    /// # Directions
2704    /// - **Callers**: Who calls this function (transitively)?
2705    /// - **Callees**: What does this function call (transitively)?
2706    /// - **Users**: What types use this type (transitively)?
2707    /// - **Uses**: What types does this type use (transitively)?
2708    pub fn graph_chain(&self, request: ChainAnalysisRequest) -> ApiResult<ChainAnalysisResponse> {
2709        use ryo_analysis::ChainDirection;
2710        use std::collections::BTreeMap;
2711
2712        // Resolve SymbolId (UUID takes precedence over id)
2713        let symbol_id = self.resolve_symbol_id_required(request.uuid.as_deref(), &request.id)?;
2714
2715        let max_depth = request.depth.unwrap_or(5);
2716
2717        // Execute chain analysis — Callers/Callees via CodeGraph, TypeUsers/TypeDeps via TypeFlow
2718        let result = match request.mode {
2719            types::ChainMode::Callers => {
2720                self.context
2721                    .code_graph
2722                    .analyze_chain(symbol_id, max_depth, ChainDirection::Callers)
2723            }
2724            types::ChainMode::Callees => {
2725                self.context
2726                    .code_graph
2727                    .analyze_chain(symbol_id, max_depth, ChainDirection::Callees)
2728            }
2729            types::ChainMode::TypeUsers => self.context.typeflow_graph.analyze_type_chain(
2730                symbol_id,
2731                max_depth,
2732                ChainDirection::TypeUsers,
2733            ),
2734            types::ChainMode::TypeDeps => self.context.typeflow_graph.analyze_type_chain(
2735                symbol_id,
2736                max_depth,
2737                ChainDirection::TypeDeps,
2738            ),
2739        };
2740
2741        // Get display name for starting symbol
2742        let display_name = self
2743            .context
2744            .registry
2745            .resolve(symbol_id)
2746            .map(|p| p.to_string())
2747            .unwrap_or_else(|| format!("{:?}", symbol_id));
2748
2749        // Group nodes by depth with full info
2750        let mut by_depth: BTreeMap<usize, Vec<types::ChainNodeInfo>> = BTreeMap::new();
2751        for node in &result.nodes {
2752            let path = self
2753                .context
2754                .registry
2755                .resolve(node.symbol)
2756                .map(|p| p.to_string())
2757                .unwrap_or_default();
2758            let kind = self
2759                .context
2760                .registry
2761                .kind(node.symbol)
2762                .map(|k| format!("{:?}", k));
2763
2764            by_depth
2765                .entry(node.depth)
2766                .or_default()
2767                .push(types::ChainNodeInfo {
2768                    id: format!("{:?}", node.symbol),
2769                    path,
2770                    kind,
2771                    depth: node.depth,
2772                });
2773        }
2774
2775        Ok(ChainAnalysisResponse {
2776            display_name,
2777            direction: result.direction.to_string(),
2778            total_count: result.nodes.len(),
2779            max_actual_depth: result.max_actual_depth,
2780            by_depth,
2781        })
2782    }
2783
2784    /// Get server/context status.
2785    pub fn status(&self) -> StatusResponse {
2786        StatusResponse {
2787            project: self.project.root().to_path_buf(),
2788            symbols: self.context.registry.len(),
2789            files: self.context.file_count(),
2790        }
2791    }
2792
2793    /// Query spec hierarchy.
2794    ///
2795    /// Uses cached SpecFlowData for efficient repeated queries.
2796    /// The cache is initialized on first call and invalidated when files change.
2797    ///
2798    /// # Example
2799    ///
2800    /// ```ignore
2801    /// let response = api.spec(SpecRequest::show())?;
2802    /// ```
2803    pub fn spec(&mut self, request: SpecRequest) -> ApiResult<SpecResponse> {
2804        use crate::spec::{LintSeverity, SpecService, SpecSourceKind};
2805
2806        // Use cached SpecFlowData (lazy initialized, invalidated on files change)
2807        if self.spec_cache.is_none() {
2808            self.spec_cache =
2809                Some(SpecService::from_context(&self.context).map_err(ApiError::Spec)?);
2810        }
2811        let data = self
2812            .spec_cache
2813            .as_ref()
2814            .expect("spec_cache was just initialized to Some above");
2815
2816        match request.query {
2817            SpecQueryKind::Show => {
2818                let show = data.to_show_response();
2819                Ok(SpecResponse::Show(SpecShowData {
2820                    groups: show
2821                        .groups
2822                        .into_iter()
2823                        .map(|g| SpecGroupData {
2824                            name: g.name,
2825                            specs: g
2826                                .specs
2827                                .into_iter()
2828                                .map(|s| SpecInfoData {
2829                                    alias_name: s.alias_name,
2830                                    wrapped_type_name: s.wrapped_type_name,
2831                                    source: match s.source {
2832                                        SpecSourceKind::TypeAlias => "type_alias".to_string(),
2833                                        SpecSourceKind::Comment => "comment".to_string(),
2834                                        SpecSourceKind::Inferred => "inferred".to_string(),
2835                                    },
2836                                })
2837                                .collect(),
2838                        })
2839                        .collect(),
2840                    relations: show
2841                        .relations
2842                        .into_iter()
2843                        .map(|r| SpecRelationData {
2844                            from: r.from,
2845                            to: r.to,
2846                            kind: format!("{:?}", r.kind).to_lowercase(),
2847                        })
2848                        .collect(),
2849                    stats: SpecStatsData {
2850                        groups: show.stats.groups,
2851                        specs: show.stats.specs,
2852                        nodes: show.stats.nodes,
2853                        edges: show.stats.edges,
2854                    },
2855                }))
2856            }
2857            SpecQueryKind::Groups => {
2858                let groups = data.group_names();
2859                Ok(SpecResponse::Groups(groups))
2860            }
2861            SpecQueryKind::TypesInGroup { group } => {
2862                let specs = data.specs_in_group(&group);
2863                Ok(SpecResponse::TypesInGroup(
2864                    specs
2865                        .into_iter()
2866                        .map(|s| SpecInfoData {
2867                            alias_name: s.alias_name,
2868                            wrapped_type_name: s.wrapped_type_name,
2869                            source: match s.source {
2870                                SpecSourceKind::TypeAlias => "type_alias".to_string(),
2871                                SpecSourceKind::Comment => "comment".to_string(),
2872                                SpecSourceKind::Inferred => "inferred".to_string(),
2873                            },
2874                        })
2875                        .collect(),
2876                ))
2877            }
2878            SpecQueryKind::Dependencies { ref type_name } => {
2879                // 未実装: SpecFlowDataが名前ベースの依存クエリを公開していない
2880                tracing::warn!(
2881                    type_name = %type_name,
2882                    "spec dependencies query is not yet implemented (requires name->SpecNodeId lookup)"
2883                );
2884                Ok(SpecResponse::Dependencies(vec![]))
2885            }
2886            SpecQueryKind::Dependents { ref type_name } => {
2887                // 未実装: SpecFlowDataが名前ベースの依存クエリを公開していない
2888                tracing::warn!(
2889                    type_name = %type_name,
2890                    "spec dependents query is not yet implemented (requires name->SpecNodeId lookup)"
2891                );
2892                Ok(SpecResponse::Dependents(vec![]))
2893            }
2894            SpecQueryKind::Stats => {
2895                let stats = data.stats();
2896                Ok(SpecResponse::Stats(SpecStatsData {
2897                    groups: stats.groups,
2898                    specs: stats.specs,
2899                    nodes: stats.nodes,
2900                    edges: stats.edges,
2901                }))
2902            }
2903            SpecQueryKind::Lint => {
2904                let lint = data.lint();
2905                Ok(SpecResponse::Lint(SpecLintData {
2906                    issues: lint
2907                        .issues
2908                        .into_iter()
2909                        .map(|i| SpecLintIssueData {
2910                            severity: match i.severity {
2911                                LintSeverity::Warning => "warning".to_string(),
2912                                LintSeverity::Error => "error".to_string(),
2913                            },
2914                            message: i.message,
2915                            location: i.location,
2916                        })
2917                        .collect(),
2918                    warnings: lint.warnings,
2919                    errors: lint.errors,
2920                }))
2921            }
2922            SpecQueryKind::Mermaid => {
2923                let mermaid = data.to_mermaid();
2924                Ok(SpecResponse::Mermaid(mermaid))
2925            }
2926        }
2927    }
2928
2929    // ========================================================================
2930    // Write Operations
2931    // ========================================================================
2932
2933    /// Execute code transformations.
2934    ///
2935    /// # Example
2936    ///
2937    /// ```ignore
2938    /// let response = api.run(RunRequest {
2939    ///     goal: Goal::new(Intent::RenameIdent { ... }),
2940    ///     dry_run: false,
2941    ///     check_syntax: true,
2942    /// })?;
2943    /// ```
2944    pub fn run(&mut self, request: RunRequest) -> ApiResult<RunResponse> {
2945        let opts = ExecuteOptions {
2946            dry_run: request.dry_run,
2947            check_syntax: request.check_syntax,
2948        };
2949
2950        match self.execute_with_options(request.goal, opts) {
2951            Ok(result) => Ok(RunResponse {
2952                success: result.success,
2953                files_modified: result.files_modified,
2954                total_changes: result.total_changes,
2955                modified_files: result.modified_files,
2956                conflicts: result
2957                    .conflicts
2958                    .iter()
2959                    .map(|c| format!("{:?}", c))
2960                    .collect(),
2961                syntax_errors: result.syntax_errors,
2962                error: None,
2963            }),
2964            Err(e) => Ok(RunResponse {
2965                success: false,
2966                error: Some(e.to_string()),
2967                ..Default::default()
2968            }),
2969        }
2970    }
2971
2972    /// Execute a goal with default options.
2973    ///
2974    /// This is a convenience wrapper around `execute_with_options` with default settings.
2975    pub fn execute(&mut self, goal: Goal) -> ApiResult<ExecutionResult> {
2976        self.execute_with_options(goal, ExecuteOptions::default())
2977    }
2978
2979    /// Expand AddVariant intents with cascade effects (AddMatchArm).
2980    ///
2981    /// When an AddVariant intent is found, this method searches for match expressions
2982    /// that use the target enum and generates AddMatchArm intents for each.
2983    fn expand_add_variant_cascades(&self, mut goal: Goal) -> Goal {
2984        use crate::discover::find_cascade_effects;
2985        use crate::intent::Intent;
2986
2987        let mut cascade_intents = Vec::new();
2988
2989        for intent in &goal.intents {
2990            if let Intent::AddVariant {
2991                target_enum,
2992                variant_name,
2993                variant_type,
2994                ..
2995            } = intent
2996            {
2997                // Find match expressions that need new arms
2998                let enum_name_str = target_enum.as_deref().unwrap_or("");
2999                let cascade_result = find_cascade_effects(
3000                    &self.context.files,
3001                    enum_name_str,
3002                    Some(variant_name),
3003                    Some(variant_type),
3004                );
3005
3006                // Convert CascadeSpecs to Intents
3007                for spec in cascade_result.specs {
3008                    cascade_intents.push(Intent::from(spec));
3009                }
3010
3011                tracing::debug!(
3012                    "AddVariant cascade: {} AddMatchArm intents generated for {}::{}",
3013                    cascade_intents.len(),
3014                    enum_name_str,
3015                    variant_name
3016                );
3017            }
3018        }
3019
3020        // Append cascade intents to the goal
3021        if !cascade_intents.is_empty() {
3022            goal.intents.extend(cascade_intents);
3023        }
3024
3025        goal
3026    }
3027
3028    /// Expand RemoveVariant intents with cascade effects (RemoveMatchArm).
3029    ///
3030    /// When a RemoveVariant intent is found, this method searches for match expressions
3031    /// that use the target enum and generates RemoveMatchArm intents for arms referencing
3032    /// the removed variant.
3033    fn expand_remove_variant_cascades(&self, mut goal: Goal) -> Goal {
3034        use crate::discover::find_remove_cascade_effects;
3035        use crate::intent::Intent;
3036
3037        let mut cascade_intents = Vec::new();
3038
3039        for intent in &goal.intents {
3040            if let Intent::RemoveVariant {
3041                target_enum,
3042                variant_name,
3043                ..
3044            } = intent
3045            {
3046                let enum_name_str = target_enum.as_deref().unwrap_or("");
3047                let cascade_result =
3048                    find_remove_cascade_effects(&self.context.files, enum_name_str, variant_name);
3049
3050                for spec in cascade_result.specs {
3051                    cascade_intents.push(Intent::from(spec));
3052                }
3053
3054                tracing::debug!(
3055                    "RemoveVariant cascade: {} RemoveMatchArm intents generated for {}::{}",
3056                    cascade_intents.len(),
3057                    enum_name_str,
3058                    variant_name
3059                );
3060            }
3061        }
3062
3063        if !cascade_intents.is_empty() {
3064            goal.intents.extend(cascade_intents);
3065        }
3066
3067        goal
3068    }
3069
3070    /// Validate that `crate::` prefixed parent paths are unambiguous in this workspace.
3071    ///
3072    /// In a multi-crate workspace, `crate::xxx` is ambiguous because it's unclear
3073    /// which crate's `xxx` is being referred to. This validation prevents such
3074    /// ambiguous paths from being processed.
3075    ///
3076    /// # Errors
3077    ///
3078    /// Returns `ApiError::Resolve(ResolveError::AmbiguousCratePath)` if an ambiguous
3079    /// `crate::` path is found in a multi-crate workspace.
3080    fn validate_workspace_parent_paths(&self, goal: &Goal) -> ApiResult<()> {
3081        let metadata = self.project.metadata();
3082        let workspace_type = metadata.workspace_type();
3083
3084        // Only validate for multi-crate workspaces
3085        if workspace_type != WorkspaceType::Workspace {
3086            return Ok(());
3087        }
3088
3089        // Get workspace members for error message
3090        let workspace_root_str = self.project.workspace_root().to_string_lossy();
3091        let workspace_members: Vec<String> = metadata
3092            .members()
3093            .filter_map(|info| {
3094                // Convert manifest path to relative crate path
3095                info.manifest_path.parent().map(|p| {
3096                    p.as_str()
3097                        .strip_prefix(workspace_root_str.as_ref())
3098                        .unwrap_or(p.as_str())
3099                        .trim_start_matches('/')
3100                        .to_string()
3101                })
3102            })
3103            .filter(|s| !s.is_empty())
3104            .collect();
3105
3106        // Check if there are multiple workspace members
3107        if workspace_members.len() <= 1 {
3108            return Ok(());
3109        }
3110
3111        // Create resolver with workspace type
3112        let resolver = self.project.path_resolver();
3113
3114        // Check each intent for ambiguous crate:: paths
3115        for intent in &goal.intents {
3116            if let Some(path) = extract_parent_path_string(intent) {
3117                resolver.validate_crate_path(&path, &workspace_members)?;
3118            }
3119        }
3120
3121        Ok(())
3122    }
3123
3124    /// Execute a goal on a project with custom options.
3125    ///
3126    /// This is the main entry point for code transformations with full control.
3127    ///
3128    /// # Flow
3129    /// 1. Validate goal confidence
3130    /// 2. Plan mutations from Intent using Planner
3131    /// 3. Build ParallelBlueprint from MutationSpecs
3132    /// 4. Handle conflicts according to ConflictStrategy
3133    /// 5. Execute via BlueprintExecutor (unless dry_run)
3134    /// 6. Verify syntax if check_syntax enabled
3135    /// 7. Record mutations to TxLog
3136    /// 8. Apply changes back to Project
3137    ///
3138    /// # Conflict Handling
3139    /// - `ConflictStrategy::Fail`: Returns error if conflicts detected
3140    /// - `ConflictStrategy::IntentOrder`: Clears conflicts, executes in intent order
3141    /// - `ConflictStrategy::ParallelOnly`: Skips conflicting mutations
3142    pub fn execute_with_options(
3143        &mut self,
3144        goal: Goal,
3145        options: ExecuteOptions,
3146    ) -> ApiResult<ExecutionResult> {
3147        // Validate goal
3148        if goal.confidence < 0.5 {
3149            return Err(ApiError::InvalidGoal(format!(
3150                "Low confidence goal: {}",
3151                goal.confidence
3152            )));
3153        }
3154
3155        tracing::info!(?goal.intents, ?options, "Executing goal");
3156
3157        // Validate workspace parent paths before planning
3158        self.validate_workspace_parent_paths(&goal)?;
3159
3160        // Expand AddVariant intents with cascade effects (AddMatchArm)
3161        let goal = self.expand_add_variant_cascades(goal);
3162
3163        // Expand RemoveVariant intents with cascade effects (RemoveMatchArm)
3164        let goal = self.expand_remove_variant_cascades(goal);
3165
3166        // Initialize transaction log
3167        let mut log = TxLog::with_project(self.project.root().to_string_lossy());
3168
3169        // 1. Plan mutations from Intent
3170        // Pass registry to resolve symbol_id to SymbolPath
3171        let specs = Planner::plan(&goal, Some(self.context.registry()))?;
3172        tracing::debug!("Planned {} mutation specs", specs.len());
3173
3174        if specs.is_empty() {
3175            return Ok(ExecutionResult {
3176                status: ExecutionStatus::no_change("No mutation specs generated from goal"),
3177                files_modified: 0,
3178                total_changes: 0,
3179                session_id: None,
3180                log,
3181                modified_files: vec![],
3182                conflicts: vec![],
3183                syntax_errors: vec![],
3184                success: true,
3185            });
3186        }
3187
3188        // 2. Build ParallelBlueprint
3189        let mut blueprint = ParallelBlueprint::from_mutations(specs);
3190        tracing::debug!(
3191            "Blueprint: {} mutations, {} conflicts",
3192            blueprint.mutations.len(),
3193            blueprint.conflicts.len()
3194        );
3195
3196        // 3. Handle conflicts according to ConflictStrategy
3197        let detected_conflicts = blueprint.conflicts.clone();
3198        if blueprint.needs_escalation() {
3199            match goal.conflict_strategy {
3200                ConflictStrategy::Fail => {
3201                    tracing::warn!(
3202                        "Conflicts detected with Fail strategy: {} conflicts",
3203                        detected_conflicts.len()
3204                    );
3205                    return Err(ApiError::Conflicts(detected_conflicts.len()));
3206                }
3207                ConflictStrategy::IntentOrder => {
3208                    // Clear conflicts to allow sequential execution in intent order
3209                    tracing::info!(
3210                        "Resolving {} conflicts by intent order",
3211                        detected_conflicts.len()
3212                    );
3213                    blueprint.conflicts.clear();
3214                }
3215                ConflictStrategy::ParallelOnly => {
3216                    // Return partial result indicating skipped mutations
3217                    tracing::warn!(
3218                        "ParallelOnly strategy: {} mutations skipped due to conflicts",
3219                        detected_conflicts.len()
3220                    );
3221                    let status = ExecutionStatus::conflict(format!(
3222                        "{} mutations skipped due to conflicts",
3223                        detected_conflicts.len()
3224                    ))
3225                    .with_explanation(
3226                        "ParallelOnly strategy was used, which skips conflicting mutations.",
3227                    )
3228                    .with_suggestion(
3229                        "Use ConflictStrategy::IntentOrder to resolve conflicts by execution order",
3230                    );
3231                    return Ok(ExecutionResult {
3232                        status,
3233                        files_modified: 0,
3234                        total_changes: 0,
3235                        session_id: None,
3236                        log,
3237                        modified_files: vec![],
3238                        conflicts: detected_conflicts,
3239                        syntax_errors: vec![],
3240                        success: false,
3241                    });
3242                }
3243            }
3244        }
3245
3246        // 4. If dry_run, return planned result without execution
3247        if options.dry_run {
3248            tracing::info!("Dry run: skipping execution");
3249            let status = ExecutionStatus::ok(format!(
3250                "{} mutations planned (dry run)",
3251                blueprint.mutations.len()
3252            ))
3253            .with_explanation("Dry run mode: changes were planned but not applied.");
3254            return Ok(ExecutionResult {
3255                status,
3256                files_modified: 0,
3257                total_changes: blueprint.mutations.len(),
3258                session_id: None,
3259                log,
3260                modified_files: vec![],
3261                conflicts: detected_conflicts,
3262                syntax_errors: vec![],
3263                success: true,
3264            });
3265        }
3266
3267        // 5. Execute via BlueprintExecutor using existing context
3268        // IMPORTANT: We reuse self.context to maintain SymbolId/FileId validity
3269        // across Discover → Run sequences. Do NOT create a new context here.
3270        // Using execute_v2 for ASTRegistry-centric execution path.
3271        let executor = BlueprintExecutor::new();
3272        let result = executor.execute_v2(&blueprint, &mut self.context);
3273
3274        // 6. Invalidate spec_cache since files may have been modified
3275        // (even on partial failure, some mutations may have succeeded)
3276        self.spec_cache = None;
3277
3278        if !result.success {
3279            return Err(ApiError::Execution(
3280                result
3281                    .error
3282                    .unwrap_or_else(|| "Unknown execution error".to_string()),
3283            ));
3284        }
3285
3286        // 6.5. Sync files and rebuild analysis graphs
3287        // execute_v2 only updates ASTRegistry - we need to sync files for disk write
3288        // and rebuild graphs for subsequent operations
3289        let modified_files = BlueprintExecutor::sync_files_and_rebuild(&result, &mut self.context)?;
3290
3291        tracing::debug!(
3292            "sync_files_and_rebuild returned {} modified files",
3293            modified_files.len()
3294        );
3295        if modified_files.is_empty() && result.total_changes > 0 {
3296            tracing::warn!(
3297                "sync_files_and_rebuild returned 0 files despite {} total changes - possible bug",
3298                result.total_changes
3299            );
3300        }
3301        for (i, file) in modified_files.iter().enumerate() {
3302            tracing::debug!("  Modified file {}: {}", i, file.as_relative().display());
3303        }
3304
3305        // 7. Verify syntax if check_syntax enabled
3306        let syntax_errors = if options.check_syntax {
3307            let mut errors = Vec::new();
3308            for file_path in &modified_files {
3309                if let Some(file) = self.context.file(file_path) {
3310                    let source = match file.to_source() {
3311                        Ok(s) => s,
3312                        Err(e) => {
3313                            errors.push(format!(
3314                                "{}: source generation failed: {}",
3315                                file_path.as_relative().display(),
3316                                e
3317                            ));
3318                            continue;
3319                        }
3320                    };
3321                    let path_str = file_path.as_relative().display().to_string();
3322                    if let Err(e) = syn::parse_file(&source) {
3323                        errors.push(format!("{}: {}", path_str, e));
3324                    }
3325                }
3326            }
3327            errors
3328        } else {
3329            vec![]
3330        };
3331
3332        // 8. Record mutations to TxLog with affected_symbols
3333        for spec_result in &result.results {
3334            if spec_result.success && spec_result.changes > 0 {
3335                // Get the original MutationSpec from blueprint
3336                let spec = &blueprint.mutations[spec_result.index];
3337                let mutation_data = serde_json::to_value(spec).ok();
3338
3339                // Get first affected file path for logging
3340                let file_path = spec_result
3341                    .affected_files
3342                    .first()
3343                    .map(|wfp| wfp.to_absolute());
3344                log.log(TxAction::MutationApplied {
3345                    mutation_type: spec_result.spec_type.clone(),
3346                    target: format!("{:?}", spec_result.affected_symbols),
3347                    changes: spec_result.changes,
3348                    mutation_data,
3349                    file_path,
3350                    pre_state: None,
3351                    post_state: None,
3352                    affected_symbols: spec_result.affected_symbols.clone(),
3353                });
3354            }
3355        }
3356
3357        // 9. Sync changes from Context to Project (for backward compatibility)
3358        // In the Context-centric design, AnalysisContext is the Single Source of Truth.
3359        // We sync to Project.files only for code that still reads from it.
3360        let success = syntax_errors.is_empty();
3361        if success {
3362            self.project
3363                .sync_from_context(&self.context, &modified_files);
3364
3365            // 10. Apply registry updates to keep SymbolRegistry in sync with AST changes
3366            // This enables correct symbol resolution after rename operations.
3367            if !result.registry_updates.is_empty() {
3368                self.context.commit_changes(&result.registry_updates);
3369            }
3370
3371            // 11. Invalidate suggest cache for affected symbols
3372            for spec_result in &result.results {
3373                for symbol_path in &spec_result.affected_symbols {
3374                    if let Some(symbol_id) = self.context.registry().lookup(symbol_path) {
3375                        self.suggest.invalidate_for_symbol(&symbol_id);
3376                    }
3377                }
3378            }
3379        }
3380
3381        // Convert WorkspaceFilePath to PathBuf for result
3382        let modified_paths: Vec<PathBuf> =
3383            modified_files.iter().map(|wfp| wfp.to_absolute()).collect();
3384
3385        tracing::info!(
3386            "Executed {} changes across {} files (success={})",
3387            result.total_changes,
3388            modified_paths.len(),
3389            success
3390        );
3391
3392        // Build status based on outcome
3393        let status = if !syntax_errors.is_empty() {
3394            ExecutionStatus::syntax_error(format!(
3395                "{} syntax errors in {} files",
3396                syntax_errors.len(),
3397                modified_paths.len()
3398            ))
3399            .with_explanation("Mutations were applied but resulted in invalid Rust syntax.")
3400            .with_suggestions(syntax_errors.iter().map(|e| format!("Fix: {}", e)))
3401        } else if result.total_changes == 0 {
3402            ExecutionStatus::no_change("No changes were made")
3403                .with_explanation("The goal was processed but no mutations were applied.")
3404        } else if !detected_conflicts.is_empty() {
3405            ExecutionStatus::resolved(format!(
3406                "{} changes ({} conflicts auto-resolved)",
3407                result.total_changes,
3408                detected_conflicts.len()
3409            ))
3410            .with_explanation("Conflicts were automatically resolved by execution order.")
3411        } else {
3412            ExecutionStatus::ok(format!(
3413                "{} changes in {} files",
3414                result.total_changes,
3415                modified_paths.len()
3416            ))
3417        };
3418
3419        Ok(ExecutionResult {
3420            status,
3421            files_modified: modified_paths.len(),
3422            total_changes: result.total_changes,
3423            session_id: None,
3424            log,
3425            modified_files: modified_paths,
3426            conflicts: detected_conflicts,
3427            syntax_errors,
3428            success,
3429        })
3430    }
3431
3432    /// Execute a goal and save the session.
3433    pub fn execute_and_save(&mut self, goal: Goal) -> ApiResult<ExecutionResult> {
3434        self.execute_and_save_with_options(goal, ExecuteOptions::default())
3435    }
3436
3437    /// Execute a goal with options and save the session.
3438    pub fn execute_and_save_with_options(
3439        &mut self,
3440        goal: Goal,
3441        options: ExecuteOptions,
3442    ) -> ApiResult<ExecutionResult> {
3443        let mut result = self.execute_with_options(goal, options)?;
3444
3445        // Save session with mutation history
3446        let session_id = self.storage.save(&result.log)?;
3447        result.session_id = Some(session_id);
3448
3449        Ok(result)
3450    }
3451
3452    /// List all sessions.
3453    pub fn list_sessions(&self) -> ApiResult<Vec<String>> {
3454        Ok(self.storage.list_sessions()?)
3455    }
3456
3457    /// Load a session's transaction log.
3458    pub fn load_session(&self, session_id: &str) -> ApiResult<ryo_storage::TxLog> {
3459        Ok(self.storage.load(session_id)?)
3460    }
3461
3462    /// Get mutable access to storage (for initialization, etc.)
3463    pub fn storage_mut(&mut self) -> &mut dyn Storage {
3464        self.storage.as_mut()
3465    }
3466
3467    /// Get reference to the analysis context.
3468    pub fn context(&self) -> &AnalysisContext {
3469        &self.context
3470    }
3471
3472    /// Get reference to the project.
3473    pub fn project(&self) -> &Project {
3474        &self.project
3475    }
3476
3477    /// Save UUID mappings to disk for persistence across sessions.
3478    ///
3479    /// **Callers must explicitly call this method** to persist UUID mappings.
3480    /// This is NOT called automatically on drop.
3481    ///
3482    /// # When to Call
3483    ///
3484    /// - **Server mode**: On shutdown RPC and idle timeout (see `RyoServer`)
3485    /// - **Standalone mode**: At the end of commands that modify symbols
3486    ///
3487    /// UUID mappings are saved to `.ryo/uuid-mapping.json` in the workspace root.
3488    pub fn save_uuid_mappings(&self) -> ApiResult<()> {
3489        let mappings = self.context.registry.export_uuid_mapping_strings();
3490        self.uuid_storage
3491            .save(&mappings)
3492            .map_err(|e| ApiError::Execution(format!("Failed to save UUID mappings: {}", e)))
3493    }
3494
3495    /// Get design choices for a suggestion.
3496    ///
3497    /// Returns the available design choices for a suggestion that has multiple
3498    /// implementation approaches (e.g., different refactoring strategies).
3499    ///
3500    /// # Example
3501    ///
3502    /// ```ignore
3503    /// let response = api.suggest_choices(SuggestChoicesRequest::new("S001g0"))?;
3504    /// if response.has_choices {
3505    ///     for choice in &response.choices {
3506    ///         println!("{}: {} ({})", choice.label, choice.title, choice.description);
3507    ///     }
3508    /// }
3509    /// ```
3510    pub fn suggest_choices(
3511        &self,
3512        request: SuggestChoicesRequest,
3513    ) -> ApiResult<SuggestChoicesResponse> {
3514        // Parse the suggestion ID
3515        let suggest_id: SuggestId = request
3516            .id
3517            .parse()
3518            .map_err(|e| ApiError::InvalidGoal(format!("Invalid ID format: {}", e)))?;
3519
3520        // Check if suggestion exists
3521        if !self.suggest.is_valid(suggest_id) {
3522            return Ok(SuggestChoicesResponse {
3523                suggestion_id: request.id,
3524                error: Some("Suggestion not found or expired".to_string()),
3525                ..Default::default()
3526            });
3527        }
3528
3529        // Get pattern name
3530        let pattern_name = self
3531            .suggest
3532            .pattern_name(suggest_id)
3533            .unwrap_or("Unknown")
3534            .to_string();
3535
3536        // Get design choices from enhanced suggestion if available
3537        let stored = self.suggest.get(suggest_id);
3538        if stored.is_none() {
3539            return Ok(SuggestChoicesResponse {
3540                suggestion_id: request.id,
3541                pattern_name,
3542                error: Some("Suggestion data not available".to_string()),
3543                ..Default::default()
3544            });
3545        }
3546
3547        // TODO: Currently design choices are not populated at scan time.
3548        // Return empty choices with has_choices = false for now.
3549        // Future: implement design choice generation in pattern detection.
3550        Ok(SuggestChoicesResponse {
3551            suggestion_id: request.id,
3552            pattern_name,
3553            choices: Vec::new(),
3554            has_choices: false,
3555            error: None,
3556        })
3557    }
3558
3559    /// Verify a suggestion before applying.
3560    ///
3561    /// Runs verification checks on a suggestion to ensure it can be safely applied.
3562    /// Supports two verification levels:
3563    /// - Light: Fast in-memory graph check
3564    /// - Full: Cargo check in temporary workspace
3565    ///
3566    /// # Example
3567    ///
3568    /// ```ignore
3569    /// let response = api.suggest_verify(SuggestVerifyRequest::new("S001g0").level(VerifyLevel::Full))?;
3570    /// if response.passed {
3571    ///     println!("Verification passed in {}ms", response.duration_ms);
3572    /// } else {
3573    ///     for diag in &response.diagnostics {
3574    ///         println!("  {}", diag);
3575    ///     }
3576    /// }
3577    /// ```
3578    pub fn suggest_verify(
3579        &self,
3580        request: SuggestVerifyRequest,
3581    ) -> ApiResult<SuggestVerifyResponse> {
3582        let start = Instant::now();
3583
3584        // Parse the suggestion ID
3585        let suggest_id: SuggestId = request
3586            .id
3587            .parse()
3588            .map_err(|e| ApiError::InvalidGoal(format!("Invalid ID format: {}", e)))?;
3589
3590        // Check if suggestion exists
3591        if !self.suggest.is_valid(suggest_id) {
3592            return Ok(SuggestVerifyResponse {
3593                suggestion_id: request.id,
3594                choice_id: request.choice_id,
3595                passed: false,
3596                level: format!("{:?}", request.level),
3597                duration_ms: start.elapsed().as_millis() as u64,
3598                diagnostics: vec!["Suggestion not found or expired".to_string()],
3599                error: Some("Suggestion not found or expired".to_string()),
3600            });
3601        }
3602
3603        // Get mutation specs for this suggestion
3604        let specs = match self.suggest.to_mutation_specs(suggest_id, &self.context) {
3605            Some(specs) if !specs.is_empty() => specs,
3606            _ => {
3607                return Ok(SuggestVerifyResponse {
3608                    suggestion_id: request.id,
3609                    choice_id: request.choice_id,
3610                    passed: false,
3611                    level: format!("{:?}", request.level),
3612                    duration_ms: start.elapsed().as_millis() as u64,
3613                    diagnostics: vec!["No mutations could be generated".to_string()],
3614                    error: Some("No mutations could be generated".to_string()),
3615                });
3616            }
3617        };
3618
3619        // Perform verification based on level
3620        let (passed, diagnostics) = match request.level {
3621            VerifyLevel::Light => {
3622                // Light verification: apply mutations speculatively and run graph check
3623                self.verify_with_graph_check(&specs)
3624            }
3625            VerifyLevel::Full => {
3626                // Full verification: apply mutations speculatively and run cargo check
3627                self.verify_with_cargo_check(&specs)
3628            }
3629        };
3630
3631        Ok(SuggestVerifyResponse {
3632            suggestion_id: request.id,
3633            choice_id: request.choice_id,
3634            passed,
3635            level: format!("{:?}", request.level),
3636            duration_ms: start.elapsed().as_millis() as u64,
3637            diagnostics,
3638            error: None,
3639        })
3640    }
3641
3642    /// Verify mutations with cargo check in a temporary workspace.
3643    ///
3644    /// This method:
3645    /// 1. Clones the analysis context
3646    /// 2. Applies mutations speculatively
3647    /// 3. Creates file changes from the diff
3648    /// 4. Runs cargo check in a temp workspace
3649    fn verify_with_cargo_check(&self, specs: &[ryo_executor::MutationSpec]) -> (bool, Vec<String>) {
3650        use ryo_symbol::WorkspacePathResolver;
3651        use std::collections::HashMap;
3652
3653        // 1. Clone context for speculative execution (O(clone) not O(rebuild))
3654        let mut forked_ctx = self.context.fork_clone();
3655        let original_files = self.context.files.clone();
3656
3657        // 2. Apply mutations speculatively
3658        let blueprint = ParallelBlueprint::from_mutations(specs.to_vec());
3659        let executor = BlueprintExecutor::default();
3660        let exec_result = executor.execute_v2(&blueprint, &mut forked_ctx);
3661
3662        if !exec_result.success {
3663            return (
3664                false,
3665                vec![format!(
3666                    "Mutation execution failed: {}",
3667                    exec_result
3668                        .error
3669                        .unwrap_or_else(|| "Unknown error".to_string())
3670                )],
3671            );
3672        }
3673
3674        // 3. No-op check via BlueprintResult (AST-level, no sync needed)
3675        if exec_result.total_changes == 0 {
3676            return (
3677                true,
3678                vec!["No file changes detected (no-op mutation)".to_string()],
3679            );
3680        }
3681
3682        // 4. Sync files from AST for subsequent verification
3683        let original_sources: HashMap<_, _> = HashMap::new();
3684        if let Err(e) = BlueprintExecutor::sync_files_and_rebuild(&exec_result, &mut forked_ctx) {
3685            return (false, vec![format!("File sync failed: {}", e)]);
3686        }
3687        let changes = match FileChange::from_execution_diff(
3688            &original_files,
3689            &forked_ctx.files,
3690            &original_sources,
3691        ) {
3692            Ok(c) => c,
3693            Err(e) => return (false, vec![format!("Source generation failed: {}", e)]),
3694        };
3695
3696        // 5. Create verification input
3697        let resolver = WorkspacePathResolver::new(self.context.workspace_root().to_path_buf());
3698        let input = VerificationInput::new(changes.clone(), resolver);
3699
3700        // 6. Run verification pipeline with cargo check
3701        let pipeline = VerificationPipeline::new().add_filesystem(CargoVerifier::new());
3702
3703        match pipeline.run_postcheck(&input) {
3704            Ok(result) => {
3705                let passed = result.pipeline_result.is_success();
3706                let mut diagnostics = vec![format!(
3707                    "{} file(s) changed, cargo check {}",
3708                    changes.len(),
3709                    if passed { "passed" } else { "failed" }
3710                )];
3711
3712                // Add any error diagnostics
3713                for diag in result.pipeline_result.all_diagnostics() {
3714                    diagnostics.push(format!("  {:?}: {}", diag.level, diag.message));
3715                }
3716
3717                (passed, diagnostics)
3718            }
3719            Err(e) => (false, vec![format!("Verification failed: {}", e)]),
3720        }
3721    }
3722
3723    /// Verify mutations with GraphVerifier (in-memory check).
3724    ///
3725    /// This method:
3726    /// 1. Clones the analysis context
3727    /// 2. Applies mutations speculatively
3728    /// 3. Creates file changes from the diff
3729    /// 4. Runs GraphVerifier for fast in-memory validation
3730    fn verify_with_graph_check(&self, specs: &[ryo_executor::MutationSpec]) -> (bool, Vec<String>) {
3731        use ryo_symbol::WorkspacePathResolver;
3732        use std::collections::HashMap;
3733
3734        // 1. Clone context for speculative execution (O(clone) not O(rebuild))
3735        let mut forked_ctx = self.context.fork_clone();
3736        let original_files = self.context.files.clone();
3737
3738        // 2. Apply mutations speculatively
3739        let blueprint = ParallelBlueprint::from_mutations(specs.to_vec());
3740        let executor = BlueprintExecutor::default();
3741        let exec_result = executor.execute_v2(&blueprint, &mut forked_ctx);
3742
3743        if !exec_result.success {
3744            return (
3745                false,
3746                vec![format!(
3747                    "Mutation execution failed: {}",
3748                    exec_result
3749                        .error
3750                        .unwrap_or_else(|| "Unknown error".to_string())
3751                )],
3752            );
3753        }
3754
3755        // 3. No-op check via BlueprintResult (AST-level, no sync needed)
3756        if exec_result.total_changes == 0 {
3757            return (
3758                true,
3759                vec!["No file changes detected (no-op mutation)".to_string()],
3760            );
3761        }
3762
3763        // 4. Sync files from AST for subsequent verification
3764        let original_sources: HashMap<_, _> = HashMap::new();
3765        if let Err(e) = BlueprintExecutor::sync_files_and_rebuild(&exec_result, &mut forked_ctx) {
3766            return (false, vec![format!("File sync failed: {}", e)]);
3767        }
3768        let changes = match FileChange::from_execution_diff(
3769            &original_files,
3770            &forked_ctx.files,
3771            &original_sources,
3772        ) {
3773            Ok(c) => c,
3774            Err(e) => return (false, vec![format!("Source generation failed: {}", e)]),
3775        };
3776
3777        // 5. Create verification input
3778        let resolver = WorkspacePathResolver::new(self.context.workspace_root().to_path_buf());
3779        let input = VerificationInput::new(changes.clone(), resolver);
3780
3781        // 6. Run GraphVerifier for in-memory validation
3782        let verifier = GraphVerifier::new();
3783        let result = verifier.verify_in_memory(&input, &forked_ctx);
3784
3785        let passed = result.is_success();
3786        let mut diagnostics = vec![format!(
3787            "{} file(s) changed, graph check {} ({:.2}ms)",
3788            changes.len(),
3789            if passed { "passed" } else { "failed" },
3790            result.duration.as_secs_f64() * 1000.0
3791        )];
3792
3793        // Add any error diagnostics
3794        for diag in &result.diagnostics {
3795            diagnostics.push(format!("  {:?}: {}", diag.level, diag.message));
3796        }
3797
3798        (passed, diagnostics)
3799    }
3800
3801    /// Compare design choices for a suggestion.
3802    ///
3803    /// Generates a comparison table showing trade-offs between different
3804    /// implementation approaches for a suggestion.
3805    ///
3806    /// # Example
3807    ///
3808    /// ```ignore
3809    /// let response = api.suggest_compare(SuggestCompareRequest::new("S001g0"))?;
3810    /// println!("{}", response.comparison_table);
3811    /// for choice in &response.ranked_choices {
3812    ///     println!("{}: {}", choice.label, choice.title);
3813    /// }
3814    /// ```
3815    pub fn suggest_compare(
3816        &self,
3817        request: SuggestCompareRequest,
3818    ) -> ApiResult<SuggestCompareResponse> {
3819        // Parse the suggestion ID
3820        let suggest_id: SuggestId = request
3821            .id
3822            .parse()
3823            .map_err(|e| ApiError::InvalidGoal(format!("Invalid ID format: {}", e)))?;
3824
3825        // Check if suggestion exists
3826        if !self.suggest.is_valid(suggest_id) {
3827            return Ok(SuggestCompareResponse {
3828                suggestion_id: request.id,
3829                error: Some("Suggestion not found or expired".to_string()),
3830                ..Default::default()
3831            });
3832        }
3833
3834        // Get pattern name
3835        let pattern_name = self
3836            .suggest
3837            .pattern_name(suggest_id)
3838            .unwrap_or("Unknown")
3839            .to_string();
3840
3841        // TODO: Currently design choices are not populated at scan time.
3842        // Return empty comparison for now.
3843        let comparison_table = format!(
3844            "Suggestion: {}\nPattern: {}\n\nNo design choices available for comparison.\n",
3845            request.id, pattern_name
3846        );
3847
3848        Ok(SuggestCompareResponse {
3849            suggestion_id: request.id,
3850            comparison_table,
3851            ranked_choices: Vec::new(),
3852            recommendation_reason: None,
3853            error: None,
3854        })
3855    }
3856
3857    /// Generate code from parameterized patterns.
3858    ///
3859    /// This method supports two modes:
3860    /// 1. **List mode** (`list: true`): Returns available parameterized patterns
3861    /// 2. **Generate mode**: Creates code from a pattern with parameters
3862    ///
3863    /// # Example
3864    ///
3865    /// ```ignore
3866    /// // List available patterns
3867    /// let list_req = SuggestGenerateRequest::list_patterns();
3868    /// let patterns = api.suggest_generate(list_req)?;
3869    ///
3870    /// // Generate OrderAPI
3871    /// let gen_req = SuggestGenerateRequest::generate("api-pattern")
3872    ///     .with_param("name", "Order");
3873    /// let result = api.suggest_generate(gen_req)?;
3874    /// ```
3875    pub fn suggest_generate(
3876        &self,
3877        request: SuggestGenerateRequest,
3878    ) -> ApiResult<SuggestGenerateResponse> {
3879        use crate::api::types::{ParamInfo, PatternInfo};
3880
3881        // Mode 1: List available patterns
3882        if request.list {
3883            // From SuggestService (parameterized Suggests)
3884            let mut patterns: Vec<PatternInfo> = self
3885                .suggest
3886                .list_parameterized()
3887                .into_iter()
3888                .map(|info| PatternInfo {
3889                    name: info.name.to_string(),
3890                    description: info.description,
3891                    category: format!("{:?}", info.category),
3892                    params: info
3893                        .param_schema
3894                        .into_iter()
3895                        .map(|p| ParamInfo {
3896                            name: p.name,
3897                            description: p.description,
3898                            required: p.required,
3899                        })
3900                        .collect(),
3901                })
3902                .collect();
3903
3904            // From GeneratorStore (YAML templates)
3905            for entry in self.generator_store.all_generators() {
3906                patterns.push(PatternInfo {
3907                    name: entry.template.name().to_string(),
3908                    description: entry.template.description().to_string(),
3909                    category: entry.template.category().unwrap_or("generator").to_string(),
3910                    params: entry
3911                        .template
3912                        .params
3913                        .iter()
3914                        .map(|p| ParamInfo {
3915                            name: p.name.clone(),
3916                            description: p.description.clone(),
3917                            required: p.required,
3918                        })
3919                        .collect(),
3920                });
3921            }
3922
3923            return Ok(SuggestGenerateResponse {
3924                patterns,
3925                ..Default::default()
3926            });
3927        }
3928
3929        // Mode 2: Generate from pattern
3930        let pattern = match &request.pattern {
3931            Some(p) => p,
3932            None => {
3933                return Ok(SuggestGenerateResponse {
3934                    error: Some(
3935                        "Pattern name required. Use --list to see available patterns.".to_string(),
3936                    ),
3937                    ..Default::default()
3938                });
3939            }
3940        };
3941
3942        // Try SuggestService first
3943        if let Some(opps) =
3944            self.suggest
3945                .generate_with_params(&self.context, pattern, &request.params)
3946        {
3947            if !opps.is_empty() {
3948                return self.generate_from_suggest_service(pattern, opps, &request);
3949            }
3950        }
3951
3952        // Try GeneratorStore (YAML templates)
3953        if let Some(entry) = self.generator_store.find_by_name(pattern) {
3954            return self.generate_from_template(entry, &request);
3955        }
3956
3957        // Also try by ID
3958        if let Some(entry) = self.generator_store.find_by_id(pattern) {
3959            return self.generate_from_template(entry, &request);
3960        }
3961
3962        Ok(SuggestGenerateResponse {
3963            error: Some(format!(
3964                "Pattern '{}' not found. Use --list to see available patterns.",
3965                pattern
3966            )),
3967            ..Default::default()
3968        })
3969    }
3970
3971    /// Generate from SuggestService parameterized suggest
3972    fn generate_from_suggest_service(
3973        &self,
3974        pattern: &str,
3975        opportunities: Vec<ryo_suggest::SuggestOpportunity>,
3976        request: &SuggestGenerateRequest,
3977    ) -> ApiResult<SuggestGenerateResponse> {
3978        // Convert to Suggestion type for response
3979        let suggestions: Vec<Suggestion> = opportunities
3980            .iter()
3981            .enumerate()
3982            .map(|(i, opp)| Suggestion {
3983                id: format!("GEN{:03}", i),
3984                rule_id: None,
3985                title: format!("Generate {}", pattern),
3986                description: opp.message.clone(),
3987                category: "Generation".to_string(),
3988                impact: "medium".to_string(),
3989                file: std::path::PathBuf::from(&opp.location.file),
3990                symbol_path: Some(opp.location.symbol_path.to_string()),
3991                fix_intent: None,
3992            })
3993            .collect();
3994
3995        // Generate preview
3996        let preview = if !request.apply {
3997            let mut preview_text = String::new();
3998            for opp in &opportunities {
3999                if let Some((_, suggest)) = self.suggest.registry().get_by_name(pattern) {
4000                    if let Ok(specs) = suggest.to_mutation_specs(&self.context, opp) {
4001                        for spec in specs {
4002                            preview_text.push_str(&format!("{:?}\n", spec));
4003                        }
4004                    }
4005                }
4006            }
4007            Some(preview_text)
4008        } else {
4009            None
4010        };
4011
4012        // Apply if requested
4013        let (applied, files_modified) = if request.apply && !request.dry_run {
4014            // TODO: Actually apply the mutations
4015            (false, 0)
4016        } else {
4017            (false, 0)
4018        };
4019
4020        Ok(SuggestGenerateResponse {
4021            suggestions,
4022            preview,
4023            applied,
4024            files_modified,
4025            ..Default::default()
4026        })
4027    }
4028
4029    /// Generate from GeneratorStore YAML template
4030    fn generate_from_template(
4031        &self,
4032        entry: &ryo_suggest::GeneratorEntry,
4033        request: &SuggestGenerateRequest,
4034    ) -> ApiResult<SuggestGenerateResponse> {
4035        let template = &entry.template;
4036
4037        // Validate and render
4038        let rendered = match template.render(&request.params) {
4039            Ok(code) => code,
4040            Err(e) => {
4041                return Ok(SuggestGenerateResponse {
4042                    error: Some(format!("Template render error: {}", e)),
4043                    ..Default::default()
4044                });
4045            }
4046        };
4047
4048        let target_file = template.render_target_file(&request.params);
4049
4050        // Create suggestion
4051        let suggestions = vec![Suggestion {
4052            id: format!("GEN-{}", template.id()),
4053            rule_id: Some(template.id().to_string()),
4054            title: format!("Generate {}", template.name()),
4055            description: template.description().to_string(),
4056            category: template.category().unwrap_or("generator").to_string(),
4057            impact: "medium".to_string(),
4058            file: std::path::PathBuf::from(&target_file),
4059            symbol_path: None, // Generator creates new code, no existing symbol
4060            fix_intent: None,
4061        }];
4062
4063        // Preview shows the rendered code
4064        let preview = if !request.apply {
4065            Some(format!(
4066                "// Target: {}\n// Position: {:?}\n\n{}",
4067                target_file, template.template.position, rendered
4068            ))
4069        } else {
4070            None
4071        };
4072
4073        // Apply if requested
4074        let (applied, files_modified) = if request.apply && !request.dry_run {
4075            // TODO: Actually write the file
4076            // For now, just return false
4077            (false, 0)
4078        } else {
4079            (false, 0)
4080        };
4081
4082        Ok(SuggestGenerateResponse {
4083            suggestions,
4084            preview,
4085            applied,
4086            files_modified,
4087            ..Default::default()
4088        })
4089    }
4090}
4091
4092/// Match a lock acquisition against a search pattern.
4093///
4094/// Supports wildcards (`*`), lock type names (`Mutex`, `RwLock`), and substring match.
4095fn matches_lock_pattern_server(acq: &ryo_analysis::LockAcquisitionV2, pattern: &str) -> bool {
4096    if pattern.is_empty() || pattern == "*" {
4097        return true;
4098    }
4099
4100    let pattern_lower = pattern.to_lowercase();
4101
4102    // Check lock type name match
4103    let type_name = format!("{:?}", acq.lock_type).to_lowercase();
4104    if type_name == pattern_lower || type_name.contains(&pattern_lower) {
4105        return true;
4106    }
4107
4108    let name_lower = acq.lock_name.to_lowercase();
4109
4110    // Glob-style matching
4111    if pattern_lower.starts_with('*') && pattern_lower.ends_with('*') && pattern_lower.len() > 1 {
4112        let middle = &pattern_lower[1..pattern_lower.len() - 1];
4113        if middle.is_empty() {
4114            return true;
4115        }
4116        name_lower.contains(middle)
4117    } else if let Some(stripped) = pattern_lower.strip_prefix('*') {
4118        name_lower.ends_with(stripped)
4119    } else if pattern_lower.ends_with('*') {
4120        name_lower.starts_with(&pattern_lower[..pattern_lower.len() - 1])
4121    } else {
4122        name_lower.contains(&pattern_lower)
4123    }
4124}
4125
4126// ============================================================================
4127// Post-Execution Hook
4128// ============================================================================
4129
4130/// Result of a post-execution hook.
4131#[derive(Debug, Clone)]
4132pub enum HookResult {
4133    /// Hook completed successfully.
4134    Success,
4135    /// Hook completed with a warning.
4136    Warning(String),
4137    /// Hook failed with an error.
4138    Error(String),
4139}
4140
4141impl HookResult {
4142    /// Check if the hook was successful.
4143    pub fn is_success(&self) -> bool {
4144        matches!(self, HookResult::Success)
4145    }
4146
4147    /// Check if the hook had an error.
4148    pub fn is_error(&self) -> bool {
4149        matches!(self, HookResult::Error(_))
4150    }
4151}
4152
4153/// Trait for post-execution hooks.
4154///
4155/// Hooks are called after mutation execution completes. They can perform
4156/// additional validation, external tool invocation, or other side effects.
4157///
4158/// # Example
4159///
4160/// ```ignore
4161/// use ryo_app::{PostExecutionHook, ExecutionResult, HookResult};
4162/// use std::path::Path;
4163///
4164/// struct CargoCheckHook;
4165///
4166/// impl PostExecutionHook for CargoCheckHook {
4167///     fn name(&self) -> &str {
4168///         "cargo-check"
4169///     }
4170///
4171///     fn run(&self, result: &ExecutionResult, project_path: &Path) -> HookResult {
4172///         // Run cargo check...
4173///         HookResult::Success
4174///     }
4175/// }
4176/// ```
4177pub trait PostExecutionHook: Send + Sync {
4178    /// Hook name for logging/display.
4179    fn name(&self) -> &str;
4180
4181    /// Run the hook after execution.
4182    ///
4183    /// # Arguments
4184    /// * `result` - The execution result from Api
4185    /// * `project_path` - Path to the project root
4186    ///
4187    /// # Returns
4188    /// * `HookResult` indicating success, warning, or error
4189    fn run(&self, result: &ExecutionResult, project_path: &Path) -> HookResult;
4190
4191    /// Whether to run this hook on dry-run executions.
4192    /// Default is false (skip on dry-run).
4193    fn run_on_dry_run(&self) -> bool {
4194        false
4195    }
4196
4197    /// Whether to run this hook when execution had syntax errors.
4198    /// Default is false (skip on syntax errors).
4199    fn run_on_syntax_error(&self) -> bool {
4200        false
4201    }
4202}
4203
4204// ============================================================================
4205// Tests
4206// ============================================================================
4207
4208#[cfg(test)]
4209mod tests {
4210    use super::*;
4211    use crate::intent::{ConflictStrategy, Goal, IdentKind, Intent, ScopeHint, StmtInsertPosition};
4212    use std::fs;
4213    use tempfile::tempdir;
4214
4215    /// Create a test project
4216    fn create_test_project() -> (tempfile::TempDir, Project) {
4217        let dir = tempdir().unwrap();
4218        let src = dir.path().join("src");
4219        fs::create_dir(&src).unwrap();
4220
4221        // Create Cargo.toml (required for CargoMetadataProvider)
4222        fs::write(
4223            dir.path().join("Cargo.toml"),
4224            r#"[package]
4225name = "test-project"
4226version = "0.1.0"
4227edition = "2021"
4228"#,
4229        )
4230        .unwrap();
4231
4232        fs::write(
4233            src.join("lib.rs"),
4234            r#"
4235pub fn old_name() -> i32 {
4236    42
4237}
4238
4239pub struct OldStruct {
4240    pub value: i32,
4241}
4242"#,
4243        )
4244        .unwrap();
4245
4246        let project = Project::load(dir.path()).unwrap();
4247        (dir, project)
4248    }
4249
4250    // ------------------------------------------------------------------------
4251    // Api Construction Tests
4252    // ------------------------------------------------------------------------
4253
4254    #[test]
4255    fn test_api_new() {
4256        let (dir, _project) = create_test_project();
4257        let _api = Api::from_path(dir.path()).unwrap();
4258        // Just verify no panic
4259    }
4260
4261    #[test]
4262    fn test_api_from_path() {
4263        let (dir, _project) = create_test_project();
4264        let api = Api::from_path(dir.path()).unwrap();
4265        assert!(!api.context.registry().is_empty());
4266    }
4267
4268    #[test]
4269    fn test_storage_mut() {
4270        let (dir, _project) = create_test_project();
4271        let mut api = Api::from_path(dir.path()).unwrap();
4272        let _storage = api.storage_mut();
4273        // Just verify access works
4274    }
4275
4276    // ------------------------------------------------------------------------
4277    // ExecuteOptions Tests
4278    // ------------------------------------------------------------------------
4279
4280    #[test]
4281    fn test_execute_options_default() {
4282        let opts = ExecuteOptions::default();
4283        assert!(!opts.dry_run);
4284        assert!(!opts.check_syntax);
4285    }
4286
4287    #[test]
4288    fn test_execute_options_new() {
4289        let opts = ExecuteOptions::new();
4290        assert!(!opts.dry_run);
4291        assert!(!opts.check_syntax);
4292    }
4293
4294    #[test]
4295    fn test_execute_options_dry_run() {
4296        let opts = ExecuteOptions::new().dry_run();
4297        assert!(opts.dry_run);
4298        assert!(!opts.check_syntax);
4299    }
4300
4301    #[test]
4302    fn test_execute_options_check_syntax() {
4303        let opts = ExecuteOptions::new().check_syntax();
4304        assert!(!opts.dry_run);
4305        assert!(opts.check_syntax);
4306    }
4307
4308    #[test]
4309    fn test_execute_options_chained() {
4310        let opts = ExecuteOptions::new().dry_run().check_syntax();
4311        assert!(opts.dry_run);
4312        assert!(opts.check_syntax);
4313    }
4314
4315    // ------------------------------------------------------------------------
4316    // Discover Tests
4317    // ------------------------------------------------------------------------
4318
4319    #[test]
4320    fn test_discover_all() {
4321        let (dir, _project) = create_test_project();
4322        let mut api = Api::from_path(dir.path()).unwrap();
4323
4324        let response = api
4325            .discover(DiscoverRequest {
4326                pattern: "*".to_string(),
4327                ..Default::default()
4328            })
4329            .unwrap();
4330
4331        assert!(response.total > 0);
4332    }
4333
4334    #[test]
4335    fn test_discover_with_pattern() {
4336        let (dir, _project) = create_test_project();
4337        let mut api = Api::from_path(dir.path()).unwrap();
4338
4339        let response = api
4340            .discover(DiscoverRequest {
4341                pattern: "Old*".to_string(),
4342                ..Default::default()
4343            })
4344            .unwrap();
4345
4346        // Should find OldStruct and old_name
4347        assert!(response.symbols.iter().any(|s| s.name.starts_with("Old")));
4348    }
4349
4350    // ------------------------------------------------------------------------
4351    // Status Tests
4352    // ------------------------------------------------------------------------
4353
4354    #[test]
4355    fn test_status() {
4356        let (dir, _project) = create_test_project();
4357        let api = Api::from_path(dir.path()).unwrap();
4358
4359        let status = api.status();
4360        assert!(status.files > 0);
4361        assert!(status.symbols > 0);
4362    }
4363
4364    // ------------------------------------------------------------------------
4365    // Goal Validation Tests
4366    // ------------------------------------------------------------------------
4367
4368    #[test]
4369    fn test_execute_low_confidence_goal() {
4370        let (dir, _project) = create_test_project();
4371        let mut api = Api::from_path(dir.path()).unwrap();
4372
4373        let goal = Goal::new(
4374            "rename old_name to new_name",
4375            Intent::RenameIdent {
4376                symbol_id: None,
4377                symbol_path: None,
4378                target_ident: Some("old_name".to_string()),
4379                to: "new_name".to_string(),
4380                kind: IdentKind::Any,
4381            },
4382        )
4383        .with_confidence(0.3); // Low confidence
4384
4385        let result = api.execute(goal);
4386        assert!(result.is_err());
4387        assert!(matches!(result.unwrap_err(), ApiError::InvalidGoal(_)));
4388    }
4389
4390    // ------------------------------------------------------------------------
4391    // Empty Goal Tests
4392    // ------------------------------------------------------------------------
4393
4394    #[test]
4395    fn test_execute_empty_intents() {
4396        let (dir, _project) = create_test_project();
4397        let mut api = Api::from_path(dir.path()).unwrap();
4398
4399        // Goal with no intents
4400        let goal = Goal {
4401            query: String::new(),
4402            intents: vec![],
4403            scope: ScopeHint::default(),
4404            constraints: vec![],
4405            confidence: 1.0,
4406            conflict_strategy: ConflictStrategy::Fail,
4407        };
4408
4409        let result = api.execute(goal).unwrap();
4410        assert!(result.success);
4411        assert_eq!(result.files_modified, 0);
4412        assert_eq!(result.total_changes, 0);
4413    }
4414
4415    // ------------------------------------------------------------------------
4416    // Dry Run Tests
4417    // ------------------------------------------------------------------------
4418
4419    #[test]
4420    fn test_execute_dry_run() {
4421        let (dir, _project) = create_test_project();
4422        let mut api = Api::from_path(dir.path()).unwrap();
4423
4424        let goal = Goal::new(
4425            "rename old_name to new_name",
4426            Intent::RenameIdent {
4427                symbol_id: None,
4428                symbol_path: None,
4429                target_ident: Some("old_name".to_string()),
4430                to: "new_name".to_string(),
4431                kind: IdentKind::Any,
4432            },
4433        );
4434
4435        let opts = ExecuteOptions::new().dry_run();
4436        let result = api.execute_with_options(goal, opts).unwrap();
4437
4438        assert!(result.success);
4439        // Dry run doesn't modify files
4440        assert_eq!(result.files_modified, 0);
4441    }
4442
4443    // ------------------------------------------------------------------------
4444    // Run API Tests
4445    // ------------------------------------------------------------------------
4446
4447    #[test]
4448    fn test_run_api() {
4449        let (dir, _project) = create_test_project();
4450        let mut api = Api::from_path(dir.path()).unwrap();
4451
4452        let goal = Goal::new(
4453            "rename old_name to new_name",
4454            Intent::RenameIdent {
4455                symbol_id: None,
4456                symbol_path: None,
4457                target_ident: Some("old_name".to_string()),
4458                to: "new_name".to_string(),
4459                kind: IdentKind::Any,
4460            },
4461        );
4462
4463        let response = api
4464            .run(RunRequest {
4465                goal,
4466                dry_run: true,
4467                check_syntax: false,
4468            })
4469            .unwrap();
4470
4471        assert!(response.success);
4472    }
4473
4474    // ------------------------------------------------------------------------
4475    // Session Tests
4476    // ------------------------------------------------------------------------
4477
4478    #[test]
4479    fn test_list_sessions_empty() {
4480        let (dir, _project) = create_test_project();
4481        let api = Api::from_path(dir.path()).unwrap();
4482
4483        let sessions = api.list_sessions().unwrap();
4484        assert!(sessions.is_empty());
4485    }
4486
4487    #[test]
4488    fn test_execute_and_save() {
4489        let (dir, _project) = create_test_project();
4490        let mut api = Api::from_path(dir.path()).unwrap();
4491
4492        // Empty goal for simple test
4493        let goal = Goal {
4494            query: String::new(),
4495            intents: vec![],
4496            scope: ScopeHint::default(),
4497            constraints: vec![],
4498            confidence: 1.0,
4499            conflict_strategy: ConflictStrategy::Fail,
4500        };
4501
4502        let result = api.execute_and_save(goal).unwrap();
4503        assert!(result.session_id.is_some());
4504
4505        let sessions = api.list_sessions().unwrap();
4506        assert_eq!(sessions.len(), 1);
4507    }
4508
4509    #[test]
4510    fn test_load_session() {
4511        let (dir, _project) = create_test_project();
4512        let mut api = Api::from_path(dir.path()).unwrap();
4513
4514        let goal = Goal {
4515            query: String::new(),
4516            intents: vec![],
4517            scope: ScopeHint::default(),
4518            constraints: vec![],
4519            confidence: 1.0,
4520            conflict_strategy: ConflictStrategy::Fail,
4521        };
4522
4523        let result = api.execute_and_save(goal).unwrap();
4524        let session_id = result.session_id.unwrap();
4525
4526        let log = api.load_session(&session_id).unwrap();
4527        let _ = log.entries().len(); // verify log is accessible
4528    }
4529
4530    // ------------------------------------------------------------------------
4531    // HookResult Tests
4532    // ------------------------------------------------------------------------
4533
4534    #[test]
4535    fn test_hook_result_success() {
4536        let result = HookResult::Success;
4537        assert!(result.is_success());
4538        assert!(!result.is_error());
4539    }
4540
4541    #[test]
4542    fn test_hook_result_warning() {
4543        let result = HookResult::Warning("test warning".to_string());
4544        assert!(!result.is_success());
4545        assert!(!result.is_error());
4546    }
4547
4548    #[test]
4549    fn test_hook_result_error() {
4550        let result = HookResult::Error("test error".to_string());
4551        assert!(!result.is_success());
4552        assert!(result.is_error());
4553    }
4554
4555    // ------------------------------------------------------------------------
4556    // ApiError Tests
4557    // ------------------------------------------------------------------------
4558
4559    #[test]
4560    fn test_api_error_display() {
4561        let err = ApiError::InvalidGoal("test".to_string());
4562        assert!(err.to_string().contains("Invalid goal"));
4563
4564        let err = ApiError::Conflicts(3);
4565        assert!(err.to_string().contains("3 conflicts"));
4566
4567        let err = ApiError::SyntaxError("parse failed".to_string());
4568        assert!(err.to_string().contains("Syntax error"));
4569    }
4570
4571    // ------------------------------------------------------------------------
4572    // ExecutionResult Tests
4573    // ------------------------------------------------------------------------
4574
4575    #[test]
4576    fn test_execution_result_fields() {
4577        let result = ExecutionResult {
4578            status: ExecutionStatus::ok("5 changes in 2 files"),
4579            files_modified: 2,
4580            total_changes: 5,
4581            session_id: Some("test-session".to_string()),
4582            log: TxLog::new(),
4583            modified_files: vec![PathBuf::from("file1.rs"), PathBuf::from("file2.rs")],
4584            conflicts: vec![],
4585            syntax_errors: vec![],
4586            success: true,
4587        };
4588
4589        assert_eq!(result.files_modified, 2);
4590        assert_eq!(result.total_changes, 5);
4591        assert!(result.success);
4592        assert!(result.status.is_success());
4593        assert_eq!(result.modified_files.len(), 2);
4594    }
4595
4596    #[test]
4597    fn test_execution_status_codes() {
4598        // Success statuses
4599        assert!(StatusCode::Ok.is_success());
4600        assert!(StatusCode::NoChange.is_success());
4601        assert!(StatusCode::Resolved.is_success());
4602
4603        // Error statuses
4604        assert!(StatusCode::Invalid.is_error());
4605        assert!(StatusCode::NotFound.is_error());
4606        assert!(StatusCode::Conflict.is_error());
4607        assert!(StatusCode::SyntaxError.is_error());
4608
4609        // HTTP codes
4610        assert_eq!(StatusCode::Ok.as_http_code(), 200);
4611        assert_eq!(StatusCode::NoChange.as_http_code(), 204);
4612        assert_eq!(StatusCode::Conflict.as_http_code(), 409);
4613    }
4614
4615    #[test]
4616    fn test_execution_status_builder() {
4617        let status = ExecutionStatus::ok("3 changes")
4618            .with_explanation("Renamed foo to bar")
4619            .with_suggestion("Run tests to verify");
4620
4621        assert!(status.is_success());
4622        assert_eq!(status.detail.reason, "3 changes");
4623        assert!(status.detail.explanation.is_some());
4624        assert_eq!(status.detail.suggestions.len(), 1);
4625    }
4626
4627    // ------------------------------------------------------------------------
4628    // Context Lifecycle Integration Tests
4629    // ------------------------------------------------------------------------
4630    //
4631    // These tests verify that AnalysisContext maintains SymbolId/FileId validity
4632    // across Discover → Run sequences. This is critical for Server mode where
4633    // a client may obtain SymbolIds from Discover and use them in subsequent Run calls.
4634
4635    /// Helper to create a test project with multiple functions for renaming tests
4636    fn create_multi_function_project() -> (tempfile::TempDir, Project) {
4637        let dir = tempdir().unwrap();
4638        let src = dir.path().join("src");
4639        fs::create_dir(&src).unwrap();
4640
4641        fs::write(
4642            src.join("lib.rs"),
4643            r#"
4644pub fn alpha() -> i32 {
4645    1
4646}
4647
4648pub fn beta() -> i32 {
4649    alpha() + 1
4650}
4651
4652pub fn gamma() -> i32 {
4653    beta() + alpha()
4654}
4655"#,
4656        )
4657        .unwrap();
4658
4659        // Add Cargo.toml for proper project detection
4660        fs::write(
4661            dir.path().join("Cargo.toml"),
4662            r#"[package]
4663name = "test-project"
4664version = "0.1.0"
4665edition = "2021"
4666"#,
4667        )
4668        .unwrap();
4669
4670        let project = Project::load(dir.path()).unwrap();
4671        (dir, project)
4672    }
4673
4674    #[test]
4675    fn test_discover_then_run_preserves_symbol_id() {
4676        // Scenario: Discover a symbol, then Run a rename on it using the same Api instance.
4677        // The SymbolId obtained from Discover should remain valid for the Run operation.
4678        let (dir, _project) = create_multi_function_project();
4679        let mut api = Api::from_path(dir.path()).unwrap();
4680
4681        // Step 1: Discover the 'alpha' function
4682        let discover_response = api
4683            .discover(DiscoverRequest {
4684                pattern: "alpha".to_string(),
4685                ..Default::default()
4686            })
4687            .unwrap();
4688
4689        assert!(discover_response.total > 0, "Should find 'alpha' function");
4690
4691        // Verify we found the symbol
4692        let alpha_symbol = discover_response
4693            .symbols
4694            .iter()
4695            .find(|s| s.name == "alpha")
4696            .expect("Should find 'alpha' in discover response");
4697        assert_eq!(alpha_symbol.name, "alpha");
4698
4699        // Step 2: Run a rename using the Api (simulating Server mode workflow)
4700        let goal = Goal::new(
4701            "rename alpha to alpha_renamed",
4702            Intent::RenameIdent {
4703                symbol_id: None,
4704                symbol_path: None,
4705                target_ident: Some("alpha".to_string()),
4706                to: "alpha_renamed".to_string(),
4707                kind: IdentKind::Fn,
4708            },
4709        );
4710
4711        let result = api.execute(goal).unwrap();
4712        assert!(result.success, "Rename should succeed");
4713        assert!(result.total_changes > 0, "Should have made changes");
4714
4715        // Step 3: Verify the Context still has valid file registry
4716        // The original FileId should still be resolvable
4717        let files: Vec<_> = api.context().files().keys().cloned().collect();
4718        assert!(!files.is_empty(), "Should still have files in context");
4719
4720        // Verify each file path can be resolved to a file
4721        for file_path in &files {
4722            assert!(
4723                api.context().file(file_path).is_some(),
4724                "File {:?} should be accessible after run",
4725                file_path
4726            );
4727        }
4728
4729        // Step 4: Verify the renamed symbol exists by discovering it
4730        let discover_after = api
4731            .discover(DiscoverRequest {
4732                pattern: "alpha_renamed".to_string(),
4733                ..Default::default()
4734            })
4735            .unwrap();
4736
4737        // Note: Due to SymbolRegistry not being updated (Phase 6 not implemented),
4738        // the old path "alpha" may still be in registry. This test verifies the
4739        // Context lifecycle is maintained, not that SymbolRegistry is updated.
4740        // Phase 6 will address SymbolRegistry updates.
4741        let _ = discover_after; // Acknowledge we've checked
4742
4743        // The key assertion: the Api's context survived the Run operation
4744        // and FileIds remain valid
4745        assert!(
4746            !api.context().registry().is_empty(),
4747            "FileRegistry should have entries after run"
4748        );
4749    }
4750
4751    #[test]
4752    fn test_run_then_discover_context_survives() {
4753        // Verify that after running a mutation, subsequent Discover calls work correctly
4754        let (dir, _project) = create_multi_function_project();
4755        let mut api = Api::from_path(dir.path()).unwrap();
4756
4757        // Step 1: Run a rename
4758        let goal = Goal::new(
4759            "rename beta to beta_new",
4760            Intent::RenameIdent {
4761                symbol_id: None,
4762                symbol_path: None,
4763                target_ident: Some("beta".to_string()),
4764                to: "beta_new".to_string(),
4765                kind: IdentKind::Fn,
4766            },
4767        );
4768
4769        let result = api.execute(goal).unwrap();
4770        assert!(result.success, "First rename should succeed");
4771
4772        // Step 2: Discover should still work and use the same Context
4773        let discover_response = api
4774            .discover(DiscoverRequest {
4775                pattern: "gamma".to_string(),
4776                ..Default::default()
4777            })
4778            .unwrap();
4779
4780        // gamma should still be discoverable (it wasn't renamed)
4781        assert!(
4782            discover_response.symbols.iter().any(|s| s.name == "gamma"),
4783            "gamma should still be discoverable after run"
4784        );
4785
4786        // Step 3: Run another mutation to verify Context is still usable
4787        let goal2 = Goal::new(
4788            "rename gamma to gamma_new",
4789            Intent::RenameIdent {
4790                symbol_id: None,
4791                symbol_path: None,
4792                target_ident: Some("gamma".to_string()),
4793                to: "gamma_new".to_string(),
4794                kind: IdentKind::Fn,
4795            },
4796        );
4797
4798        let result2 = api.execute(goal2).unwrap();
4799        assert!(result2.success, "Second rename should succeed");
4800    }
4801
4802    #[test]
4803    fn test_multiple_sequential_runs_preserve_context() {
4804        // Verify that multiple Run operations don't corrupt the Context
4805        let (dir, _project) = create_multi_function_project();
4806        let mut api = Api::from_path(dir.path()).unwrap();
4807
4808        // Get initial file count
4809        let initial_file_count = api.context().files().len();
4810
4811        // Run 3 sequential renames
4812        let renames = [
4813            ("alpha", "alpha_v2"),
4814            ("beta", "beta_v2"),
4815            ("gamma", "gamma_v2"),
4816        ];
4817
4818        for (from, to) in renames {
4819            let goal = Goal::new(
4820                format!("rename {} to {}", from, to),
4821                Intent::RenameIdent {
4822                    symbol_id: None,
4823                    symbol_path: None,
4824                    target_ident: Some(from.to_string()),
4825                    to: to.to_string(),
4826                    kind: IdentKind::Fn,
4827                },
4828            );
4829
4830            let result = api.execute(goal).unwrap();
4831            assert!(result.success, "Rename {} -> {} should succeed", from, to);
4832        }
4833
4834        // Verify Context integrity after multiple runs
4835        assert_eq!(
4836            api.context().files().len(),
4837            initial_file_count,
4838            "File count should remain stable after multiple runs"
4839        );
4840
4841        // Verify all files are still accessible
4842        for (file_path, _) in api.context().files().iter() {
4843            assert!(
4844                api.context().file(file_path).is_some(),
4845                "File {:?} should be accessible after multiple runs",
4846                file_path
4847            );
4848        }
4849    }
4850
4851    #[test]
4852    fn test_spec_cache_invalidated_after_run() {
4853        // Verify that spec_cache is properly invalidated after a Run operation
4854        let (dir, _project) = create_multi_function_project();
4855        let mut api = Api::from_path(dir.path()).unwrap();
4856
4857        // Step 1: Call spec() to populate the cache
4858        let spec_request = SpecRequest::show();
4859        let spec_result1 = api.spec(spec_request.clone());
4860        assert!(spec_result1.is_ok(), "First spec() call should succeed");
4861
4862        // Step 2: Run a mutation that modifies files
4863        let goal = Goal::new(
4864            "rename alpha to alpha_changed",
4865            Intent::RenameIdent {
4866                symbol_id: None,
4867                symbol_path: None,
4868                target_ident: Some("alpha".to_string()),
4869                to: "alpha_changed".to_string(),
4870                kind: IdentKind::Fn,
4871            },
4872        );
4873
4874        let result = api.execute(goal).unwrap();
4875        assert!(result.success, "Rename should succeed");
4876
4877        // Step 3: Call spec() again - it should work (cache was invalidated and rebuilt)
4878        let spec_result2 = api.spec(spec_request);
4879        assert!(
4880            spec_result2.is_ok(),
4881            "spec() after run should succeed (cache invalidated and rebuilt)"
4882        );
4883    }
4884
4885    #[test]
4886    fn test_file_content_updated_in_context_after_run() {
4887        // Verify that file content in Context is actually updated after Run
4888        let (dir, _project) = create_multi_function_project();
4889        let mut api = Api::from_path(dir.path()).unwrap();
4890
4891        // Get initial file content
4892        let file_id = api.context().files().keys().next().unwrap().clone();
4893        let initial_content = api.context().file(&file_id).unwrap().to_source().unwrap();
4894        assert!(
4895            initial_content.contains("fn alpha()"),
4896            "Initial content should contain 'fn alpha()'"
4897        );
4898
4899        // Run a rename
4900        let goal = Goal::new(
4901            "rename alpha to alpha_modified",
4902            Intent::RenameIdent {
4903                symbol_id: None,
4904                symbol_path: None,
4905                target_ident: Some("alpha".to_string()),
4906                to: "alpha_modified".to_string(),
4907                kind: IdentKind::Fn,
4908            },
4909        );
4910
4911        let result = api.execute(goal).unwrap();
4912        assert!(result.success);
4913
4914        // Verify the file content in Context was updated
4915        let updated_content = api.context().file(&file_id).unwrap().to_source().unwrap();
4916        assert!(
4917            updated_content.contains("fn alpha_modified()"),
4918            "Updated content should contain 'fn alpha_modified()'"
4919        );
4920        assert!(
4921            !updated_content.contains("fn alpha()"),
4922            "Updated content should NOT contain original 'fn alpha()'"
4923        );
4924    }
4925
4926    // ------------------------------------------------------------------------
4927    // Phase 6: SymbolRegistry Update Tests
4928    // ------------------------------------------------------------------------
4929
4930    #[test]
4931    fn test_discover_finds_renamed_symbol() {
4932        // Phase 6 test: After renaming a symbol, Discover should find it by the new name
4933        // and NOT find it by the old name.
4934        let (dir, _project) = create_multi_function_project();
4935        let mut api = Api::from_path(dir.path()).unwrap();
4936
4937        // Step 1: Verify 'alpha' exists before rename
4938        let before = api
4939            .discover(DiscoverRequest {
4940                pattern: "alpha".to_string(),
4941                ..Default::default()
4942            })
4943            .unwrap();
4944        assert!(
4945            before.symbols.iter().any(|s| s.name == "alpha"),
4946            "Should find 'alpha' before rename"
4947        );
4948
4949        // Step 2: Rename alpha → omega
4950        let goal = Goal::new(
4951            "rename alpha to omega",
4952            Intent::RenameIdent {
4953                symbol_id: None,
4954                symbol_path: None,
4955                target_ident: Some("alpha".to_string()),
4956                to: "omega".to_string(),
4957                kind: IdentKind::Fn,
4958            },
4959        );
4960
4961        let result = api.execute(goal).unwrap();
4962        assert!(result.success, "Rename should succeed");
4963
4964        // Step 3: Discover should now find 'omega'
4965        let after_new = api
4966            .discover(DiscoverRequest {
4967                pattern: "omega".to_string(),
4968                ..Default::default()
4969            })
4970            .unwrap();
4971        assert!(
4972            after_new.symbols.iter().any(|s| s.name == "omega"),
4973            "Should find 'omega' after rename"
4974        );
4975
4976        // Step 4: Discover should NOT find 'alpha' anymore (in registry)
4977        let after_old = api
4978            .discover(DiscoverRequest {
4979                pattern: "alpha".to_string(),
4980                ..Default::default()
4981            })
4982            .unwrap();
4983        assert!(
4984            !after_old.symbols.iter().any(|s| s.name == "alpha"),
4985            "Should NOT find 'alpha' in registry after rename"
4986        );
4987    }
4988
4989    #[test]
4990    fn test_registry_updated_after_sequential_renames() {
4991        // Phase 6 test: Sequential renames should keep registry in sync
4992        // alpha → beta_new, then beta → gamma_new
4993        let (dir, _project) = create_multi_function_project();
4994        let mut api = Api::from_path(dir.path()).unwrap();
4995
4996        // First rename: alpha → alpha_new
4997        let goal1 = Goal::new(
4998            "rename alpha to alpha_new",
4999            Intent::RenameIdent {
5000                symbol_id: None,
5001                symbol_path: None,
5002                target_ident: Some("alpha".to_string()),
5003                to: "alpha_new".to_string(),
5004                kind: IdentKind::Fn,
5005            },
5006        );
5007
5008        let result1 = api.execute(goal1).unwrap();
5009        assert!(result1.success, "First rename should succeed");
5010
5011        // Second rename: beta → beta_new
5012        let goal2 = Goal::new(
5013            "rename beta to beta_new",
5014            Intent::RenameIdent {
5015                symbol_id: None,
5016                symbol_path: None,
5017                target_ident: Some("beta".to_string()),
5018                to: "beta_new".to_string(),
5019                kind: IdentKind::Fn,
5020            },
5021        );
5022
5023        let result2 = api.execute(goal2).unwrap();
5024        assert!(result2.success, "Second rename should succeed");
5025
5026        // Verify both new names are findable
5027        let discover_alpha = api
5028            .discover(DiscoverRequest {
5029                pattern: "alpha_new".to_string(),
5030                ..Default::default()
5031            })
5032            .unwrap();
5033        assert!(
5034            discover_alpha.symbols.iter().any(|s| s.name == "alpha_new"),
5035            "Should find 'alpha_new' after rename"
5036        );
5037
5038        let discover_beta = api
5039            .discover(DiscoverRequest {
5040                pattern: "beta_new".to_string(),
5041                ..Default::default()
5042            })
5043            .unwrap();
5044        assert!(
5045            discover_beta.symbols.iter().any(|s| s.name == "beta_new"),
5046            "Should find 'beta_new' after rename"
5047        );
5048
5049        // Old names should not be found
5050        let discover_old_alpha = api
5051            .discover(DiscoverRequest {
5052                pattern: "alpha".to_string(),
5053                ..Default::default()
5054            })
5055            .unwrap();
5056        assert!(
5057            !discover_old_alpha.symbols.iter().any(|s| s.name == "alpha"),
5058            "Should NOT find 'alpha' after rename"
5059        );
5060    }
5061
5062    // ------------------------------------------------------------------------
5063    // Phase 7: SymbolId Resolution Tests
5064    // ------------------------------------------------------------------------
5065
5066    #[test]
5067    fn test_symbol_id_resolves_to_name() {
5068        // Phase 7 test: symbol_id field should resolve to the symbol's name
5069        // via SymbolRegistry when planning mutations.
5070        let (dir, _project) = create_multi_function_project();
5071        let mut api = Api::from_path(dir.path()).unwrap();
5072
5073        // Step 1: Discover 'alpha' to get its SymbolId
5074        let discover_result = api
5075            .discover(DiscoverRequest {
5076                pattern: "alpha".to_string(),
5077                ..Default::default()
5078            })
5079            .unwrap();
5080
5081        let alpha_symbol = discover_result
5082            .symbols
5083            .iter()
5084            .find(|s| s.name == "alpha")
5085            .expect("Should find 'alpha' symbol");
5086
5087        // Step 2: Create a rename intent using symbol_id field with the SymbolId
5088        let goal = Goal::new(
5089            "rename alpha to zeta using symbol_id",
5090            Intent::RenameIdent {
5091                symbol_id: Some(alpha_symbol.id.clone()),
5092                symbol_path: None,
5093                target_ident: None,
5094                to: "zeta".to_string(),
5095                kind: IdentKind::Fn,
5096            },
5097        );
5098
5099        // Step 3: Execute the rename - this should work because symbol_id
5100        // is resolved to "alpha" via the SymbolRegistry
5101        let result = api.execute(goal).unwrap();
5102        assert!(result.success, "Rename using symbol_id should succeed");
5103        assert!(result.total_changes > 0, "Should have made changes");
5104
5105        // Step 4: Verify 'zeta' exists and 'alpha' doesn't
5106        let discover_zeta = api
5107            .discover(DiscoverRequest {
5108                pattern: "zeta".to_string(),
5109                ..Default::default()
5110            })
5111            .unwrap();
5112        assert!(
5113            discover_zeta.symbols.iter().any(|s| s.name == "zeta"),
5114            "Should find 'zeta' after rename via symbol_id"
5115        );
5116
5117        let discover_alpha = api
5118            .discover(DiscoverRequest {
5119                pattern: "alpha".to_string(),
5120                ..Default::default()
5121            })
5122            .unwrap();
5123        assert!(
5124            !discover_alpha.symbols.iter().any(|s| s.name == "alpha"),
5125            "Should NOT find 'alpha' after rename via symbol_id"
5126        );
5127    }
5128
5129    #[test]
5130    fn test_symbol_id_sequential_operations() {
5131        // Phase 7 test: symbol_id should work correctly in a sequence of operations
5132        // where the symbol is discovered, renamed, discovered again, and renamed again.
5133        let (dir, _project) = create_multi_function_project();
5134        let mut api = Api::from_path(dir.path()).unwrap();
5135
5136        // Step 1: Discover 'alpha' to get its SymbolId
5137        let discover1 = api
5138            .discover(DiscoverRequest {
5139                pattern: "alpha".to_string(),
5140                ..Default::default()
5141            })
5142            .unwrap();
5143        let alpha_id_str = discover1
5144            .symbols
5145            .iter()
5146            .find(|s| s.name == "alpha")
5147            .unwrap()
5148            .id
5149            .clone();
5150
5151        // Step 2: Rename alpha → phi using symbol_id
5152        let goal1 = Goal::new(
5153            "rename alpha to phi",
5154            Intent::RenameIdent {
5155                symbol_id: Some(alpha_id_str.clone()),
5156                symbol_path: None,
5157                target_ident: None,
5158                to: "phi".to_string(),
5159                kind: IdentKind::Fn,
5160            },
5161        );
5162        let result1 = api.execute(goal1).unwrap();
5163        assert!(result1.success, "First rename should succeed");
5164
5165        // Step 3: Discover 'phi' to get its (updated) SymbolId
5166        let discover2 = api
5167            .discover(DiscoverRequest {
5168                pattern: "phi".to_string(),
5169                ..Default::default()
5170            })
5171            .unwrap();
5172        let phi_id_str = discover2
5173            .symbols
5174            .iter()
5175            .find(|s| s.name == "phi")
5176            .unwrap()
5177            .id
5178            .clone();
5179
5180        // The symbol ID should be the same (we renamed, not created new)
5181        assert_eq!(
5182            alpha_id_str, phi_id_str,
5183            "SymbolId should be preserved across renames"
5184        );
5185
5186        // Step 4: Rename phi → psi using symbol_id with the same ID
5187        let goal2 = Goal::new(
5188            "rename phi to psi",
5189            Intent::RenameIdent {
5190                symbol_id: Some(phi_id_str),
5191                symbol_path: None,
5192                target_ident: None,
5193                to: "psi".to_string(),
5194                kind: IdentKind::Fn,
5195            },
5196        );
5197        let result2 = api.execute(goal2).unwrap();
5198        assert!(result2.success, "Second rename should succeed");
5199
5200        // Step 5: Verify 'psi' exists
5201        let discover3 = api
5202            .discover(DiscoverRequest {
5203                pattern: "psi".to_string(),
5204                ..Default::default()
5205            })
5206            .unwrap();
5207        assert!(
5208            discover3.symbols.iter().any(|s| s.name == "psi"),
5209            "Should find 'psi' after sequential renames using symbol_id"
5210        );
5211    }
5212
5213    // ------------------------------------------------------------------------
5214    // AddVariant → Cascade → AddMatchArm E2E Tests
5215    // ------------------------------------------------------------------------
5216
5217    fn create_enum_match_project() -> (tempfile::TempDir, Project) {
5218        let dir = tempdir().unwrap();
5219        let src = dir.path().join("src");
5220        fs::create_dir(&src).unwrap();
5221
5222        fs::write(
5223            dir.path().join("Cargo.toml"),
5224            r#"[package]
5225name = "test-enum-match"
5226version = "0.1.0"
5227edition = "2021"
5228"#,
5229        )
5230        .unwrap();
5231
5232        fs::write(
5233            src.join("lib.rs"),
5234            r#"
5235pub enum Status {
5236    Active,
5237    Inactive,
5238}
5239
5240pub fn handle_status(s: Status) -> &'static str {
5241    match s {
5242        Status::Active => "active",
5243        Status::Inactive => "inactive",
5244    }
5245}
5246
5247pub struct Processor;
5248
5249impl Processor {
5250    pub fn label(s: &Status) -> &'static str {
5251        match s {
5252            Status::Active => "A",
5253            Status::Inactive => "I",
5254        }
5255    }
5256}
5257"#,
5258        )
5259        .unwrap();
5260
5261        let project = Project::load(dir.path()).unwrap();
5262        (dir, project)
5263    }
5264
5265    #[test]
5266    fn test_add_variant_cascade_adds_match_arms() {
5267        // Regression E2E: AddVariant should cascade AddMatchArm for both
5268        // free functions and methods, producing total_changes > 0.
5269        let (dir, _project) = create_enum_match_project();
5270        let mut api = Api::from_path(dir.path()).unwrap();
5271
5272        let goal = Goal::new(
5273            "add Pending variant to Status",
5274            Intent::AddVariant {
5275                symbol_id: None,
5276                symbol_path: None,
5277                target_enum: Some("Status".to_string()),
5278                variant_name: "Pending".to_string(),
5279                variant_type: "unit".to_string(),
5280            },
5281        );
5282
5283        let result = api.execute(goal).unwrap();
5284
5285        // The AddVariant itself should succeed
5286        assert!(
5287            result.success,
5288            "AddVariant with cascade should succeed: {:?}",
5289            result
5290        );
5291
5292        // Should have changes: AddVariant (1) + AddMatchArm (2, one per match expr)
5293        assert!(
5294            result.total_changes >= 3,
5295            "Expected at least 3 changes (1 AddVariant + 2 AddMatchArm), got {}",
5296            result.total_changes
5297        );
5298    }
5299
5300    #[test]
5301    fn test_graph_cascade_enum_returns_match_functions() {
5302        // Regression: graph_cascade for an enum should return match_functions,
5303        // not just callers (which are always 0 for enums).
5304        let (dir, _project) = create_enum_match_project();
5305        let mut api = Api::from_path(dir.path()).unwrap();
5306
5307        // Find the Status enum SymbolId
5308        let discover_result = api
5309            .discover(DiscoverRequest {
5310                pattern: "Status".to_string(),
5311                kind: Some(ryo_analysis::SymbolKind::Enum),
5312                ..Default::default()
5313            })
5314            .unwrap();
5315        let status_sym = discover_result
5316            .symbols
5317            .iter()
5318            .find(|s| s.name == "Status")
5319            .expect("Status enum should exist");
5320
5321        let cascade = api
5322            .graph_cascade(CascadeRequest::new(&status_sym.id))
5323            .unwrap();
5324
5325        // match_functions should contain both handle_status and label
5326        assert!(
5327            cascade.match_functions.len() >= 2,
5328            "Expected at least 2 match functions, got {}: {:?}",
5329            cascade.match_functions.len(),
5330            cascade.match_functions
5331        );
5332
5333        let has_handle_status = cascade
5334            .match_functions
5335            .iter()
5336            .any(|f| f.contains("handle_status"));
5337        let has_label = cascade.match_functions.iter().any(|f| f.contains("label"));
5338        assert!(
5339            has_handle_status,
5340            "match_functions should include handle_status: {:?}",
5341            cascade.match_functions
5342        );
5343        assert!(
5344            has_label,
5345            "match_functions should include label: {:?}",
5346            cascade.match_functions
5347        );
5348    }
5349
5350    /// Create a project with TupleStruct variant patterns for match arm testing
5351    fn create_tuple_variant_project() -> (tempfile::TempDir, Project) {
5352        let dir = tempdir().unwrap();
5353        let src = dir.path().join("src");
5354        fs::create_dir(&src).unwrap();
5355
5356        fs::write(
5357            dir.path().join("Cargo.toml"),
5358            r#"[package]
5359name = "test-tuple-variant"
5360version = "0.1.0"
5361edition = "2021"
5362"#,
5363        )
5364        .unwrap();
5365
5366        fs::write(
5367            src.join("lib.rs"),
5368            r#"
5369pub enum Message {
5370    Text(String),
5371    Number(i32),
5372    Empty,
5373}
5374
5375pub fn describe(msg: &Message) -> String {
5376    match msg {
5377        Message::Text(s) => format!("text: {}", s),
5378        Message::Number(n) => format!("num: {}", n),
5379        Message::Empty => "empty".to_string(),
5380    }
5381}
5382"#,
5383        )
5384        .unwrap();
5385
5386        let project = Project::load(dir.path()).unwrap();
5387        (dir, project)
5388    }
5389
5390    #[test]
5391    fn test_replace_match_arm_tuple_variant() {
5392        // Regression: ReplaceMatchArm should work with TupleStruct patterns
5393        // like Message::Text(s), not just unit patterns like Status::Active.
5394        let (dir, _project) = create_tuple_variant_project();
5395        let mut api = Api::from_path(dir.path()).unwrap();
5396
5397        let goal = Goal::new(
5398            "replace Text arm body",
5399            Intent::ReplaceMatchArm {
5400                symbol_id: None,
5401                symbol_path: None,
5402                target_fn: Some("describe".to_string()),
5403                enum_name: "Message".to_string(),
5404                old_pattern: "Message::Text(s)".to_string(),
5405                new_pattern: "Message::Text(s)".to_string(),
5406                new_body: "format!(\"text_v2: {}\", s)".to_string(),
5407            },
5408        );
5409
5410        let result = api.execute(goal).unwrap();
5411        assert!(
5412            result.success && result.total_changes >= 1,
5413            "ReplaceMatchArm with TupleStruct pattern should succeed: {:?}",
5414            result
5415        );
5416    }
5417
5418    #[test]
5419    fn test_replace_match_arm_by_symbol_id() {
5420        // ReplaceMatchArm works with symbol_id
5421        let (dir, _project) = create_enum_match_project();
5422        let mut api = Api::from_path(dir.path()).unwrap();
5423
5424        let discover_result = api
5425            .discover(DiscoverRequest {
5426                pattern: "handle_status".to_string(),
5427                ..Default::default()
5428            })
5429            .unwrap();
5430        let fn_sym = discover_result
5431            .symbols
5432            .iter()
5433            .find(|s| s.name == "handle_status")
5434            .expect("handle_status should exist");
5435
5436        let goal = Goal::new(
5437            "replace Active arm body",
5438            Intent::ReplaceMatchArm {
5439                symbol_id: Some(fn_sym.id.clone()),
5440                symbol_path: None,
5441                target_fn: None,
5442                enum_name: "Status".to_string(),
5443                old_pattern: "Status::Active".to_string(),
5444                new_pattern: "Status::Active".to_string(),
5445                new_body: "\"active_v2\"".to_string(),
5446            },
5447        );
5448
5449        let result = api.execute(goal).unwrap();
5450        assert!(
5451            result.success && result.total_changes >= 1,
5452            "ReplaceMatchArm by symbol_id should succeed: {:?}",
5453            result
5454        );
5455    }
5456
5457    #[test]
5458    fn test_replace_match_arm_by_target_fn() {
5459        // ReplaceMatchArm with target_fn (Priority 3, name-based search)
5460        let (dir, _project) = create_enum_match_project();
5461        let mut api = Api::from_path(dir.path()).unwrap();
5462
5463        let goal = Goal::new(
5464            "replace Active arm body via target_fn",
5465            Intent::ReplaceMatchArm {
5466                symbol_id: None,
5467                symbol_path: None,
5468                target_fn: Some("handle_status".to_string()),
5469                enum_name: "Status".to_string(),
5470                old_pattern: "Status::Active".to_string(),
5471                new_pattern: "Status::Active".to_string(),
5472                new_body: "\"active_v2\"".to_string(),
5473            },
5474        );
5475
5476        let result = api.execute(goal).unwrap();
5477        assert!(
5478            result.success && result.total_changes >= 1,
5479            "ReplaceMatchArm by target_fn should succeed: {:?}",
5480            result
5481        );
5482    }
5483
5484    #[test]
5485    fn test_replace_match_arm_by_symbol_path() {
5486        // ReplaceMatchArm with full symbol_path
5487        let (dir, _project) = create_enum_match_project();
5488        let mut api = Api::from_path(dir.path()).unwrap();
5489
5490        let goal = Goal::new(
5491            "replace Active arm body via symbol_path",
5492            Intent::ReplaceMatchArm {
5493                symbol_id: None,
5494                symbol_path: Some("test_enum_match::handle_status".to_string()),
5495                target_fn: None,
5496                enum_name: "Status".to_string(),
5497                old_pattern: "Status::Active".to_string(),
5498                new_pattern: "Status::Active".to_string(),
5499                new_body: "\"active_v2\"".to_string(),
5500            },
5501        );
5502
5503        let result = api.execute(goal).unwrap();
5504        assert!(
5505            result.success && result.total_changes >= 1,
5506            "ReplaceMatchArm by symbol_path should succeed: {:?}",
5507            result
5508        );
5509    }
5510
5511    // -----------------------------------------------------------------------
5512    // Bug #4: AddVariant cascade — multi-match function overmatch
5513    // -----------------------------------------------------------------------
5514
5515    /// Create a project where functions have multiple match expressions
5516    /// on DIFFERENT types to test cascade specificity.
5517    fn create_multi_match_project() -> (tempfile::TempDir, Project) {
5518        let dir = tempdir().unwrap();
5519        let src = dir.path().join("src");
5520        fs::create_dir(&src).unwrap();
5521        fs::write(
5522            dir.path().join("Cargo.toml"),
5523            r#"[package]
5524name = "test-multi-match"
5525version = "0.1.0"
5526edition = "2021"
5527"#,
5528        )
5529        .unwrap();
5530        fs::write(
5531            src.join("lib.rs"),
5532            r#"
5533pub enum Filter {
5534    Recurse(usize),
5535    Exclude(String),
5536}
5537
5538pub enum FilterKind {
5539    Inclusive,
5540    Exclusive,
5541}
5542
5543pub fn evaluate(f: Filter, name: &str) -> String {
5544    // First: match on Option (unrelated)
5545    let idx = match name.find('/') {
5546        Some(i) => i,
5547        None => 0,
5548    };
5549    // Second: match on Filter (the target)
5550    match f {
5551        Filter::Recurse(depth) => format!("recurse {}", depth),
5552        Filter::Exclude(pat) => format!("exclude {}", pat),
5553    }
5554}
5555
5556pub fn classify(fk: FilterKind) -> &'static str {
5557    // Matches on FilterKind — NOT Filter
5558    // But "FilterKind::Inclusive".contains("Filter") == true (false positive)
5559    match fk {
5560        FilterKind::Inclusive => "inclusive",
5561        FilterKind::Exclusive => "exclusive",
5562    }
5563}
5564
5565pub fn nested_eval(f: Filter) -> String {
5566    // Nested match on the SAME enum — should still produce only 1 arm addition
5567    match f {
5568        Filter::Recurse(depth) => {
5569            match f {
5570                Filter::Recurse(d) => format!("double recurse {}", d),
5571                Filter::Exclude(_) => String::new(),
5572            }
5573        }
5574        Filter::Exclude(pat) => format!("exclude {}", pat),
5575    }
5576}
5577"#,
5578        )
5579        .unwrap();
5580        let project = Project::load(dir.path()).unwrap();
5581        (dir, project)
5582    }
5583
5584    #[test]
5585    fn test_add_variant_cascade_no_overmatch() {
5586        // AddVariant(Filter::Map(String)) should:
5587        // 1. Add variant Map(String) to enum Filter
5588        // 2. Add match arm Filter::Map(_) ONLY to evaluate() — NOT type_name()
5589        // 3. No duplicate arms
5590        // 4. Pattern must include "Filter::" prefix
5591        let (dir, _project) = create_multi_match_project();
5592        let mut api = Api::from_path(dir.path()).unwrap();
5593
5594        let goal = Goal::new(
5595            "add Map variant to Filter",
5596            Intent::AddVariant {
5597                symbol_id: None,
5598                symbol_path: None,
5599                target_enum: Some("Filter".to_string()),
5600                variant_name: "Map".to_string(),
5601                variant_type: "tuple:String".to_string(),
5602            },
5603        );
5604
5605        let result = api.execute(goal).unwrap();
5606        assert!(
5607            result.success && result.total_changes >= 1,
5608            "AddVariant cascade should succeed with changes: {:?}",
5609            result
5610        );
5611
5612        // Read the generated source from in-memory context
5613        // (execute() doesn't write to disk — it updates the AST registry)
5614        let src_content = {
5615            let ctx = api.context();
5616            let mut source = String::new();
5617            for (_wfp, file) in ctx.files().iter() {
5618                source = file.to_source().unwrap();
5619            }
5620            source
5621        };
5622
5623        // The Filter enum should have the new variant
5624        assert!(
5625            src_content.contains("Map(String)"),
5626            "Filter enum should contain Map(String):\n{}",
5627            src_content
5628        );
5629
5630        // Count Filter::Map occurrences in match arm positions.
5631        // Expected: exactly 2 (evaluate: 1, nested_eval: 1 in outer match).
5632        // The walker returns true after the first matching match expression,
5633        // so nested_eval's inner match does not get a separate arm.
5634        // NOT in classify (FilterKind != Filter).
5635        let map_arm_count = src_content.matches("Filter::Map").count();
5636        assert_eq!(
5637            map_arm_count, 2,
5638            "Should have exactly 2 Filter::Map arms (evaluate=1, nested_eval=1), got {}:\n{}",
5639            map_arm_count, src_content
5640        );
5641
5642        // classify() should NOT have any Map arm
5643        // (FilterKind::Inclusive contains "Filter" as substring, but is a different enum)
5644        let classify_start = src_content
5645            .find("fn classify")
5646            .expect("classify should exist");
5647        let classify_end = src_content[classify_start..]
5648            .find("\n}\n")
5649            .map(|i| classify_start + i + 3)
5650            .unwrap_or(src_content.len());
5651        let classify_body = &src_content[classify_start..classify_end];
5652        assert!(
5653            !classify_body.contains("Filter::Map"),
5654            "classify() should NOT have Filter::Map arm (FilterKind != Filter):\n{}",
5655            classify_body
5656        );
5657
5658        // nested_eval: should NOT have duplicate Filter::Map arms
5659        let nested_start = src_content
5660            .find("fn nested_eval")
5661            .expect("nested_eval should exist");
5662        let nested_body = &src_content[nested_start..];
5663        let nested_map_count = nested_body.matches("Filter::Map").count();
5664        // nested_eval has 2 match expressions on Filter.
5665        // Each should get at most 1 Filter::Map arm.
5666        assert!(
5667            nested_map_count <= 2,
5668            "nested_eval() should have at most 2 Filter::Map arms (one per match), got {}:\n{}",
5669            nested_map_count,
5670            nested_body
5671        );
5672
5673        // No "Map(_)" without "Filter::" prefix in match arms
5674        let has_bare_map = src_content.lines().any(|line| {
5675            let trimmed = line.trim();
5676            trimmed.contains("Map(_)") && !trimmed.contains("Filter::Map")
5677        });
5678        assert!(
5679            !has_bare_map,
5680            "Should not have bare 'Map(_)' without 'Filter::' prefix:\n{}",
5681            src_content
5682        );
5683    }
5684
5685    // =========================================================================
5686    // InsertStatement AfterPattern E2E tests
5687    // =========================================================================
5688
5689    fn create_insert_statement_project() -> (tempfile::TempDir, Project) {
5690        let dir = tempdir().unwrap();
5691        let src = dir.path().join("src");
5692        fs::create_dir(&src).unwrap();
5693
5694        fs::write(
5695            dir.path().join("Cargo.toml"),
5696            r#"[package]
5697name = "test-insert"
5698version = "0.1.0"
5699edition = "2021"
5700"#,
5701        )
5702        .unwrap();
5703
5704        fs::write(
5705            src.join("lib.rs"),
5706            r#"
5707pub fn setup(config: &mut Config) {
5708    let db = connect_db();
5709    let cache = init_cache();
5710    start_server();
5711}
5712"#,
5713        )
5714        .unwrap();
5715
5716        let project = Project::load(dir.path()).unwrap();
5717        (dir, project)
5718    }
5719
5720    #[test]
5721    fn test_insert_statement_after_pattern_e2e() {
5722        let (dir, _project) = create_insert_statement_project();
5723        let mut api = Api::from_path(dir.path()).unwrap();
5724
5725        let goal = Goal::new(
5726            "insert pool init after db connect",
5727            Intent::InsertStatement {
5728                target_mod: None,
5729                target_fn: "setup".to_string(),
5730                stmt: "let pool = create_pool(&db);".to_string(),
5731                position: StmtInsertPosition::AfterPattern,
5732                reference_pattern: Some("let db = connect_db()".to_string()),
5733                symbol_path: None,
5734            },
5735        );
5736
5737        let result = api.execute(goal).unwrap();
5738        eprintln!("InsertStatement AfterPattern result: {:?}", result);
5739
5740        assert!(
5741            result.success,
5742            "InsertStatement AfterPattern should succeed: {:?}",
5743            result
5744        );
5745        assert!(
5746            result.total_changes >= 1,
5747            "Should have at least 1 change, got {}. Result: {:?}",
5748            result.total_changes,
5749            result
5750        );
5751
5752        // Verify the inserted statement appears after the reference
5753        let src_content = {
5754            let ctx = api.context();
5755            let mut source = String::new();
5756            for (_wfp, file) in ctx.files().iter() {
5757                source = file.to_source().unwrap();
5758            }
5759            source
5760        };
5761
5762        assert!(
5763            src_content.contains("create_pool"),
5764            "Inserted statement should appear in output:\n{}",
5765            src_content
5766        );
5767
5768        // Verify ordering: "connect_db" should appear before "create_pool"
5769        let db_pos = src_content
5770            .find("connect_db")
5771            .expect("connect_db should exist");
5772        let pool_pos = src_content
5773            .find("create_pool")
5774            .expect("create_pool should exist");
5775        assert!(
5776            db_pos < pool_pos,
5777            "connect_db should appear before create_pool:\n{}",
5778            src_content
5779        );
5780    }
5781
5782    #[test]
5783    fn test_insert_statement_after_pattern_macro_e2e() {
5784        // Test with macro statement as reference pattern
5785        let dir = tempdir().unwrap();
5786        let src = dir.path().join("src");
5787        fs::create_dir(&src).unwrap();
5788
5789        fs::write(
5790            dir.path().join("Cargo.toml"),
5791            r#"[package]
5792name = "test-insert-macro"
5793version = "0.1.0"
5794edition = "2021"
5795"#,
5796        )
5797        .unwrap();
5798
5799        fs::write(
5800            src.join("lib.rs"),
5801            r#"
5802pub fn run() {
5803    println!("starting");
5804    do_work();
5805    println!("done");
5806}
5807"#,
5808        )
5809        .unwrap();
5810
5811        let _project = Project::load(dir.path()).unwrap();
5812        let mut api = Api::from_path(dir.path()).unwrap();
5813
5814        let goal = Goal::new(
5815            "insert log after starting",
5816            Intent::InsertStatement {
5817                target_mod: None,
5818                target_fn: "run".to_string(),
5819                stmt: r#"println!("logging");"#.to_string(),
5820                position: StmtInsertPosition::AfterPattern,
5821                reference_pattern: Some(r#"println!("starting")"#.to_string()),
5822                symbol_path: None,
5823            },
5824        );
5825
5826        let result = api.execute(goal).unwrap();
5827        eprintln!("InsertStatement macro AfterPattern result: {:?}", result);
5828
5829        assert!(
5830            result.success,
5831            "InsertStatement macro AfterPattern should succeed: {:?}",
5832            result
5833        );
5834        assert!(
5835            result.total_changes >= 1,
5836            "Should have at least 1 change, got {}. Result: {:?}",
5837            result.total_changes,
5838            result
5839        );
5840    }
5841
5842    /// JSON経由のデシリアライズ: 正しいフィールド名で成功するか
5843    #[test]
5844    fn test_insert_statement_json_deserialize_correct_fields() {
5845        let json = serde_json::json!({
5846            "type": "InsertStatement",
5847            "target_fn": "setup",
5848            "stmt": "let pool = create_pool();",
5849            "position": "after_pattern",
5850            "reference_pattern": "let db = connect_db()"
5851        });
5852        let intent: Result<Intent, _> = serde_json::from_value(json);
5853        assert!(
5854            intent.is_ok(),
5855            "Correct field names should deserialize: {:?}",
5856            intent.err()
5857        );
5858    }
5859}