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, ¢er),
122 alignment_angle: self.calculate_alignment(g, ¢er),
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 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 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 let shared_goals = parent
197 .affected_goals
198 .iter()
199 .filter(|g| child.affected_goals.contains(g))
200 .count();
201
202 let has_negative_consequences = child
204 .consequences
205 .iter()
206 .any(|c| matches!(c.impact, Impact::Negative));
207
208 if parent.caused_by == Some(child.id) {
210 return CausalityType::Requires;
211 }
212
213 if has_negative_consequences && shared_goals > 0 {
215 return CausalityType::Constrains;
216 }
217
218 if child.status == DecisionStatus::Crystallized
220 && parent.status != DecisionStatus::Crystallized
221 {
222 return CausalityType::Suggests;
223 }
224
225 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 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 if child.status == DecisionStatus::Crystallized {
246 strength += 0.2;
247 }
248
249 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let accelerating: Vec<GoalMomentumEntry> = entries
1235 .iter()
1236 .filter(|e| e.velocity > e.momentum && e.velocity > 0.0)
1237 .cloned()
1238 .collect();
1239
1240 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}