airs_memspec/parser/
navigation.rs

1//! Memory bank file system navigation and discovery
2//!
3//! This module provides functionality for discovering, validating, and navigating
4//! Multi-Project Memory Bank directory structures and files.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use crate::utils::fs::FsResult;
10
11/// Memory bank directory structure representation
12///
13/// This structure represents the complete discovered layout of a Multi-Project Memory Bank,
14/// including all workspace-level files, sub-projects, and their associated task files.
15/// It serves as the primary data structure for navigation and file access operations.
16#[derive(Debug, Clone)]
17pub struct MemoryBankStructure {
18    /// Root path of the memory bank (.copilot/memory_bank/)
19    /// This is the base directory from which all other paths are resolved
20    pub root_path: PathBuf,
21
22    /// Workspace-level files and configuration
23    /// Contains shared patterns, project briefs, and workspace-wide documentation
24    pub workspace: WorkspaceFiles,
25
26    /// Current context file (current_context.md)
27    /// Tracks the currently active sub-project and workspace state
28    pub current_context: Option<PathBuf>,
29
30    /// Context snapshots directory (context_snapshots/)
31    /// Contains historical snapshots for restoration and analysis
32    pub snapshots_dir: Option<PathBuf>,
33
34    /// Sub-projects directory (sub_projects/)
35    /// Root directory containing all individual project folders
36    pub sub_projects_dir: Option<PathBuf>,
37
38    /// Discovered sub-projects and their complete file structures
39    /// Maps sub-project names to their discovered file layouts
40    pub sub_projects: HashMap<String, SubProjectFiles>,
41}
42/// Workspace-level file structure
43///
44/// Represents the files found in the workspace/ directory, which contain
45/// shared configuration, patterns, and documentation that apply across
46/// all sub-projects in the workspace.
47#[derive(Debug, Clone)]
48pub struct WorkspaceFiles {
49    /// workspace/project_brief.md - Overall workspace vision and objectives
50    /// Contains the high-level description of what this workspace aims to achieve
51    pub project_brief: Option<PathBuf>,
52
53    /// workspace/shared_patterns.md - Cross-project patterns and practices
54    /// Documents reusable patterns, coding standards, and architectural decisions
55    pub shared_patterns: Option<PathBuf>,
56
57    /// workspace/workspace_architecture.md - High-level system architecture
58    /// Describes the overall structure and relationships between components
59    pub workspace_architecture: Option<PathBuf>,
60
61    /// workspace/workspace_progress.md - Cross-project progress and milestones
62    /// Tracks workspace-wide progress, strategic decisions, and major milestones
63    pub workspace_progress: Option<PathBuf>,
64}
65
66/// Sub-project file structure
67///
68/// Represents the complete file layout of an individual sub-project within
69/// the workspace. Each sub-project has its own directory with standardized
70/// files for project management, technical documentation, and task tracking.
71#[derive(Debug, Clone)]
72pub struct SubProjectFiles {
73    /// Sub-project root directory (sub_projects/{project_name}/)
74    /// Base path for all files belonging to this specific sub-project
75    pub root_path: PathBuf,
76
77    /// project_brief.md - Sub-project foundation and scope definition
78    /// Defines what this specific sub-project does and why it exists
79    pub project_brief: Option<PathBuf>,
80
81    /// product_context.md - User experience and product requirements
82    /// Documents user needs, functionality, and success criteria
83    pub product_context: Option<PathBuf>,
84
85    /// active_context.md - Current work focus and recent changes
86    /// Tracks what's currently being worked on and immediate next steps
87    pub active_context: Option<PathBuf>,
88
89    /// system_patterns.md - Technical architecture and design patterns
90    /// Documents the technical approach, patterns, and architectural decisions
91    pub system_patterns: Option<PathBuf>,
92
93    /// tech_context.md - Technology stack and infrastructure details
94    /// Lists technologies used, dependencies, constraints, and deployment context
95    pub tech_context: Option<PathBuf>,
96
97    /// progress.md - Work completed, current status, and known issues
98    /// Tracks what's working, what's left to build, and current challenges
99    pub progress: Option<PathBuf>,
100
101    /// tasks/ directory - Contains all task management files
102    /// Directory where individual task files and the task index are stored
103    pub tasks_dir: Option<PathBuf>,
104
105    /// Individual task files (task_*.md files excluding _index.md)
106    /// All discovered task files with their complete paths for direct access
107    pub task_files: Vec<PathBuf>,
108}
109
110/// Memory bank discovery and navigation functionality
111///
112/// This struct provides static methods for discovering, analyzing, and validating
113/// Multi-Project Memory Bank directory structures. It implements the core logic
114/// for finding memory banks, parsing their contents, and providing access to
115/// discovered files and metadata.
116///
117/// The navigator follows these key principles:
118/// - Upward directory traversal to find memory bank roots
119/// - Comprehensive file discovery with graceful handling of missing files
120/// - Detailed validation and diagnostic reporting
121/// - Consistent path resolution and accessibility checking
122pub struct MemoryBankNavigator;
123
124impl MemoryBankNavigator {
125    /// Discover memory bank structure starting from a given path
126    ///
127    /// This function will traverse the directory structure looking for
128    /// `.copilot/memory_bank/` and analyze its contents.
129    ///
130    /// # Arguments
131    /// * `start_path` - Path to start searching from (typically workspace root)
132    ///
133    /// # Returns
134    /// * `Ok(MemoryBankStructure)` - If memory bank structure is found
135    /// * `Err(FsError)` - If there are file system errors or structure not found
136    ///
137    /// # Examples
138    /// ```rust,no_run
139    /// use airs_memspec::parser::navigation::MemoryBankNavigator;
140    /// use std::path::PathBuf;
141    ///
142    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
143    /// let structure = MemoryBankNavigator::discover_structure(
144    ///     &PathBuf::from("/workspace/project")
145    /// )?;
146    /// println!("Found {} sub-projects", structure.sub_projects.len());
147    /// # Ok(())
148    /// # }
149    /// ```
150    pub fn discover_structure(start_path: &Path) -> FsResult<MemoryBankStructure> {
151        let memory_bank_path = Self::find_memory_bank_root(start_path)?;
152        
153        // Validate memory bank structure before analyzing
154        crate::utils::fs::validate_memory_bank_structure(
155            memory_bank_path.parent()
156                .and_then(|p| p.parent())
157                .unwrap_or(start_path)
158        )?;
159        
160        Self::analyze_structure(&memory_bank_path)
161    }
162
163    /// Find the memory bank root directory
164    ///
165    /// Performs upward directory traversal starting from the given path to locate
166    /// the `.copilot/memory_bank/` directory. This allows the navigator to work
167    /// from any location within a workspace hierarchy.
168    ///
169    /// # Arguments
170    /// * `start_path` - Starting point for the upward search
171    ///
172    /// # Returns
173    /// * `Ok(PathBuf)` - Path to the memory bank root if found
174    /// * `Err(FsError::PathNotFound)` - If no memory bank is found in the hierarchy
175    ///
176    /// # Search Strategy
177    /// 1. Check current directory for `.copilot/memory_bank/`
178    /// 2. Move up one directory level and repeat
179    /// 3. Continue until root directory is reached
180    /// 4. Return error if no memory bank directory is found
181    fn find_memory_bank_root(start_path: &Path) -> FsResult<PathBuf> {
182        let mut current_path = start_path.to_path_buf();
183
184        loop {
185            // Construct the expected memory bank path at this level
186            let memory_bank_path = current_path.join(".copilot").join("memory_bank");
187
188            // Check if this directory exists and is actually a directory
189            if memory_bank_path.exists() && memory_bank_path.is_dir() {
190                return Ok(memory_bank_path);
191            }
192
193            // Move up one directory level
194            match current_path.parent() {
195                Some(parent) => current_path = parent.to_path_buf(),
196                None => break, // Reached filesystem root without finding memory bank
197            }
198        }
199
200        // No memory bank found in the entire hierarchy
201        Err(crate::utils::fs::FsError::PathNotFound {
202            path: PathBuf::from(".copilot/memory_bank"),
203        })
204    }
205
206    /// Analyze the memory bank directory structure
207    ///
208    /// Once the memory bank root is found, this method performs a comprehensive
209    /// analysis of its contents, discovering all workspace files, sub-projects,
210    /// and their associated task files.
211    ///
212    /// # Arguments
213    /// * `memory_bank_path` - Root path of the memory bank directory
214    ///
215    /// # Returns
216    /// * `Ok(MemoryBankStructure)` - Complete structure representation
217    /// * `Err(FsError)` - File system errors during discovery
218    ///
219    /// # Discovery Process
220    /// 1. Discover workspace-level files in workspace/ directory
221    /// 2. Find current_context.md and context_snapshots/ directory
222    /// 3. Locate sub_projects/ directory
223    /// 4. Recursively discover all sub-projects and their files
224    /// 5. Build complete structure representation
225    fn analyze_structure(memory_bank_path: &Path) -> FsResult<MemoryBankStructure> {
226        let mut structure = MemoryBankStructure {
227            root_path: memory_bank_path.to_path_buf(),
228            workspace: Self::discover_workspace_files(memory_bank_path)?,
229            current_context: Self::find_file(memory_bank_path, "current_context.md"),
230            snapshots_dir: Self::find_directory(memory_bank_path, "context_snapshots"),
231            sub_projects_dir: Self::find_directory(memory_bank_path, "sub_projects"),
232            sub_projects: HashMap::new(),
233        };
234
235        // Discover sub-projects if the directory exists
236        // This is optional as some workspaces might not have sub-projects yet
237        if let Some(sub_projects_dir) = &structure.sub_projects_dir {
238            structure.sub_projects = Self::discover_sub_projects(sub_projects_dir)?;
239        }
240
241        Ok(structure)
242    }
243
244    /// Discover workspace-level files
245    ///
246    /// Searches the workspace/ directory for standard Multi-Project Memory Bank
247    /// workspace files. These files contain configuration and documentation
248    /// that applies across all sub-projects.
249    ///
250    /// # Arguments
251    /// * `memory_bank_path` - Root path of the memory bank
252    ///
253    /// # Returns
254    /// * `Ok(WorkspaceFiles)` - Structure containing paths to found workspace files
255    /// * `Err(FsError)` - File system errors during discovery
256    ///
257    /// # Files Discovered
258    /// - project_brief.md: Workspace vision and objectives
259    /// - shared_patterns.md: Cross-project patterns and practices
260    /// - workspace_architecture.md: High-level system architecture
261    /// - workspace_progress.md: Cross-project progress tracking
262    fn discover_workspace_files(memory_bank_path: &Path) -> FsResult<WorkspaceFiles> {
263        let workspace_dir = memory_bank_path.join("workspace");
264
265        Ok(WorkspaceFiles {
266            project_brief: Self::find_file(&workspace_dir, "project_brief.md"),
267            shared_patterns: Self::find_file(&workspace_dir, "shared_patterns.md"),
268            workspace_architecture: Self::find_file(&workspace_dir, "workspace_architecture.md"),
269            workspace_progress: Self::find_file(&workspace_dir, "workspace_progress.md"),
270        })
271    }
272
273    /// Discover all sub-projects in the sub_projects directory
274    ///
275    /// Iterates through the sub_projects/ directory to find all individual
276    /// project folders, then analyzes each one to discover its file structure.
277    ///
278    /// # Arguments
279    /// * `sub_projects_dir` - Path to the sub_projects directory
280    ///
281    /// # Returns
282    /// * `Ok(HashMap<String, SubProjectFiles>)` - Map of project names to their file structures
283    /// * `Err(FsError)` - File system errors during discovery
284    ///
285    /// # Discovery Process
286    /// 1. Read all entries in the sub_projects directory
287    /// 2. Filter for directories only (ignore files)
288    /// 3. For each directory, discover its internal file structure
289    /// 4. Map directory names to their discovered file layouts
290    /// 5. Return complete mapping of all sub-projects
291    fn discover_sub_projects(
292        sub_projects_dir: &Path,
293    ) -> FsResult<HashMap<String, SubProjectFiles>> {
294        let mut sub_projects = HashMap::new();
295
296        // Gracefully handle case where sub_projects directory doesn't exist
297        if !sub_projects_dir.exists() {
298            return Ok(sub_projects);
299        }
300
301        let entries = std::fs::read_dir(sub_projects_dir).map_err(crate::utils::fs::FsError::Io)?;
302
303        for entry in entries {
304            let entry = entry.map_err(crate::utils::fs::FsError::Io)?;
305            let path = entry.path();
306
307            // Only process directories (sub-projects), ignore files
308            if path.is_dir() {
309                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
310                    // Discover the file structure for this specific sub-project
311                    let sub_project_files = Self::discover_sub_project_files(&path)?;
312                    sub_projects.insert(name.to_string(), sub_project_files);
313                }
314            }
315        }
316
317        Ok(sub_projects)
318    }
319
320    /// Discover files within a specific sub-project directory
321    ///
322    /// Analyzes a single sub-project directory to find all standard Multi-Project
323    /// Memory Bank files and the tasks directory with its associated task files.
324    ///
325    /// # Arguments
326    /// * `sub_project_path` - Root path of the sub-project directory
327    ///
328    /// # Returns
329    /// * `Ok(SubProjectFiles)` - Complete file structure for this sub-project
330    /// * `Err(FsError)` - File system errors during discovery
331    ///
332    /// # Files Discovered
333    /// - Core memory bank files (project_brief.md, product_context.md, etc.)
334    /// - tasks/ directory if it exists
335    /// - Individual task files within the tasks/ directory
336    ///
337    /// # Discovery Strategy
338    /// - Uses standard file discovery for core memory bank files
339    /// - Special handling for tasks directory to find individual task files
340    /// - Returns complete sub-project structure with all found files
341    fn discover_sub_project_files(sub_project_path: &Path) -> FsResult<SubProjectFiles> {
342        let tasks_dir = sub_project_path.join("tasks");
343
344        // Discover task files if the tasks directory exists
345        let task_files = if tasks_dir.exists() {
346            Self::discover_task_files(&tasks_dir)?
347        } else {
348            Vec::new() // No task files if tasks directory doesn't exist
349        };
350
351        Ok(SubProjectFiles {
352            root_path: sub_project_path.to_path_buf(),
353            // Discover all standard sub-project files
354            project_brief: Self::find_file(sub_project_path, "project_brief.md"),
355            product_context: Self::find_file(sub_project_path, "product_context.md"),
356            active_context: Self::find_file(sub_project_path, "active_context.md"),
357            system_patterns: Self::find_file(sub_project_path, "system_patterns.md"),
358            tech_context: Self::find_file(sub_project_path, "tech_context.md"),
359            progress: Self::find_file(sub_project_path, "progress.md"),
360            // Tasks directory (optional)
361            tasks_dir: if tasks_dir.exists() {
362                Some(tasks_dir)
363            } else {
364                None
365            },
366            task_files,
367        })
368    }
369
370    /// Discover task files in the tasks directory
371    ///
372    /// Finds all individual task files within a sub-project's tasks/ directory.
373    /// Excludes the _index.md file as it's a special index file, not a task file.
374    ///
375    /// # Arguments
376    /// * `tasks_dir` - Path to the tasks directory
377    ///
378    /// # Returns
379    /// * `Ok(Vec<PathBuf>)` - List of task file paths, sorted by filename
380    /// * `Err(FsError)` - File system errors during discovery
381    ///
382    /// # Discovery Rules
383    /// - Only include .md files (Markdown task files)
384    /// - Exclude _index.md (special index file, not a task)
385    /// - Sort results by filename for consistent ordering
386    /// - Return empty vector if no task files found
387    fn discover_task_files(tasks_dir: &Path) -> FsResult<Vec<PathBuf>> {
388        let mut task_files = Vec::new();
389
390        let entries = std::fs::read_dir(tasks_dir).map_err(crate::utils::fs::FsError::Io)?;
391
392        for entry in entries {
393            let entry = entry.map_err(crate::utils::fs::FsError::Io)?;
394            let path = entry.path();
395
396            // Only process regular files, not directories
397            if path.is_file() {
398                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
399                    // Include .md files that are not the index file
400                    // Task files follow the pattern task_*.md
401                    if name.ends_with(".md") && name != "_index.md" {
402                        task_files.push(path);
403                    }
404                }
405            }
406        }
407
408        // Sort task files by name for consistent ordering across discovery runs
409        task_files.sort();
410        Ok(task_files)
411    }
412
413    /// Find a specific file within a directory
414    ///
415    /// Safely checks for the existence of a named file within a given directory.
416    /// Returns the full path if found, or None if the file doesn't exist or
417    /// the path refers to a directory instead of a file.
418    ///
419    /// # Arguments
420    /// * `dir` - Directory to search within
421    /// * `filename` - Name of the file to find
422    ///
423    /// # Returns
424    /// * `Some(PathBuf)` - Full path to the file if found and is a regular file
425    /// * `None` - File not found, doesn't exist, or is not a regular file
426    ///
427    /// # Safety
428    /// This method validates that the found path is actually a file (not a directory)
429    /// before returning it, ensuring type safety for file operations.
430    fn find_file(dir: &Path, filename: &str) -> Option<PathBuf> {
431        let file_path = dir.join(filename);
432        if file_path.exists() && file_path.is_file() {
433            Some(file_path)
434        } else {
435            None
436        }
437    }
438
439    /// Find a specific directory within a parent directory
440    ///
441    /// Safely checks for the existence of a named directory within a given parent.
442    /// Returns the full path if found, or None if the directory doesn't exist or
443    /// the path refers to a file instead of a directory.
444    ///
445    /// # Arguments
446    /// * `dir` - Parent directory to search within
447    /// * `dirname` - Name of the directory to find
448    ///
449    /// # Returns
450    /// * `Some(PathBuf)` - Full path to the directory if found and is a directory
451    /// * `None` - Directory not found, doesn't exist, or is not a directory
452    ///
453    /// # Safety
454    /// This method validates that the found path is actually a directory (not a file)
455    /// before returning it, ensuring type safety for directory operations.
456    fn find_directory(dir: &Path, dirname: &str) -> Option<PathBuf> {
457        let dir_path = dir.join(dirname);
458        if dir_path.exists() && dir_path.is_dir() {
459            Some(dir_path)
460        } else {
461            None
462        }
463    }
464
465    /// Validate memory bank structure completeness
466    ///
467    /// Checks for required files and provides diagnostics about what's missing.
468    ///
469    /// # Arguments
470    /// * `structure` - The discovered memory bank structure
471    ///
472    /// # Returns
473    /// A vector of validation messages (warnings about missing files)
474    pub fn validate_structure(structure: &MemoryBankStructure) -> Vec<String> {
475        let mut warnings = Vec::new();
476
477        // Check workspace files
478        if structure.workspace.project_brief.is_none() {
479            warnings.push("Missing workspace/project_brief.md".to_string());
480        }
481        if structure.workspace.shared_patterns.is_none() {
482            warnings.push("Missing workspace/shared_patterns.md".to_string());
483        }
484
485        // Check current context
486        if structure.current_context.is_none() {
487            warnings.push("Missing current_context.md".to_string());
488        }
489
490        // Check sub-projects
491        if structure.sub_projects.is_empty() {
492            warnings.push("No sub-projects found in sub_projects/ directory".to_string());
493        } else {
494            for (name, sub_project) in &structure.sub_projects {
495                if sub_project.project_brief.is_none() {
496                    warnings.push(format!("Missing {name}/project_brief.md"));
497                }
498                if sub_project.active_context.is_none() {
499                    warnings.push(format!("Missing {name}/active_context.md"));
500                }
501                if sub_project.progress.is_none() {
502                    warnings.push(format!("Missing {name}/progress.md"));
503                }
504            }
505        }
506
507        warnings
508    }
509
510    /// Extract the active sub-project name from current_context.md
511    ///
512    /// Parses the current_context.md file to identify which sub-project is currently
513    /// active for development work. This enables context-aware operations and helps
514    /// tools know which sub-project to focus on for commands and navigation.
515    ///
516    /// # Arguments
517    /// * `structure` - The discovered memory bank structure containing file paths
518    ///
519    /// # Returns
520    /// * `Ok(Some(String))` - Name of the active sub-project if found and valid
521    /// * `Ok(None)` - No current context file or no active project specified
522    /// * `Err(FsError)` - File reading errors or permission issues
523    ///
524    /// # File Format
525    /// Expects current_context.md to contain a line in this format:
526    /// `**active_sub_project:** project_name`
527    ///
528    /// # Example
529    /// ```markdown
530    /// # Current Context
531    /// **active_sub_project:** analytics_engine
532    /// ```
533    pub fn get_active_sub_project(structure: &MemoryBankStructure) -> FsResult<Option<String>> {
534        if let Some(current_context_path) = &structure.current_context {
535            let content = std::fs::read_to_string(current_context_path)
536                .map_err(crate::utils::fs::FsError::Io)?;
537
538            // Parse content line by line to find active sub-project declaration
539            for line in content.lines() {
540                if line.starts_with("**active_sub_project:**") {
541                    // Extract project name after the marker
542                    if let Some(remainder) = line.strip_prefix("**active_sub_project:**") {
543                        let project_name = remainder.trim();
544                        if !project_name.is_empty() {
545                            return Ok(Some(project_name.to_string()));
546                        }
547                    }
548                }
549            }
550        }
551
552        Ok(None)
553    }
554
555    /// Verify path accessibility and existence
556    ///
557    /// Safely checks whether a path exists and is accessible for read operations.
558    /// This is a fundamental utility for validating file system access before
559    /// attempting to read files or traverse directories.
560    ///
561    /// # Arguments
562    /// * `path` - File or directory path to verify
563    ///
564    /// # Returns
565    /// * `Ok(true)` - Path exists and is readable/accessible
566    /// * `Ok(false)` - Path does not exist (but no access errors)
567    /// * `Err(FsError)` - Permission denied or other file system errors
568    ///
569    /// # Error Handling
570    /// - Permission errors are wrapped in FsError::PermissionDenied
571    /// - Other I/O errors are wrapped in FsError::Io
572    /// - Non-existence is returned as Ok(false), not an error
573    ///
574    /// # Use Cases
575    /// - Validate paths before attempting file operations
576    /// - Check accessibility of memory bank directories
577    /// - Diagnose permission issues during discovery
578    pub fn path_is_accessible(path: &Path) -> FsResult<bool> {
579        match path.try_exists() {
580            Ok(exists) => Ok(exists),
581            Err(e) => match e.kind() {
582                std::io::ErrorKind::PermissionDenied => {
583                    Err(crate::utils::fs::FsError::PermissionDenied {
584                        path: path.to_path_buf(),
585                    })
586                }
587                _ => Err(crate::utils::fs::FsError::Io(e)),
588            },
589        }
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use std::fs;
597    use tempfile::TempDir;
598
599    /// Create a test memory bank structure for testing
600    fn create_test_memory_bank() -> TempDir {
601        let temp_dir = TempDir::new().unwrap();
602        let memory_bank_path = temp_dir.path().join(".copilot").join("memory_bank");
603
604        // Create directory structure
605        fs::create_dir_all(&memory_bank_path).unwrap();
606        fs::create_dir_all(memory_bank_path.join("workspace")).unwrap();
607        fs::create_dir_all(
608            memory_bank_path
609                .join("sub_projects")
610                .join("test-project")
611                .join("tasks"),
612        )
613        .unwrap();
614
615        // Create files
616        fs::write(
617            memory_bank_path.join("current_context.md"),
618            "**active_sub_project:** test-project\n",
619        )
620        .unwrap();
621
622        fs::write(
623            memory_bank_path.join("workspace").join("project_brief.md"),
624            "# Workspace Brief\n",
625        )
626        .unwrap();
627
628        fs::write(
629            memory_bank_path
630                .join("sub_projects")
631                .join("test-project")
632                .join("project_brief.md"),
633            "# Test Project Brief\n",
634        )
635        .unwrap();
636
637        temp_dir
638    }
639
640    #[test]
641    fn test_discover_structure() {
642        let temp_dir = create_test_memory_bank();
643        let structure = MemoryBankNavigator::discover_structure(temp_dir.path()).unwrap();
644
645        assert!(structure.current_context.is_some());
646        assert!(structure.workspace.project_brief.is_some());
647        assert_eq!(structure.sub_projects.len(), 1);
648        assert!(structure.sub_projects.contains_key("test-project"));
649    }
650
651    #[test]
652    fn test_get_active_sub_project() {
653        let temp_dir = create_test_memory_bank();
654        let structure = MemoryBankNavigator::discover_structure(temp_dir.path()).unwrap();
655
656        let active_project = MemoryBankNavigator::get_active_sub_project(&structure).unwrap();
657        assert_eq!(active_project, Some("test-project".to_string()));
658    }
659
660    #[test]
661    fn test_validate_structure() {
662        let temp_dir = create_test_memory_bank();
663        let structure = MemoryBankNavigator::discover_structure(temp_dir.path()).unwrap();
664
665        let warnings = MemoryBankNavigator::validate_structure(&structure);
666
667        // Should have some warnings for missing files
668        assert!(!warnings.is_empty());
669        assert!(warnings.iter().any(|w| w.contains("shared_patterns.md")));
670    }
671}