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}