1use std::collections::HashMap;
8use std::path::Path;
9
10use chrono::{DateTime, Utc};
11
12use crate::models::workspace::CurrentContext;
13use crate::parser::markdown::{MarkdownContent, MarkdownParser, TaskItem, TaskStatus};
14use crate::parser::navigation::{MemoryBankNavigator, MemoryBankStructure};
15use crate::utils::fs::{FsError, FsResult};
16
17#[derive(Debug)]
22pub struct ContextCorrelator {
23 workspace_context: Option<WorkspaceContext>,
25}
26
27#[derive(Debug, Clone)]
32pub struct WorkspaceContext {
33 pub structure: MemoryBankStructure,
35
36 pub current_context: CurrentContext,
38
39 pub workspace_content: WorkspaceContent,
41
42 pub sub_project_contexts: HashMap<String, SubProjectContext>,
44
45 pub last_updated: DateTime<Utc>,
47}
48
49#[derive(Debug, Clone)]
51pub struct WorkspaceContent {
52 pub project_brief: Option<MarkdownContent>,
54
55 pub shared_patterns: Option<MarkdownContent>,
57
58 pub workspace_architecture: Option<MarkdownContent>,
60
61 pub workspace_progress: Option<MarkdownContent>,
63}
64
65#[derive(Debug, Clone)]
67pub struct SubProjectContext {
68 pub name: String,
70
71 pub content: SubProjectContent,
73
74 pub task_summary: TaskSummary,
76
77 pub derived_status: DerivedStatus,
79
80 pub last_updated: DateTime<Utc>,
82}
83
84#[derive(Debug, Clone)]
86pub struct SubProjectContent {
87 pub project_brief: Option<MarkdownContent>,
89
90 pub product_context: Option<MarkdownContent>,
92
93 pub active_context: Option<MarkdownContent>,
95
96 pub system_patterns: Option<MarkdownContent>,
98
99 pub tech_context: Option<MarkdownContent>,
101
102 pub progress: Option<MarkdownContent>,
104}
105
106#[derive(Debug, Clone)]
108pub struct TaskSummary {
109 pub total_tasks: usize,
111
112 pub tasks_by_status: HashMap<TaskStatus, Vec<TaskItem>>,
114
115 pub recent_tasks: Vec<TaskItem>,
117
118 pub completion_percentage: f64,
120
121 pub blocked_tasks: Vec<TaskItem>,
123
124 pub next_tasks: Vec<TaskItem>,
126}
127
128#[derive(Debug, Clone)]
130pub struct DerivedStatus {
131 pub health: ProjectHealth,
133
134 pub current_phase: String,
136
137 pub progress_indicators: Vec<ProgressIndicator>,
139
140 pub issues: Vec<Issue>,
142
143 pub recommendations: Vec<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
149pub enum ProjectHealth {
150 Critical,
152 Warning,
154 Healthy,
156 Unknown,
158}
159
160#[derive(Debug, Clone)]
162pub struct ProgressIndicator {
163 pub source: String,
165
166 pub metric_type: ProgressMetricType,
168
169 pub current_value: f64,
171
172 pub target_value: Option<f64>,
174
175 pub description: String,
177}
178
179#[derive(Debug, Clone)]
181pub enum ProgressMetricType {
182 TaskCompletion,
184 MilestoneProgress,
186 FeatureProgress,
188 DocumentationProgress,
190 Custom(String),
192}
193
194#[derive(Debug, Clone)]
196pub struct Issue {
197 pub severity: IssueSeverity,
199
200 pub description: String,
202
203 pub source: String,
205
206 pub resolution: Option<String>,
208}
209
210#[derive(Debug, Clone, PartialEq, PartialOrd)]
212pub enum IssueSeverity {
213 Low,
215 Medium,
217 High,
219 Critical,
221}
222
223impl Default for ContextCorrelator {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl ContextCorrelator {
230 pub fn new() -> Self {
241 Self {
242 workspace_context: None,
243 }
244 }
245
246 pub fn discover_and_correlate(&mut self, root_path: &Path) -> FsResult<&WorkspaceContext> {
270 let structure = MemoryBankNavigator::discover_structure(root_path)?;
272
273 let current_context = self.parse_current_context(&structure)?;
275
276 let workspace_content = self.parse_workspace_content(&structure)?;
278
279 let sub_project_contexts = self.parse_sub_project_contexts(&structure)?;
281
282 let workspace_context = WorkspaceContext {
284 structure,
285 current_context,
286 workspace_content,
287 sub_project_contexts,
288 last_updated: Utc::now(),
289 };
290
291 self.workspace_context = Some(workspace_context);
292 Ok(self.workspace_context.as_ref().unwrap())
293 }
294
295 pub fn get_workspace_context(&self) -> Option<&WorkspaceContext> {
303 self.workspace_context.as_ref()
304 }
305
306 pub fn switch_context(&mut self, sub_project_name: &str, switched_by: &str) -> FsResult<()> {
319 let workspace_context = self.workspace_context.as_mut().ok_or_else(|| {
320 FsError::ParseError {
321 message: "Workspace context not initialized. Call discover_and_correlate first.".to_string(),
322 suggestion: "Make sure you're in a directory with a memory bank structure.".to_string(),
323 }
324 })?;
325
326 if !workspace_context
328 .sub_project_contexts
329 .contains_key(sub_project_name)
330 {
331 let available_projects: Vec<String> = workspace_context.sub_project_contexts.keys().cloned().collect();
332 return Err(FsError::ParseError {
333 message: format!("Sub-project '{sub_project_name}' not found. Available projects: {}", available_projects.join(", ")),
334 suggestion: "Check available projects with 'airs-memspec status' or verify the project name.".to_string(),
335 });
336 }
337
338 workspace_context.current_context = CurrentContext {
340 active_sub_project: sub_project_name.to_string(),
341 switched_on: Utc::now(),
342 switched_by: switched_by.to_string(),
343 status: format!("switched_to_{sub_project_name}"),
344 metadata: HashMap::new(),
345 };
346
347 let (structure, current_context) = {
349 let ws_ctx = workspace_context;
350 (ws_ctx.structure.clone(), ws_ctx.current_context.clone())
351 };
352
353 self.update_current_context_file(&structure, ¤t_context)?;
354
355 if let Some(workspace_context) = &mut self.workspace_context {
357 workspace_context.last_updated = Utc::now();
358 }
359
360 Ok(())
361 }
362
363 pub fn get_task_summary(&self, sub_project: Option<&str>) -> Option<TaskSummary> {
374 let workspace_context = self.workspace_context.as_ref()?;
375
376 match sub_project {
377 Some(name) => workspace_context
378 .sub_project_contexts
379 .get(name)
380 .map(|ctx| ctx.task_summary.clone()),
381 None => {
382 Some(self.aggregate_workspace_tasks(workspace_context))
384 }
385 }
386 }
387
388 fn parse_current_context(&self, structure: &MemoryBankStructure) -> FsResult<CurrentContext> {
390 if let Some(context_path) = &structure.current_context {
391 let content = MarkdownParser::parse_file(context_path)?;
392
393 let active_sub_project = content
395 .metadata
396 .title
397 .or_else(|| {
398 content
399 .sections
400 .keys()
401 .find(|k| k.contains("active"))
402 .cloned()
403 })
404 .unwrap_or_else(|| "unknown".to_string());
405
406 Ok(CurrentContext {
407 active_sub_project,
408 switched_on: Utc::now(),
409 switched_by: "system".to_string(),
410 status: "initialized".to_string(),
411 metadata: HashMap::new(),
412 })
413 } else {
414 Ok(CurrentContext {
416 active_sub_project: "unknown".to_string(),
417 switched_on: Utc::now(),
418 switched_by: "system".to_string(),
419 status: "no_context_file".to_string(),
420 metadata: HashMap::new(),
421 })
422 }
423 }
424
425 fn parse_workspace_content(
427 &self,
428 structure: &MemoryBankStructure,
429 ) -> FsResult<WorkspaceContent> {
430 let project_brief = if let Some(path) = &structure.workspace.project_brief {
431 Some(MarkdownParser::parse_file(path)?)
432 } else {
433 None
434 };
435
436 let shared_patterns = if let Some(path) = &structure.workspace.shared_patterns {
437 Some(MarkdownParser::parse_file(path)?)
438 } else {
439 None
440 };
441
442 let workspace_architecture = if let Some(path) = &structure.workspace.workspace_architecture
443 {
444 Some(MarkdownParser::parse_file(path)?)
445 } else {
446 None
447 };
448
449 let workspace_progress = if let Some(path) = &structure.workspace.workspace_progress {
450 Some(MarkdownParser::parse_file(path)?)
451 } else {
452 None
453 };
454
455 Ok(WorkspaceContent {
456 project_brief,
457 shared_patterns,
458 workspace_architecture,
459 workspace_progress,
460 })
461 }
462
463 fn parse_sub_project_contexts(
465 &self,
466 structure: &MemoryBankStructure,
467 ) -> FsResult<HashMap<String, SubProjectContext>> {
468 let mut contexts = HashMap::new();
469
470 for (name, sub_project_files) in &structure.sub_projects {
471 let context = self.parse_single_sub_project_context(name, sub_project_files)?;
472 contexts.insert(name.clone(), context);
473 }
474
475 Ok(contexts)
476 }
477
478 fn parse_single_sub_project_context(
480 &self,
481 name: &str,
482 files: &crate::parser::navigation::SubProjectFiles,
483 ) -> FsResult<SubProjectContext> {
484 let project_brief = if let Some(path) = &files.project_brief {
486 Some(MarkdownParser::parse_file(path)?)
487 } else {
488 None
489 };
490
491 let product_context = if let Some(path) = &files.product_context {
492 Some(MarkdownParser::parse_file(path)?)
493 } else {
494 None
495 };
496
497 let active_context = if let Some(path) = &files.active_context {
498 Some(MarkdownParser::parse_file(path)?)
499 } else {
500 None
501 };
502
503 let system_patterns = if let Some(path) = &files.system_patterns {
504 Some(MarkdownParser::parse_file(path)?)
505 } else {
506 None
507 };
508
509 let tech_context = if let Some(path) = &files.tech_context {
510 Some(MarkdownParser::parse_file(path)?)
511 } else {
512 None
513 };
514
515 let progress = if let Some(path) = &files.progress {
516 Some(MarkdownParser::parse_file(path)?)
517 } else {
518 None
519 };
520
521 let content = SubProjectContent {
522 project_brief,
523 product_context,
524 active_context,
525 system_patterns,
526 tech_context,
527 progress,
528 };
529
530 let task_summary = self.parse_task_summary(files)?;
532
533 let derived_status = self.derive_project_status(&content, &task_summary);
535
536 Ok(SubProjectContext {
537 name: name.to_string(),
538 content,
539 task_summary,
540 derived_status,
541 last_updated: Utc::now(),
542 })
543 }
544
545 fn parse_task_summary(
547 &self,
548 files: &crate::parser::navigation::SubProjectFiles,
549 ) -> FsResult<TaskSummary> {
550 let mut all_tasks = Vec::new();
551
552 for task_file in &files.task_files {
554 let content = MarkdownParser::parse_file(task_file)?;
555 all_tasks.extend(content.tasks);
556 }
557
558 let total_tasks = all_tasks.len();
560 let mut tasks_by_status = HashMap::new();
561
562 for task in &all_tasks {
563 tasks_by_status
564 .entry(task.status.clone())
565 .or_insert_with(Vec::new)
566 .push(task.clone());
567 }
568
569 let completed_count = tasks_by_status
570 .get(&TaskStatus::Completed)
571 .map(|tasks| tasks.len())
572 .unwrap_or(0);
573
574 let completion_percentage = if total_tasks > 0 {
575 (completed_count as f64 / total_tasks as f64) * 100.0
576 } else {
577 0.0
578 };
579
580 let blocked_tasks = tasks_by_status
581 .get(&TaskStatus::Blocked)
582 .cloned()
583 .unwrap_or_default();
584
585 let next_tasks = tasks_by_status
586 .get(&TaskStatus::NotStarted)
587 .cloned()
588 .unwrap_or_default();
589
590 let mut recent_tasks = all_tasks.clone();
592 recent_tasks.sort_by(|a, b| b.updated.cmp(&a.updated));
593 recent_tasks.truncate(5); Ok(TaskSummary {
596 total_tasks,
597 tasks_by_status,
598 recent_tasks,
599 completion_percentage,
600 blocked_tasks,
601 next_tasks,
602 })
603 }
604
605 fn derive_project_status(
607 &self,
608 content: &SubProjectContent,
609 task_summary: &TaskSummary,
610 ) -> DerivedStatus {
611 let health = if task_summary.completion_percentage > 80.0 {
612 ProjectHealth::Healthy
613 } else if task_summary.completion_percentage > 50.0 {
614 ProjectHealth::Warning
615 } else if !task_summary.blocked_tasks.is_empty() {
616 ProjectHealth::Critical
617 } else {
618 ProjectHealth::Unknown
619 };
620
621 let current_phase = content
622 .active_context
623 .as_ref()
624 .and_then(|ctx| ctx.metadata.description.clone())
625 .unwrap_or_else(|| "unknown".to_string());
626
627 let progress_indicators = vec![ProgressIndicator {
628 source: "task_summary".to_string(),
629 metric_type: ProgressMetricType::TaskCompletion,
630 current_value: task_summary.completion_percentage,
631 target_value: Some(100.0),
632 description: format!(
633 "Task completion: {:.1}%",
634 task_summary.completion_percentage
635 ),
636 }];
637
638 let issues = task_summary
639 .blocked_tasks
640 .iter()
641 .map(|task| Issue {
642 severity: IssueSeverity::High,
643 description: format!("Blocked task: {}", task.title),
644 source: "task_analysis".to_string(),
645 resolution: task.details.clone(),
646 })
647 .collect();
648
649 let recommendations = if task_summary.completion_percentage < 50.0 {
650 vec!["Focus on completing pending tasks".to_string()]
651 } else if !task_summary.blocked_tasks.is_empty() {
652 vec!["Address blocked tasks to maintain progress".to_string()]
653 } else {
654 vec!["Continue current development pace".to_string()]
655 };
656
657 DerivedStatus {
658 health,
659 current_phase,
660 progress_indicators,
661 issues,
662 recommendations,
663 }
664 }
665
666 fn update_current_context_file(
668 &self,
669 _structure: &MemoryBankStructure,
670 _context: &CurrentContext,
671 ) -> FsResult<()> {
672 Ok(())
675 }
676
677 fn aggregate_workspace_tasks(&self, workspace_context: &WorkspaceContext) -> TaskSummary {
679 let mut total_tasks = 0;
680 let mut all_tasks_by_status: HashMap<TaskStatus, Vec<TaskItem>> = HashMap::new();
681 let mut all_recent_tasks = Vec::new();
682 let mut all_blocked_tasks = Vec::new();
683 let mut all_next_tasks = Vec::new();
684
685 for sub_project_context in workspace_context.sub_project_contexts.values() {
686 let summary = &sub_project_context.task_summary;
687 total_tasks += summary.total_tasks;
688
689 for (status, tasks) in &summary.tasks_by_status {
691 all_tasks_by_status
692 .entry(status.clone())
693 .or_default()
694 .extend(tasks.clone());
695 }
696
697 all_recent_tasks.extend(summary.recent_tasks.clone());
698 all_blocked_tasks.extend(summary.blocked_tasks.clone());
699 all_next_tasks.extend(summary.next_tasks.clone());
700 }
701
702 let completed_count = all_tasks_by_status
703 .get(&TaskStatus::Completed)
704 .map(|tasks| tasks.len())
705 .unwrap_or(0);
706
707 let completion_percentage = if total_tasks > 0 {
708 (completed_count as f64 / total_tasks as f64) * 100.0
709 } else {
710 0.0
711 };
712
713 all_recent_tasks.sort_by(|a, b| b.updated.cmp(&a.updated));
715 all_recent_tasks.truncate(10);
716
717 TaskSummary {
718 total_tasks,
719 tasks_by_status: all_tasks_by_status,
720 recent_tasks: all_recent_tasks,
721 completion_percentage,
722 blocked_tasks: all_blocked_tasks,
723 next_tasks: all_next_tasks,
724 }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 #[test]
732 fn test_context_correlator_creation() {
733 let correlator = ContextCorrelator::new();
734
735 assert!(correlator.get_workspace_context().is_none());
736 }
737
738 #[test]
739 fn test_project_health_ordering() {
740 assert!(ProjectHealth::Critical < ProjectHealth::Warning);
741 assert!(ProjectHealth::Warning < ProjectHealth::Healthy);
742 assert!(ProjectHealth::Unknown == ProjectHealth::Unknown);
743 }
744
745 #[test]
746 fn test_issue_severity_ordering() {
747 assert!(IssueSeverity::Low < IssueSeverity::Medium);
748 assert!(IssueSeverity::Medium < IssueSeverity::High);
749 assert!(IssueSeverity::High < IssueSeverity::Critical);
750 }
751}