Skip to main content

agentic_planning/
query_engine.rs

1use crate::error::{Error, Result};
2use crate::types::*;
3use crate::PlanningEngine;
4use std::collections::HashMap;
5
6impl PlanningEngine {
7    pub fn get_goal(&self, id: GoalId) -> Option<&Goal> {
8        self.goal_store.get(&id)
9    }
10
11    pub fn list_goals(&self, filter: GoalFilter) -> Vec<&Goal> {
12        let mut goals: Vec<&Goal> = self
13            .goal_store
14            .values()
15            .filter(|g| self.matches_goal_filter(g, &filter))
16            .collect();
17
18        goals.sort_by_key(|g| g.created_at);
19
20        if let Some(limit) = filter.limit {
21            goals.truncate(limit);
22        }
23
24        goals
25    }
26
27    pub fn get_root_goals(&self) -> Vec<&Goal> {
28        self.indexes
29            .root_goals
30            .iter()
31            .filter_map(|id| self.goal_store.get(id))
32            .collect()
33    }
34
35    pub fn get_active_goals(&self) -> Vec<&Goal> {
36        self.indexes
37            .active_goals
38            .iter()
39            .filter_map(|id| self.goal_store.get(id))
40            .collect()
41    }
42
43    pub fn get_blocked_goals(&self) -> Vec<&Goal> {
44        self.indexes
45            .blocked_goals
46            .iter()
47            .filter_map(|id| self.goal_store.get(id))
48            .collect()
49    }
50
51    pub fn get_urgent_goals(&self, within_days: f64) -> Vec<&Goal> {
52        let now = Timestamp::now();
53        let cutoff = Timestamp(now.0 + (within_days * 86_400.0 * 1e9) as i64);
54
55        self.indexes
56            .goals_by_deadline
57            .iter()
58            .filter(|(deadline, _)| **deadline <= cutoff)
59            .flat_map(|(_, ids)| ids.iter())
60            .filter_map(|id| self.goal_store.get(id))
61            .filter(|g| g.status == GoalStatus::Active || g.status == GoalStatus::Blocked)
62            .collect()
63    }
64
65    pub fn get_goal_tree(&self, root_id: GoalId) -> Option<GoalTree> {
66        self.goal_store.get(&root_id)?;
67        let mut tree = GoalTree {
68            root: root_id,
69            nodes: HashMap::new(),
70            edges: Vec::new(),
71        };
72        self.build_tree_recursive(root_id, 0, &mut tree);
73        Some(tree)
74    }
75
76    fn build_tree_recursive(&self, id: GoalId, depth: usize, tree: &mut GoalTree) {
77        if let Some(goal) = self.goal_store.get(&id) {
78            tree.nodes.insert(
79                id,
80                GoalTreeNode {
81                    goal: goal.clone(),
82                    depth,
83                },
84            );
85            for child_id in &goal.children {
86                tree.edges.push((id, *child_id));
87                self.build_tree_recursive(*child_id, depth + 1, tree);
88            }
89        }
90    }
91
92    pub fn search_goals(&self, query: &str) -> Vec<&Goal> {
93        let q = query.to_lowercase();
94        self.goal_store
95            .values()
96            .filter(|g| {
97                g.title.to_lowercase().contains(&q)
98                    || g.description.to_lowercase().contains(&q)
99                    || g.soul.intention.to_lowercase().contains(&q)
100                    || g.tags.iter().any(|t| t.to_lowercase().contains(&q))
101            })
102            .collect()
103    }
104
105    pub fn get_intention_singularity(&self) -> IntentionSingularity {
106        let active_goals: Vec<_> = self.get_active_goals().into_iter().cloned().collect();
107
108        if active_goals.is_empty() {
109            return IntentionSingularity::default();
110        }
111
112        let center = self.calculate_intention_center(&active_goals);
113
114        let positions = active_goals
115            .iter()
116            .map(|g| {
117                (
118                    g.id,
119                    IntentionPosition {
120                        goal_id: g.id,
121                        centrality: self.calculate_centrality(g, &center),
122                        alignment_angle: self.calculate_alignment(g, &center),
123                        gravitational_pull: g.physics.gravity,
124                        drift_risk: g.feelings.neglect,
125                    },
126                )
127            })
128            .collect();
129
130        IntentionSingularity {
131            unified_vision: self.synthesize_vision(&active_goals),
132            goal_positions: positions,
133            themes: self.extract_themes(&active_goals),
134            tension_lines: self.find_tensions(&active_goals),
135            golden_path: self.calculate_optimal_path(&active_goals),
136            center,
137        }
138    }
139
140    pub fn get_decision(&self, id: DecisionId) -> Option<&Decision> {
141        self.decision_store.get(&id)
142    }
143
144    pub fn get_decision_chain(&self, id: DecisionId) -> Option<DecisionChain> {
145        self.decision_store.get(&id)?;
146
147        let mut root = id;
148        while let Some(parent_id) = self.decision_store.get(&root).and_then(|d| d.caused_by) {
149            root = parent_id;
150        }
151
152        let mut chain = DecisionChain {
153            root,
154            descendants: Vec::new(),
155            causality: Vec::new(),
156            cascade_analysis: CascadeAnalysis::default(),
157        };
158
159        self.build_chain_recursive(root, &mut chain);
160        chain.cascade_analysis.total_nodes = chain.descendants.len() + 1;
161        Some(chain)
162    }
163
164    fn build_chain_recursive(&self, id: DecisionId, chain: &mut DecisionChain) {
165        if let Some(decision) = self.decision_store.get(&id) {
166            for child_id in &decision.causes {
167                if let Some(child) = self.decision_store.get(child_id) {
168                    // Determine causality type from relationship characteristics
169                    let causality_type = self.infer_causality_type(decision, child);
170                    let strength = self.infer_causality_strength(decision, child);
171
172                    chain.descendants.push(*child_id);
173                    chain.causality.push(CausalLink {
174                        from: id,
175                        to: *child_id,
176                        causality_type,
177                        strength,
178                    });
179                } else {
180                    // Child not found — still record with default
181                    chain.descendants.push(*child_id);
182                    chain.causality.push(CausalLink {
183                        from: id,
184                        to: *child_id,
185                        causality_type: CausalityType::Enables,
186                        strength: 0.5,
187                    });
188                }
189                self.build_chain_recursive(*child_id, chain);
190            }
191        }
192    }
193
194    fn infer_causality_type(&self, parent: &Decision, child: &Decision) -> CausalityType {
195        // If parent and child affect overlapping goals, they have a stronger causal link
196        let shared_goals = parent
197            .affected_goals
198            .iter()
199            .filter(|g| child.affected_goals.contains(g))
200            .count();
201
202        // Check if child's consequences constrain parent's path
203        let has_negative_consequences = child
204            .consequences
205            .iter()
206            .any(|c| matches!(c.impact, Impact::Negative));
207
208        // If the parent was caused_by the child, it's a direct requirement
209        if parent.caused_by == Some(child.id) {
210            return CausalityType::Requires;
211        }
212
213        // If child has negative consequences that overlap with parent's goals, it constrains
214        if has_negative_consequences && shared_goals > 0 {
215            return CausalityType::Constrains;
216        }
217
218        // If child is crystallized and parent isn't, child suggests the parent's direction
219        if child.status == DecisionStatus::Crystallized
220            && parent.status != DecisionStatus::Crystallized
221        {
222            return CausalityType::Suggests;
223        }
224
225        // Strong shared goal overlap implies direct enablement
226        if shared_goals >= 2 {
227            return CausalityType::Requires;
228        }
229
230        CausalityType::Enables
231    }
232
233    fn infer_causality_strength(&self, parent: &Decision, child: &Decision) -> f64 {
234        let mut strength = 0.5;
235
236        // Shared affected goals increase strength
237        let shared = parent
238            .affected_goals
239            .iter()
240            .filter(|g| child.affected_goals.contains(g))
241            .count();
242        strength += shared as f64 * 0.15;
243
244        // Crystallized decisions have stronger links
245        if child.status == DecisionStatus::Crystallized {
246            strength += 0.2;
247        }
248
249        // High confidence reasoning increases link strength
250        strength += child.reasoning.confidence * 0.1;
251
252        strength.clamp(0.1, 1.0)
253    }
254
255    pub fn get_shadows(&self, id: DecisionId) -> Vec<&CrystalShadow> {
256        self.decision_store
257            .get(&id)
258            .map(|d| d.shadows.iter().collect())
259            .unwrap_or_default()
260    }
261
262    pub fn project_counterfactual(
263        &self,
264        decision_id: DecisionId,
265        path_id: PathId,
266    ) -> Option<CounterfactualProjection> {
267        let decision = self.decision_store.get(&decision_id)?;
268        let shadow = decision.shadows.iter().find(|s| s.path.id == path_id)?;
269
270        Some(CounterfactualProjection {
271            projected_at: Timestamp::now(),
272            timeline: self.generate_projected_timeline(decision, &shadow.path),
273            final_state: self.project_final_state(decision, &shadow.path),
274            confidence: self.calculate_projection_confidence(decision),
275        })
276    }
277
278    pub fn decision_archaeology(&self, artifact: &str) -> DecisionArchaeology {
279        let mut relevant: Vec<_> = self
280            .decision_store
281            .values()
282            .filter(|d| {
283                d.question.question.contains(artifact) || d.question.context.contains(artifact)
284            })
285            .collect();
286
287        relevant.sort_by_key(|d| d.crystallized_at.unwrap_or(Timestamp(0)));
288
289        let strata: Vec<ArchaeologicalStratum> = relevant
290            .iter()
291            .enumerate()
292            .map(|(i, d)| {
293                // Assess reasonableness from consequence ratio
294                let positive = d
295                    .consequences
296                    .iter()
297                    .filter(|c| matches!(c.impact, Impact::Positive))
298                    .count();
299                let negative = d
300                    .consequences
301                    .iter()
302                    .filter(|c| matches!(c.impact, Impact::Negative))
303                    .count();
304                let total = d.consequences.len().max(1);
305                let was_reasonable = if d.consequences.is_empty() {
306                    // No consequences recorded yet — give benefit of the doubt
307                    d.reasoning.confidence >= 0.4
308                } else {
309                    positive >= negative
310                };
311
312                let modern_assessment = if d.consequences.is_empty() {
313                    format!(
314                        "Pending assessment (confidence: {:.0}%)",
315                        d.reasoning.confidence * 100.0
316                    )
317                } else if was_reasonable {
318                    format!("Reasonable: {}/{} positive outcomes", positive, total)
319                } else {
320                    format!("Questionable: {}/{} negative outcomes", negative, total)
321                };
322
323                ArchaeologicalStratum {
324                    depth: relevant.len() - i,
325                    decision: d.id,
326                    age: self.calculate_age(d),
327                    impact_on_artifact: format!("{:?}", d.chosen.as_ref().map(|c| &c.name)),
328                    context_at_time: d.question.context.clone(),
329                    was_reasonable,
330                    modern_assessment,
331                }
332            })
333            .collect();
334
335        // Generate insights from patterns across the strata
336        let mut insights = Vec::new();
337        let unreasonable_count = strata.iter().filter(|s| !s.was_reasonable).count();
338        if unreasonable_count > 0 {
339            insights.push(format!(
340                "{} of {} decisions about '{}' had questionable outcomes",
341                unreasonable_count,
342                strata.len(),
343                artifact
344            ));
345        }
346        if strata.len() >= 3 {
347            insights.push(format!(
348                "'{}' has been a recurring decision point ({} times) — consider a standing policy",
349                artifact,
350                strata.len()
351            ));
352        }
353        if relevant.iter().any(|d| {
354            d.status == DecisionStatus::Regretted || d.status == DecisionStatus::Recrystallized
355        }) {
356            insights.push(format!(
357                "At least one decision about '{}' was regretted or recrystallized — pattern may be unstable",
358                artifact
359            ));
360        }
361
362        DecisionArchaeology {
363            artifact: artifact.to_string(),
364            strata,
365            cumulative_impact: self.calculate_cumulative_impact(&relevant),
366            insights,
367        }
368    }
369
370    pub fn get_commitment(&self, id: CommitmentId) -> Option<&Commitment> {
371        self.commitment_store.get(&id)
372    }
373
374    pub fn get_dream(&self, id: DreamId) -> Option<&Dream> {
375        self.dream_store.get(&id)
376    }
377
378    pub fn list_dreams(&self) -> Vec<&Dream> {
379        self.dream_store.values().collect()
380    }
381
382    pub fn list_goal_dreams(&self, goal_id: GoalId) -> Vec<&Dream> {
383        self.dream_store
384            .values()
385            .filter(|d| d.goal_id == goal_id)
386            .collect()
387    }
388
389    pub fn list_decisions(&self) -> Vec<&Decision> {
390        self.decision_store.values().collect()
391    }
392
393    pub fn list_commitments(&self) -> Vec<&Commitment> {
394        self.commitment_store.values().collect()
395    }
396
397    pub fn get_due_soon(&self, within_days: f64) -> Vec<&Commitment> {
398        let now = Timestamp::now();
399        let cutoff = Timestamp(now.0 + (within_days * 86_400.0 * 1e9) as i64);
400
401        self.indexes
402            .commitments_by_due
403            .iter()
404            .filter(|(due, _)| **due <= cutoff)
405            .flat_map(|(_, ids)| ids.iter())
406            .filter_map(|id| self.commitment_store.get(id))
407            .filter(|c| c.status == CommitmentStatus::Active)
408            .collect()
409    }
410
411    pub fn get_commitment_inventory(&self) -> CommitmentInventory {
412        let commitments: Vec<_> = self.commitment_store.values().collect();
413        let total_weight: f64 = commitments
414            .iter()
415            .filter(|c| c.status == CommitmentStatus::Active)
416            .map(|c| c.weight)
417            .sum();
418
419        CommitmentInventory {
420            total_count: commitments.len(),
421            active_count: commitments
422                .iter()
423                .filter(|c| c.status == CommitmentStatus::Active)
424                .count(),
425            total_weight,
426            sustainable_weight: 2.0,
427            is_overloaded: total_weight > 2.0,
428            by_stakeholder: self.group_by_stakeholder(&commitments),
429        }
430    }
431
432    pub fn get_at_risk_commitments(&self) -> Vec<&Commitment> {
433        self.commitment_store
434            .values()
435            .filter(|c| c.status == CommitmentStatus::Active && self.is_at_risk(c))
436            .collect()
437    }
438
439    pub fn get_federation(&self, id: FederationId) -> Option<&Federation> {
440        self.federation_store.get(&id)
441    }
442
443    pub fn list_federations(&self) -> Vec<&Federation> {
444        self.federation_store.values().collect()
445    }
446
447    pub fn get_federation_members(&self, id: FederationId) -> Option<Vec<FederationMember>> {
448        self.federation_store.get(&id).map(|f| f.members.clone())
449    }
450
451    pub fn scan_blocker_prophecy(&self) -> Vec<BlockerProphecy> {
452        let mut prophecies = Vec::new();
453
454        // Count blocker type frequency across all goals for confidence calibration
455        let mut blocker_type_counts: HashMap<String, usize> = HashMap::new();
456        for goal in self.goal_store.values() {
457            for blocker in &goal.blockers {
458                let key = format!("{:?}", std::mem::discriminant(&blocker.blocker_type));
459                *blocker_type_counts.entry(key).or_insert(0) += 1;
460            }
461        }
462        let total_historical = blocker_type_counts.values().sum::<usize>().max(1);
463
464        for goal in self.get_active_goals() {
465            for blocker in self.predict_blockers(goal) {
466                // Confidence from: blocker type recurrence + goal severity signals
467                let type_key = format!("{:?}", std::mem::discriminant(&blocker.blocker_type));
468                let type_frequency = *blocker_type_counts.get(&type_key).unwrap_or(&0) as f64
469                    / total_historical as f64;
470                let severity_signal = blocker.severity;
471                let prediction_confidence =
472                    (0.3 + type_frequency * 0.3 + severity_signal * 0.3).clamp(0.1, 0.95);
473
474                // Days until materialization from deadline proximity and severity
475                let days_until = goal
476                    .deadline
477                    .map(|d| {
478                        let days = ((d.0 - Timestamp::now().0) as f64 / (86_400.0 * 1e9)).max(0.5);
479                        (days * (1.0 - severity_signal)).max(1.0)
480                    })
481                    .unwrap_or(14.0 * (1.0 - severity_signal * 0.5));
482
483                // Build evidence from goal state
484                let mut evidence = Vec::new();
485                if goal.progress.velocity == 0.0 {
486                    evidence.push("zero progress velocity".to_string());
487                }
488                if goal.feelings.neglect > 0.5 {
489                    evidence.push(format!("high neglect score ({:.2})", goal.feelings.neglect));
490                }
491                if !goal.dependencies.is_empty() {
492                    let incomplete_deps = goal
493                        .dependencies
494                        .iter()
495                        .filter(|d| {
496                            self.goal_store
497                                .get(d)
498                                .map(|g| g.status != GoalStatus::Completed)
499                                .unwrap_or(true)
500                        })
501                        .count();
502                    if incomplete_deps > 0 {
503                        evidence.push(format!("{} incomplete dependencies", incomplete_deps));
504                    }
505                }
506
507                // Recommended actions based on blocker type
508                let recommended_actions = match &blocker.blocker_type {
509                    BlockerType::DependencyBlocked { goal: dep_id } => {
510                        let dep_name = self
511                            .goal_store
512                            .get(dep_id)
513                            .map(|g| g.title.clone())
514                            .unwrap_or_else(|| format!("{:?}", dep_id));
515                        vec![format!("Prioritize completing '{}'", dep_name)]
516                    }
517                    BlockerType::ResourceUnavailable { resource } => {
518                        vec![format!("Secure resource: {}", resource)]
519                    }
520                    BlockerType::ExternalEvent { event } => {
521                        vec![format!("Monitor and prepare for: {}", event)]
522                    }
523                    BlockerType::SkillGap { skill } => {
524                        vec![format!("Acquire skill or delegate: {}", skill)]
525                    }
526                    BlockerType::ApprovalPending { .. } => {
527                        vec!["Follow up on approval request".to_string()]
528                    }
529                    BlockerType::TechnicalDebt { description } => {
530                        vec![format!("Address technical debt: {}", description)]
531                    }
532                    BlockerType::DeadlineMiss { .. } => {
533                        vec!["Renegotiate deadline or increase resources".to_string()]
534                    }
535                    BlockerType::Unknown { signals } => {
536                        if signals.is_empty() {
537                            vec!["Investigate root cause".to_string()]
538                        } else {
539                            vec![format!("Investigate signals: {}", signals.join(", "))]
540                        }
541                    }
542                };
543
544                prophecies.push(BlockerProphecy {
545                    goal_id: goal.id,
546                    predicted_blocker: blocker,
547                    prediction_confidence,
548                    days_until_materialization: days_until,
549                    evidence,
550                    recommended_actions,
551                });
552            }
553        }
554        prophecies
555    }
556
557    pub fn listen_progress_echoes(&self) -> Vec<ProgressEcho> {
558        let mut echoes = Vec::new();
559        for goal in self.get_active_goals() {
560            if goal.progress.percentage > 0.5 && goal.physics.momentum > 0.2 {
561                let eta_days = goal
562                    .progress
563                    .eta
564                    .map(|eta| ((eta.0 - Timestamp::now().0) as f64 / (86_400.0 * 1e9)).max(1.0))
565                    .unwrap_or(30.0);
566
567                if eta_days < 30.0 {
568                    echoes.push(ProgressEcho {
569                        goal_id: goal.id,
570                        source_milestone: Milestone {
571                            name: format!("{} completed", goal.title),
572                            description: "Goal completion".to_string(),
573                        },
574                        echo_strength: goal.physics.momentum,
575                        estimated_arrival_secs: (eta_days * 86_400.0) as u64,
576                        carried_information: self.extract_echo_info(goal),
577                        confidence: goal.feelings.confidence,
578                    });
579                }
580            }
581        }
582        echoes
583    }
584
585    pub fn get_decision_prophecy(
586        &self,
587        question: &str,
588        options: &[DecisionPath],
589    ) -> DecisionProphecy {
590        let paths = options
591            .iter()
592            .map(|option| ProphecyPath {
593                path: option.clone(),
594                timeline: self.project_path_timeline(option),
595                final_state: self.project_path_final_state(option),
596                risk_profile: self.assess_path_risk(option),
597                opportunity_profile: self.assess_path_opportunity(option),
598            })
599            .collect();
600
601        DecisionProphecy {
602            question: DecisionQuestion {
603                question: question.to_string(),
604                context: String::new(),
605                constraints: Vec::new(),
606                asked_at: Timestamp::now(),
607            },
608            paths,
609            confidence: 0.7,
610            sources: Vec::new(),
611            warnings: Vec::new(),
612        }
613    }
614
615    fn matches_goal_filter(&self, goal: &Goal, filter: &GoalFilter) -> bool {
616        if let Some(statuses) = &filter.status {
617            if !statuses.contains(&goal.status) {
618                return false;
619            }
620        }
621        if let Some(priorities) = &filter.priority {
622            if !priorities.contains(&goal.priority) {
623                return false;
624            }
625        }
626        if let Some(parent) = filter.parent {
627            if goal.parent != Some(parent) {
628                return false;
629            }
630        }
631        if let Some(has_deadline) = filter.has_deadline {
632            if goal.deadline.is_some() != has_deadline {
633                return false;
634            }
635        }
636        if let Some(before) = filter.deadline_before {
637            if goal.deadline.map(|d| d > before).unwrap_or(true) {
638                return false;
639            }
640        }
641        if let Some(after) = filter.deadline_after {
642            if goal.deadline.map(|d| d < after).unwrap_or(true) {
643                return false;
644            }
645        }
646        if let Some(tags) = &filter.tags {
647            if !tags.iter().all(|t| goal.tags.contains(t)) {
648                return false;
649            }
650        }
651        if let Some(created_after) = filter.created_after {
652            if goal.created_at < created_after {
653                return false;
654            }
655        }
656        if let Some(min) = filter.min_progress {
657            if goal.progress.percentage < min {
658                return false;
659            }
660        }
661        if let Some(max) = filter.max_progress {
662            if goal.progress.percentage > max {
663                return false;
664            }
665        }
666        if let Some(min_momentum) = filter.min_momentum {
667            if goal.physics.momentum < min_momentum {
668                return false;
669            }
670        }
671        true
672    }
673
674    fn calculate_intention_center(&self, goals: &[Goal]) -> IntentionCenter {
675        if goals.is_empty() {
676            return IntentionCenter {
677                urgency: 0.0,
678                confidence: 0.0,
679                momentum: 0.0,
680            };
681        }
682
683        let len = goals.len() as f64;
684        IntentionCenter {
685            urgency: goals.iter().map(|g| g.feelings.urgency).sum::<f64>() / len,
686            confidence: goals.iter().map(|g| g.feelings.confidence).sum::<f64>() / len,
687            momentum: goals.iter().map(|g| g.physics.momentum).sum::<f64>() / len,
688        }
689    }
690
691    fn calculate_centrality(&self, goal: &Goal, center: &IntentionCenter) -> f64 {
692        let d = (goal.feelings.urgency - center.urgency).abs()
693            + (goal.feelings.confidence - center.confidence).abs()
694            + (goal.physics.momentum - center.momentum).abs();
695        (1.0 - (d / 3.0)).clamp(0.0, 1.0)
696    }
697
698    fn calculate_alignment(&self, goal: &Goal, center: &IntentionCenter) -> f64 {
699        ((goal.physics.momentum + goal.feelings.confidence + goal.feelings.urgency)
700            - (center.momentum + center.confidence + center.urgency))
701            .atan()
702    }
703
704    fn synthesize_vision(&self, goals: &[Goal]) -> String {
705        if goals.is_empty() {
706            return "No active intentions".to_string();
707        }
708
709        let mut top = goals.to_vec();
710        top.sort_by(|a, b| {
711            let sa = a.physics.gravity + a.feelings.urgency;
712            let sb = b.physics.gravity + b.feelings.urgency;
713            sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
714        });
715
716        // Group by parent to detect clusters
717        let mut root_clusters: HashMap<Option<GoalId>, Vec<&Goal>> = HashMap::new();
718        for g in &top {
719            root_clusters.entry(g.parent).or_default().push(g);
720        }
721
722        // Identify dominant themes from tags
723        let themes = self.extract_themes(goals);
724        let theme_str = if themes.is_empty() {
725            String::new()
726        } else {
727            format!(
728                " Themes: {}.",
729                themes
730                    .iter()
731                    .take(3)
732                    .cloned()
733                    .collect::<Vec<_>>()
734                    .join(", ")
735            )
736        };
737
738        // Detect conflicts (goals with opposing momentum)
739        let stalled: Vec<_> = top
740            .iter()
741            .filter(|g| g.progress.velocity == 0.0 && g.feelings.urgency > 0.6)
742            .collect();
743        let conflict_str = if !stalled.is_empty() {
744            format!(" Warning: {} urgent goal(s) stalled.", stalled.len())
745        } else {
746            String::new()
747        };
748
749        // Build vision from top goals with cluster context
750        let primary: Vec<String> = top.iter().take(3).map(|g| g.title.clone()).collect();
751        let cluster_count = root_clusters.len();
752
753        if cluster_count == 1 {
754            format!(
755                "Unified focus: {}.{}{}",
756                primary.join(", "),
757                theme_str,
758                conflict_str
759            )
760        } else {
761            format!(
762                "Focus across {} streams: {}.{}{}",
763                cluster_count,
764                primary.join(", "),
765                theme_str,
766                conflict_str
767            )
768        }
769    }
770
771    fn extract_themes(&self, goals: &[Goal]) -> Vec<String> {
772        let mut counts: HashMap<String, usize> = HashMap::new();
773        for g in goals {
774            for t in &g.tags {
775                *counts.entry(t.to_lowercase()).or_insert(0) += 1;
776            }
777        }
778        let mut pairs: Vec<_> = counts.into_iter().collect();
779        pairs.sort_by(|a, b| b.1.cmp(&a.1));
780        pairs.into_iter().take(5).map(|(k, _)| k).collect()
781    }
782
783    fn find_tensions(&self, goals: &[Goal]) -> Vec<TensionLine> {
784        let mut tensions = Vec::new();
785        for i in 0..goals.len() {
786            for j in (i + 1)..goals.len() {
787                let a = &goals[i];
788                let b = &goals[j];
789
790                // 1. Urgency divergence
791                let urgency_delta = (a.feelings.urgency - b.feelings.urgency).abs();
792                if urgency_delta > 0.4 {
793                    tensions.push(TensionLine {
794                        a: a.id,
795                        b: b.id,
796                        magnitude: urgency_delta,
797                        reason: "urgency divergence".to_string(),
798                    });
799                }
800
801                // 2. Resource conflict: shared stakeholders via commitments
802                let a_stakeholders: Vec<_> = a
803                    .commitments
804                    .iter()
805                    .filter_map(|cid| self.commitment_store.get(cid))
806                    .map(|c| c.made_to.id)
807                    .collect();
808                let b_stakeholders: Vec<_> = b
809                    .commitments
810                    .iter()
811                    .filter_map(|cid| self.commitment_store.get(cid))
812                    .map(|c| c.made_to.id)
813                    .collect();
814                let shared_stakeholders = a_stakeholders
815                    .iter()
816                    .filter(|s| b_stakeholders.contains(s))
817                    .count();
818                if shared_stakeholders > 0 {
819                    tensions.push(TensionLine {
820                        a: a.id,
821                        b: b.id,
822                        magnitude: (shared_stakeholders as f64 * 0.3).min(1.0),
823                        reason: format!(
824                            "shared stakeholder conflict ({} shared)",
825                            shared_stakeholders
826                        ),
827                    });
828                }
829
830                // 3. Timeline conflict: overlapping deadlines with dependency relationship
831                if let (Some(da), Some(db)) = (a.deadline, b.deadline) {
832                    let days_apart = ((da.0 - db.0) as f64 / (86_400.0 * 1e9)).abs();
833                    if days_apart < 3.0
834                        && (a.dependencies.contains(&b.id) || b.dependencies.contains(&a.id))
835                    {
836                        tensions.push(TensionLine {
837                            a: a.id,
838                            b: b.id,
839                            magnitude: (1.0 - days_apart / 3.0).clamp(0.3, 1.0),
840                            reason: "deadline collision with dependency".to_string(),
841                        });
842                    }
843                }
844
845                // 4. Energy conflict: both high-gravity goals competing for momentum
846                if a.physics.gravity > 0.7 && b.physics.gravity > 0.7 {
847                    let combined_energy_demand = a.physics.gravity + b.physics.gravity;
848                    if combined_energy_demand > 1.5 {
849                        tensions.push(TensionLine {
850                            a: a.id,
851                            b: b.id,
852                            magnitude: (combined_energy_demand - 1.5).min(1.0),
853                            reason: "energy competition (both high gravity)".to_string(),
854                        });
855                    }
856                }
857            }
858        }
859        tensions
860    }
861
862    fn calculate_optimal_path(&self, goals: &[Goal]) -> Vec<GoalId> {
863        let mut ranked = goals.to_vec();
864        ranked.sort_by(|a, b| {
865            let sa = a.physics.gravity + a.feelings.urgency + a.physics.momentum;
866            let sb = b.physics.gravity + b.feelings.urgency + b.physics.momentum;
867            sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
868        });
869        ranked.into_iter().map(|g| g.id).collect()
870    }
871
872    fn calculate_age(&self, decision: &Decision) -> String {
873        let at = decision
874            .crystallized_at
875            .unwrap_or(decision.question.asked_at);
876        let days = ((Timestamp::now().0 - at.0) as f64 / (86_400.0 * 1e9)).max(0.0);
877        format!("{days:.1} days")
878    }
879
880    fn calculate_cumulative_impact(&self, decisions: &[&Decision]) -> String {
881        let crystallized = decisions
882            .iter()
883            .filter(|d| {
884                d.status == DecisionStatus::Crystallized
885                    || d.status == DecisionStatus::Recrystallized
886            })
887            .count();
888        format!("{} crystallized layers", crystallized)
889    }
890
891    fn group_by_stakeholder(&self, commitments: &[&Commitment]) -> HashMap<String, usize> {
892        let mut map = HashMap::new();
893        for c in commitments {
894            *map.entry(c.made_to.name.clone()).or_insert(0) += 1;
895        }
896        map
897    }
898
899    fn is_at_risk(&self, commitment: &Commitment) -> bool {
900        let Some(due) = commitment.due else {
901            return false;
902        };
903
904        let now = Timestamp::now();
905        let days_remaining = (due.0 - now.0) as f64 / (86_400.0 * 1e9);
906
907        if let Some(goal_id) = commitment.goal {
908            if let Some(goal) = self.goal_store.get(&goal_id) {
909                let remaining_work = 1.0 - goal.progress.percentage;
910                let days_needed = if goal.progress.velocity > 0.0 {
911                    remaining_work / goal.progress.velocity
912                } else {
913                    f64::INFINITY
914                };
915                return days_needed > days_remaining;
916            }
917        }
918
919        days_remaining < 7.0
920    }
921
922    pub(crate) fn predict_blockers(&self, goal: &Goal) -> Vec<Blocker> {
923        let mut blockers = Vec::new();
924
925        if goal.progress.velocity == 0.0 && goal.progress.percentage < 0.2 {
926            blockers.push(Blocker {
927                id: uuid::Uuid::new_v4(),
928                blocker_type: BlockerType::Unknown {
929                    signals: vec!["no progress velocity".to_string()],
930                },
931                description: "Execution stall likely".to_string(),
932                severity: 0.6,
933                identified_at: Timestamp::now(),
934                resolved_at: None,
935                resolution: None,
936            });
937        }
938
939        if goal.feelings.neglect > 0.6 {
940            blockers.push(Blocker {
941                id: uuid::Uuid::new_v4(),
942                blocker_type: BlockerType::ExternalEvent {
943                    event: "attention drift".to_string(),
944                },
945                description: "Attention drift may block completion".to_string(),
946                severity: 0.5,
947                identified_at: Timestamp::now(),
948                resolved_at: None,
949                resolution: None,
950            });
951        }
952
953        blockers
954    }
955
956    pub(crate) fn extract_echo_info(&self, goal: &Goal) -> Vec<String> {
957        vec![
958            format!("momentum:{:.2}", goal.physics.momentum),
959            format!("velocity:{:.3}", goal.progress.velocity),
960            format!("confidence:{:.2}", goal.feelings.confidence),
961        ]
962    }
963
964    fn generate_projected_timeline(
965        &self,
966        decision: &Decision,
967        path: &DecisionPath,
968    ) -> Vec<ProjectedEvent> {
969        let mut events = Vec::new();
970        let risk = path.estimated_risk.unwrap_or(0.5);
971        let effort = path.estimated_effort.unwrap_or(0.5);
972
973        // Compute aggregate momentum and velocity from affected goals
974        let (avg_momentum, avg_velocity) = if decision.affected_goals.is_empty() {
975            (0.5, 0.3)
976        } else {
977            let mut total_momentum = 0.0;
978            let mut total_velocity = 0.0;
979            let mut count = 0.0;
980            for gid in &decision.affected_goals {
981                if let Some(goal) = self.goal_store.get(gid) {
982                    total_momentum += goal.physics.momentum;
983                    total_velocity += goal.progress.velocity;
984                    count += 1.0;
985                }
986            }
987            if count > 0.0 {
988                (total_momentum / count, total_velocity / count)
989            } else {
990                (0.5, 0.3)
991            }
992        };
993
994        // Phase 1: Adoption/initiation — scaled by effort and momentum
995        let adoption_days = (7.0 * effort / avg_momentum.max(0.1)).clamp(2.0, 30.0);
996        let adoption_prob = (0.6 + avg_momentum * 0.3 - risk * 0.2).clamp(0.2, 0.95);
997        events.push(ProjectedEvent {
998            time_offset_days: adoption_days,
999            event: format!("{} adoption begins", path.name),
1000            probability: adoption_prob,
1001            impact: if adoption_prob > 0.7 {
1002                "smooth start expected".to_string()
1003            } else {
1004                "slow start likely".to_string()
1005            },
1006        });
1007
1008        // Phase 2: Progress milestone — 50% mark
1009        let progress_days =
1010            (adoption_days * 2.5 / avg_velocity.max(0.05)).clamp(adoption_days + 3.0, 90.0);
1011        let progress_prob = (adoption_prob * 0.85 - risk * 0.1).clamp(0.15, 0.9);
1012        events.push(ProjectedEvent {
1013            time_offset_days: progress_days,
1014            event: format!("{} reaches midpoint", path.name),
1015            probability: progress_prob,
1016            impact: "execution cadence established".to_string(),
1017        });
1018
1019        // Phase 3: Stabilization / completion
1020        let stable_days = (progress_days * 1.8).clamp(progress_days + 5.0, 180.0);
1021        let stable_prob = (progress_prob * 0.8).clamp(0.1, 0.85);
1022        events.push(ProjectedEvent {
1023            time_offset_days: stable_days,
1024            event: format!("{} stabilizes", path.name),
1025            probability: stable_prob,
1026            impact: if risk > 0.6 {
1027                "high-risk delivery, monitor closely".to_string()
1028            } else {
1029                "delivery quality on track".to_string()
1030            },
1031        });
1032
1033        events
1034    }
1035
1036    fn project_final_state(&self, _decision: &Decision, path: &DecisionPath) -> String {
1037        format!("{} selected with moderate confidence", path.name)
1038    }
1039
1040    fn calculate_projection_confidence(&self, decision: &Decision) -> f64 {
1041        (0.5 + decision.reasoning.confidence * 0.4).clamp(0.1, 1.0)
1042    }
1043
1044    fn project_path_timeline(&self, path: &DecisionPath) -> Vec<ProjectedEvent> {
1045        vec![ProjectedEvent {
1046            time_offset_days: 14.0,
1047            event: format!("{} key milestone", path.name),
1048            probability: 0.6,
1049            impact: "roadmap shift".to_string(),
1050        }]
1051    }
1052
1053    fn project_path_final_state(&self, path: &DecisionPath) -> String {
1054        format!("Path {} likely reaches usable state", path.name)
1055    }
1056
1057    fn assess_path_risk(&self, path: &DecisionPath) -> String {
1058        let risk = path.estimated_risk.unwrap_or(0.5);
1059        if risk > 0.7 {
1060            "high".to_string()
1061        } else if risk > 0.4 {
1062            "medium".to_string()
1063        } else {
1064            "low".to_string()
1065        }
1066    }
1067
1068    fn assess_path_opportunity(&self, path: &DecisionPath) -> String {
1069        if path.pros.len() >= path.cons.len() {
1070            "favorable".to_string()
1071        } else {
1072            "constrained".to_string()
1073        }
1074    }
1075
1076    // --- Missing query operations (SPEC-PART2) ---
1077
1078    pub fn search_decisions(&self, query: &str) -> Vec<Decision> {
1079        let query_lower = query.to_lowercase();
1080        self.decision_store
1081            .values()
1082            .filter(|d| {
1083                d.question.question.to_lowercase().contains(&query_lower)
1084                    || d.question.context.to_lowercase().contains(&query_lower)
1085                    || d.reasoning.rationale.to_lowercase().contains(&query_lower)
1086                    || d.chosen
1087                        .as_ref()
1088                        .map(|p| {
1089                            p.name.to_lowercase().contains(&query_lower)
1090                                || p.description.to_lowercase().contains(&query_lower)
1091                        })
1092                        .unwrap_or(false)
1093                    || d.shadows.iter().any(|s| {
1094                        s.path.name.to_lowercase().contains(&query_lower)
1095                            || s.path.description.to_lowercase().contains(&query_lower)
1096                            || s.rejection_reason.to_lowercase().contains(&query_lower)
1097                    })
1098                    || d.consequences
1099                        .iter()
1100                        .any(|c| c.description.to_lowercase().contains(&query_lower))
1101            })
1102            .cloned()
1103            .collect()
1104    }
1105
1106    pub fn get_progress_forecast(&self, id: GoalId) -> Result<ProgressForecast> {
1107        let goal = self.goal_store.get(&id).ok_or(Error::GoalNotFound(id))?;
1108
1109        let now = Timestamp::now();
1110        let velocity = goal.progress.velocity;
1111        let current = goal.progress.percentage;
1112
1113        let mut milestones = Vec::new();
1114        let mut risk_factors = Vec::new();
1115
1116        if velocity > 0.0 {
1117            for target in &[0.25, 0.5, 0.75, 1.0] {
1118                if current < *target {
1119                    let remaining = target - current;
1120                    let days_needed = remaining / velocity;
1121                    let est_at =
1122                        Timestamp::from_nanos(now.0 + (days_needed * 86_400.0 * 1e9) as i64);
1123                    let confidence = (1.0_f64 - remaining * 0.3).clamp(0.1, 0.95);
1124                    milestones.push(ForecastMilestone {
1125                        percentage: *target,
1126                        estimated_at: est_at,
1127                        confidence,
1128                    });
1129                }
1130            }
1131        }
1132
1133        let estimated_completion = if velocity > 0.0 && current < 1.0 {
1134            let days_to_complete = (1.0 - current) / velocity;
1135            Some(Timestamp::from_nanos(
1136                now.0 + (days_to_complete * 86_400.0 * 1e9) as i64,
1137            ))
1138        } else if current >= 1.0 {
1139            goal.completed_at
1140        } else {
1141            None
1142        };
1143
1144        // Check deadline risk
1145        if let (Some(deadline), Some(est)) = (goal.deadline, estimated_completion) {
1146            if est.0 > deadline.0 {
1147                risk_factors.push("Forecast exceeds deadline".to_string());
1148            }
1149        }
1150
1151        if !goal.blockers.iter().all(|b| b.resolved_at.is_some()) {
1152            risk_factors.push("Active blockers may slow progress".to_string());
1153        }
1154
1155        if goal.feelings.neglect > 0.5 {
1156            risk_factors.push("High neglect may reduce velocity".to_string());
1157        }
1158
1159        if goal.physics.momentum < 0.2 {
1160            risk_factors.push("Low momentum suggests stalling".to_string());
1161        }
1162
1163        let confidence = if velocity > 0.0 {
1164            (goal.feelings.confidence * 0.5 + goal.physics.momentum * 0.3 + 0.2).clamp(0.1, 0.95)
1165        } else {
1166            0.1
1167        };
1168
1169        Ok(ProgressForecast {
1170            goal_id: id,
1171            current_percentage: current,
1172            current_velocity: velocity,
1173            projected_milestones: milestones,
1174            estimated_completion,
1175            confidence,
1176            risk_factors,
1177        })
1178    }
1179
1180    pub fn get_momentum_report(&self) -> MomentumReport {
1181        let active_goals: Vec<&Goal> = self
1182            .goal_store
1183            .values()
1184            .filter(|g| matches!(g.status, GoalStatus::Active | GoalStatus::Blocked))
1185            .collect();
1186
1187        let total = active_goals.len();
1188        let avg = if total > 0 {
1189            active_goals.iter().map(|g| g.physics.momentum).sum::<f64>() / total as f64
1190        } else {
1191            0.0
1192        };
1193
1194        let mut distribution = MomentumDistribution {
1195            high: 0,
1196            medium: 0,
1197            low: 0,
1198            zero: 0,
1199        };
1200
1201        let mut entries: Vec<GoalMomentumEntry> = active_goals
1202            .iter()
1203            .map(|g| {
1204                match g.physics.momentum {
1205                    m if m >= 0.7 => distribution.high += 1,
1206                    m if m >= 0.3 => distribution.medium += 1,
1207                    m if m > 0.0 => distribution.low += 1,
1208                    _ => distribution.zero += 1,
1209                }
1210                GoalMomentumEntry {
1211                    goal_id: g.id,
1212                    title: g.title.clone(),
1213                    momentum: g.physics.momentum,
1214                    velocity: g.progress.velocity,
1215                    progress: g.progress.percentage,
1216                }
1217            })
1218            .collect();
1219
1220        entries.sort_by(|a, b| {
1221            b.momentum
1222                .partial_cmp(&a.momentum)
1223                .unwrap_or(std::cmp::Ordering::Equal)
1224        });
1225
1226        let top = entries.iter().take(5).cloned().collect();
1227        let stalled: Vec<GoalMomentumEntry> = entries
1228            .iter()
1229            .filter(|e| e.momentum < 0.05 && e.progress < 1.0)
1230            .cloned()
1231            .collect();
1232
1233        // Accelerating: velocity > momentum (gaining speed)
1234        let accelerating: Vec<GoalMomentumEntry> = entries
1235            .iter()
1236            .filter(|e| e.velocity > e.momentum && e.velocity > 0.0)
1237            .cloned()
1238            .collect();
1239
1240        // Decelerating: momentum > velocity (losing speed)
1241        let decelerating: Vec<GoalMomentumEntry> = entries
1242            .iter()
1243            .filter(|e| e.momentum > e.velocity + 0.1 && e.velocity >= 0.0)
1244            .cloned()
1245            .collect();
1246
1247        MomentumReport {
1248            total_goals: total,
1249            average_momentum: avg,
1250            momentum_distribution: distribution,
1251            top_momentum: top,
1252            stalled,
1253            accelerating,
1254            decelerating,
1255        }
1256    }
1257
1258    pub fn get_gravity_field(&self) -> GravityField {
1259        let active_goals: Vec<&Goal> = self
1260            .goal_store
1261            .values()
1262            .filter(|g| matches!(g.status, GoalStatus::Active | GoalStatus::Blocked))
1263            .collect();
1264
1265        let total = active_goals.len();
1266
1267        let (weighted_urgency, weighted_priority, weighted_momentum) = if total > 0 {
1268            let u = active_goals
1269                .iter()
1270                .map(|g| g.feelings.urgency * g.physics.gravity)
1271                .sum::<f64>()
1272                / total as f64;
1273            let p = active_goals
1274                .iter()
1275                .map(|g| {
1276                    let pv = match g.priority {
1277                        Priority::Critical => 1.0,
1278                        Priority::High => 0.8,
1279                        Priority::Medium => 0.5,
1280                        Priority::Low => 0.3,
1281                        Priority::Someday => 0.1,
1282                    };
1283                    pv * g.physics.gravity
1284                })
1285                .sum::<f64>()
1286                / total as f64;
1287            let m = active_goals
1288                .iter()
1289                .map(|g| g.physics.momentum * g.physics.gravity)
1290                .sum::<f64>()
1291                / total as f64;
1292            (u, p, m)
1293        } else {
1294            (0.0, 0.0, 0.0)
1295        };
1296
1297        let mut wells: Vec<GravityWell> = active_goals
1298            .iter()
1299            .filter(|g| g.physics.gravity > 0.3)
1300            .map(|g| {
1301                let pull_radius = g.physics.gravity * (1.0 + g.dependents.len() as f64 * 0.2);
1302                let captured: Vec<GoalId> = g
1303                    .children
1304                    .iter()
1305                    .chain(g.dependents.iter())
1306                    .copied()
1307                    .collect();
1308                GravityWell {
1309                    goal_id: g.id,
1310                    title: g.title.clone(),
1311                    gravity: g.physics.gravity,
1312                    pull_radius,
1313                    captured_goals: captured,
1314                }
1315            })
1316            .collect();
1317
1318        wells.sort_by(|a, b| {
1319            b.gravity
1320                .partial_cmp(&a.gravity)
1321                .unwrap_or(std::cmp::Ordering::Equal)
1322        });
1323
1324        let total_pull = wells.iter().map(|w| w.gravity).sum();
1325        let dominant = wells.first().map(|w| w.goal_id);
1326
1327        GravityField {
1328            total_goals: total,
1329            field_center: GravityCenter {
1330                weighted_urgency,
1331                weighted_priority,
1332                weighted_momentum,
1333            },
1334            wells,
1335            total_pull,
1336            dominant_attractor: dominant,
1337        }
1338    }
1339}