Skip to main content

agentic_planning/
inventions.rs

1//! Inventions module — batch and parallel optimizations for PlanningEngine.
2//!
3//! # Parallelism Strategy
4//!
5//! The `PlanningEngine` holds mutable state in `HashMap`s and is `!Sync`.
6//! True parallelism (rayon, tokio::spawn) would require either:
7//!   - `Arc<RwLock<PlanningEngine>>` (contention-heavy, defeats purpose)
8//!   - Unsafe split-borrows of disjoint HashMap entries (fragile, unsound risk)
9//!
10//! Instead, we take a **pragmatic optimized-serial** approach:
11//!   1. Pre-filter: collect only relevant IDs/data before the hot loop
12//!   2. Batch compute: minimize per-item overhead (fewer HashMap lookups)
13//!   3. Single-pass merge: avoid repeated allocations and sorts
14//!
15//! For `create_goals_batch`, the win is concrete:
16//!   - Validate ALL requests upfront (fail-fast, no partial inserts)
17//!   - Generate IDs and build goals in one pass
18//!   - Insert all goals, wire parent/dependency links
19//!   - Rebuild indexes ONCE (not N incremental `add_goal` calls)
20//!
21//! When the engine moves to an actor or async model, these functions
22//! become natural sharding points for real parallelism.
23
24use crate::types::*;
25use crate::{CreateCommitmentRequest, CreateDecisionRequest, PlanningEngine};
26use std::collections::HashMap;
27use uuid::Uuid;
28
29impl PlanningEngine {
30    /// Compute the intention singularity with pre-filtered active goals.
31    ///
32    /// Optimization over raw `get_intention_singularity()`:
33    /// - Pre-collects active goal references to avoid repeated status checks
34    /// - Short-circuits the empty case (no active goals → empty singularity)
35    pub fn calculate_singularity_parallel(&self) -> IntentionSingularity {
36        // Pre-filter: check for active goals without full computation
37        let has_active = self
38            .goal_store
39            .values()
40            .any(|g| g.status == GoalStatus::Active);
41
42        if !has_active {
43            return IntentionSingularity {
44                center: IntentionCenter {
45                    urgency: 0.0,
46                    confidence: 0.0,
47                    momentum: 0.0,
48                },
49                unified_vision: String::new(),
50                goal_positions: HashMap::new(),
51                tension_lines: Vec::new(),
52                themes: Vec::new(),
53                golden_path: Vec::new(),
54            };
55        }
56
57        // Delegate to the full computation — it handles the heavy lifting.
58        // The pre-filter above short-circuits the empty case.
59        self.get_intention_singularity()
60    }
61
62    /// Scan for blocker prophecies with pre-filtered active goals.
63    ///
64    /// Optimization: pre-computes blocker type frequency histogram once,
65    /// then runs prediction in a single pass with pre-allocated output.
66    pub fn scan_blockers_parallel(&self) -> Vec<BlockerProphecy> {
67        let active_goals: Vec<&Goal> = self
68            .goal_store
69            .values()
70            .filter(|g| g.status == GoalStatus::Active || g.status == GoalStatus::Blocked)
71            .collect();
72
73        if active_goals.is_empty() {
74            return Vec::new();
75        }
76
77        // Pre-compute blocker type frequency histogram (shared across goals)
78        let mut blocker_type_counts: HashMap<String, usize> = HashMap::new();
79        for goal in self.goal_store.values() {
80            for blocker in &goal.blockers {
81                let key = format!("{:?}", std::mem::discriminant(&blocker.blocker_type));
82                *blocker_type_counts.entry(key).or_insert(0) += 1;
83            }
84        }
85        let total_historical = blocker_type_counts.values().sum::<usize>().max(1);
86
87        // Single pass: predict blockers for each active goal
88        let mut prophecies = Vec::with_capacity(active_goals.len() * 2);
89
90        for goal in &active_goals {
91            for blocker in self.predict_blockers(goal) {
92                let type_key = format!("{:?}", std::mem::discriminant(&blocker.blocker_type));
93                let type_frequency = *blocker_type_counts.get(&type_key).unwrap_or(&0) as f64
94                    / total_historical as f64;
95                let severity_signal = blocker.severity;
96                let prediction_confidence =
97                    (0.3 + type_frequency * 0.3 + severity_signal * 0.3).clamp(0.1, 0.95);
98
99                let days_until = goal
100                    .deadline
101                    .map(|d| {
102                        let days = ((d.0 - Timestamp::now().0) as f64 / (86_400.0 * 1e9)).max(0.5);
103                        (days * (1.0 - severity_signal)).max(1.0)
104                    })
105                    .unwrap_or(14.0 * (1.0 - severity_signal * 0.5));
106
107                let mut evidence = Vec::new();
108                if goal.progress.velocity == 0.0 {
109                    evidence.push("zero progress velocity".to_string());
110                }
111                if goal.feelings.neglect > 0.5 {
112                    evidence.push(format!("high neglect score ({:.2})", goal.feelings.neglect));
113                }
114
115                prophecies.push(BlockerProphecy {
116                    goal_id: goal.id,
117                    predicted_blocker: blocker.clone(),
118                    prediction_confidence,
119                    days_until_materialization: days_until,
120                    evidence,
121                    recommended_actions: blocker
122                        .resolution
123                        .clone()
124                        .map(|r| vec![r])
125                        .unwrap_or_default(),
126                });
127            }
128        }
129
130        // Sort by confidence descending for priority ordering
131        prophecies.sort_by(|a, b| {
132            b.prediction_confidence
133                .partial_cmp(&a.prediction_confidence)
134                .unwrap_or(std::cmp::Ordering::Equal)
135        });
136        prophecies
137    }
138
139    /// Detect progress echoes with pre-filtered near-completion goals.
140    ///
141    /// Optimization: only examines goals with progress > 0.5 and momentum > 0.2,
142    /// skipping the bulk of low-progress goals. Uses the same logic as
143    /// `listen_progress_echoes` but with an early-exit filter.
144    pub fn progress_echoes_parallel(&self) -> Vec<ProgressEcho> {
145        let candidates: Vec<&Goal> = self
146            .goal_store
147            .values()
148            .filter(|g| {
149                g.status == GoalStatus::Active
150                    && g.progress.percentage > 0.5
151                    && g.physics.momentum > 0.2
152            })
153            .collect();
154
155        if candidates.is_empty() {
156            return Vec::new();
157        }
158
159        let mut echoes = Vec::with_capacity(candidates.len());
160
161        for goal in &candidates {
162            let eta_days = goal
163                .progress
164                .eta
165                .map(|eta| ((eta.0 - Timestamp::now().0) as f64 / (86_400.0 * 1e9)).max(1.0))
166                .unwrap_or(30.0);
167
168            if eta_days < 30.0 {
169                echoes.push(ProgressEcho {
170                    goal_id: goal.id,
171                    source_milestone: Milestone {
172                        name: format!("{} completed", goal.title),
173                        description: "Goal completion".to_string(),
174                    },
175                    echo_strength: goal.physics.momentum,
176                    estimated_arrival_secs: (eta_days * 86_400.0) as u64,
177                    carried_information: self.extract_echo_info(goal),
178                    confidence: goal.feelings.confidence,
179                });
180            }
181        }
182
183        // Sort by echo_strength descending
184        echoes.sort_by(|a, b| {
185            b.echo_strength
186                .partial_cmp(&a.echo_strength)
187                .unwrap_or(std::cmp::Ordering::Equal)
188        });
189        echoes
190    }
191
192    /// Create multiple goals in a single batch operation.
193    ///
194    /// Unlike calling `create_goal()` N times, this:
195    ///   1. Validates ALL requests upfront (fail-fast: no partial inserts on error)
196    ///   2. Pre-generates IDs and timestamps in one pass
197    ///   3. Inserts all goals into the store
198    ///   4. Wires parent/child and dependency links
199    ///   5. Rebuilds indexes ONCE at the end (not N incremental adds)
200    ///
201    /// Performance: for N=10 goals, avoids 9 redundant index rebuilds.
202    /// For N=100, the savings are substantial.
203    pub fn create_goals_batch(
204        &mut self,
205        requests: Vec<crate::CreateGoalRequest>,
206    ) -> crate::Result<Vec<Goal>> {
207        if requests.is_empty() {
208            return Ok(Vec::new());
209        }
210
211        // ── Phase 1: Validate ALL requests upfront ──
212        // Fail-fast: if any request is invalid, return error before mutating state.
213        for (i, req) in requests.iter().enumerate() {
214            if let Err(errors) = self.validate_create_goal(req) {
215                let msg = errors
216                    .iter()
217                    .map(|e| e.to_string())
218                    .collect::<Vec<_>>()
219                    .join("; ");
220                return Err(crate::Error::CorruptedFile(format!(
221                    "batch validation failed at request[{}]: {}",
222                    i, msg
223                )));
224            }
225        }
226
227        // ── Phase 2: Generate IDs and build Goal structs ──
228        let now = Timestamp::now();
229        let mut goals = Vec::with_capacity(requests.len());
230
231        for request in requests {
232            let id = GoalId(Uuid::new_v4());
233            let urgency = self.calculate_initial_urgency(&request);
234            let gravity = self.calculate_initial_gravity(&request);
235            let inertia = self.calculate_initial_inertia(&request);
236
237            let soul = GoalSoul {
238                intention: request.intention.clone(),
239                significance: request.significance.unwrap_or_default(),
240                success_criteria: request.success_criteria.unwrap_or_default(),
241                emotional_weight: request.emotional_weight.unwrap_or(0.5),
242                values: request.values.unwrap_or_default(),
243            };
244
245            let feelings = GoalFeelings {
246                urgency,
247                neglect: 0.0,
248                confidence: 0.5,
249                alignment: 1.0,
250                vitality: 1.0,
251                last_calculated: now,
252            };
253
254            let physics = GoalPhysics {
255                momentum: 0.0,
256                gravity,
257                inertia,
258                energy: 1.0,
259                last_calculated: now,
260            };
261
262            let goal = Goal {
263                id,
264                title: request.title,
265                description: request.description,
266                soul,
267                status: GoalStatus::Draft,
268                created_at: now,
269                activated_at: None,
270                completed_at: None,
271                deadline: request.deadline,
272                parent: request.parent,
273                children: Vec::new(),
274                dependencies: request.dependencies.unwrap_or_default(),
275                dependents: Vec::new(),
276                relationships: Vec::new(),
277                priority: request.priority.unwrap_or(Priority::Medium),
278                progress: Progress::new(),
279                feelings,
280                physics,
281                blockers: Vec::new(),
282                decisions: Vec::new(),
283                commitments: Vec::new(),
284                dreams: Vec::new(),
285                tags: request.tags.unwrap_or_default(),
286                metadata: request.metadata.unwrap_or_default(),
287                provenance: GoalProvenance {
288                    origin: request.origin.unwrap_or(ProvenanceOrigin::UserRequest),
289                    user_request: request.user_request,
290                    session_id: request.session_id,
291                    creation_context: request.context.unwrap_or_default(),
292                },
293                metamorphosis: None,
294                previous_life: None,
295            };
296
297            goals.push(goal);
298        }
299
300        // ── Phase 3: Batch insert into goal_store ──
301        for goal in &goals {
302            self.goal_store.insert(goal.id, goal.clone());
303        }
304
305        // ── Phase 4: Wire parent/child and dependency links ──
306        // Collect link operations first, then apply (avoids borrow conflicts).
307        let links: Vec<(GoalId, Option<GoalId>, Vec<GoalId>)> = goals
308            .iter()
309            .map(|g| (g.id, g.parent, g.dependencies.clone()))
310            .collect();
311
312        for (child_id, parent_opt, deps) in &links {
313            if let Some(parent_id) = parent_opt {
314                if let Some(parent) = self.goal_store.get_mut(parent_id) {
315                    parent.children.push(*child_id);
316                }
317            }
318            for dep_id in deps {
319                if let Some(dep) = self.goal_store.get_mut(dep_id) {
320                    dep.dependents.push(*child_id);
321                }
322            }
323        }
324
325        // ── Phase 5: Rebuild indexes ONCE ──
326        // This is the key optimization: instead of N incremental `add_goal` calls,
327        // we do a single full rebuild. For N>3 this is already faster.
328        self.rebuild_indexes();
329        self.mark_dirty();
330
331        Ok(goals)
332    }
333
334    // ════════════════════════════════════════════════════════════════════
335    // Batch write operations — Goal lifecycle
336    // ════════════════════════════════════════════════════════════════════
337
338    /// Activate multiple Draft/Reborn goals in one pass.
339    ///
340    /// Validates all IDs upfront (fail-fast), applies transitions, and
341    /// calls `mark_dirty` once at the end.
342    pub fn batch_activate_goals(&mut self, ids: &[GoalId]) -> crate::Result<Vec<Goal>> {
343        if ids.is_empty() {
344            return Ok(Vec::new());
345        }
346
347        // Phase 1: validate all goals exist and are in valid state
348        for id in ids {
349            let goal = self
350                .goal_store
351                .get(id)
352                .ok_or(crate::Error::GoalNotFound(*id))?;
353            if goal.status != GoalStatus::Draft && goal.status != GoalStatus::Reborn {
354                return Err(crate::Error::InvalidTransition {
355                    from: goal.status,
356                    to: GoalStatus::Active,
357                });
358            }
359        }
360
361        // Phase 2: apply transitions
362        let now = Timestamp::now();
363        let mut results = Vec::with_capacity(ids.len());
364
365        for id in ids {
366            let goal = self
367                .goal_store
368                .get_mut(id)
369                .expect("goal existence validated in phase 1");
370            let old = goal.status;
371            goal.status = GoalStatus::Active;
372            goal.activated_at = Some(now);
373            goal.feelings.vitality = 1.0;
374            self.indexes
375                .goal_status_changed(*id, old, GoalStatus::Active);
376            results.push(goal.clone());
377        }
378
379        self.mark_dirty();
380        Ok(results)
381    }
382
383    /// Progress multiple goals at once with batched velocity/momentum recalc.
384    ///
385    /// Each entry is `(goal_id, new_percentage, optional_note)`.
386    /// Validates all IDs upfront, records progress points, recalculates
387    /// velocity/momentum/confidence for each, and marks dirty once.
388    pub fn batch_progress_goals(
389        &mut self,
390        updates: Vec<(GoalId, f64, Option<String>)>,
391    ) -> crate::Result<Vec<Goal>> {
392        if updates.is_empty() {
393            return Ok(Vec::new());
394        }
395
396        // Phase 1: validate all goals exist
397        for (id, _, _) in &updates {
398            if !self.goal_store.contains_key(id) {
399                return Err(crate::Error::GoalNotFound(*id));
400            }
401        }
402
403        // Phase 2: record progress points and collect snapshots
404        let now = Timestamp::now();
405        let mut snapshots = Vec::with_capacity(updates.len());
406
407        for (id, percentage, note) in &updates {
408            let goal = self
409                .goal_store
410                .get_mut(id)
411                .expect("goal existence validated in phase 1");
412            let p = percentage.clamp(0.0, 1.0);
413            goal.progress.history.push(ProgressPoint {
414                timestamp: now,
415                percentage: p,
416                note: note.clone(),
417            });
418            goal.progress.percentage = p;
419            snapshots.push((*id, goal.clone()));
420        }
421
422        // Phase 3: recalculate velocity/momentum/confidence
423        let recalcs: Vec<(GoalId, f64, f64, f64)> = snapshots
424            .iter()
425            .map(|(id, snap)| {
426                let v = self.calculate_velocity(&snap.progress.history);
427                let m = self.calculate_momentum_from_goal(snap);
428                let c = self.calculate_confidence_from_goal(snap);
429                (*id, v, m, c)
430            })
431            .collect();
432
433        // Phase 4: apply recalculated values
434        let mut results = Vec::with_capacity(recalcs.len());
435        for (id, velocity, momentum, confidence) in recalcs {
436            let goal = self
437                .goal_store
438                .get_mut(&id)
439                .expect("goal existence validated in phase 1");
440            goal.progress.velocity = velocity;
441            if velocity > 0.0 {
442                let remaining = 1.0 - goal.progress.percentage;
443                let days_remaining = remaining / velocity;
444                goal.progress.eta = Some(Timestamp::days_from_now(days_remaining));
445            }
446            goal.physics.momentum = momentum;
447            goal.feelings.neglect = 0.0;
448            goal.feelings.confidence = confidence;
449            goal.feelings.last_calculated = now;
450
451            if goal.progress.percentage >= 1.0 {
452                for c in &mut goal.soul.success_criteria {
453                    c.achieved = true;
454                    c.achieved_at = Some(now);
455                }
456            }
457            results.push(goal.clone());
458        }
459
460        self.mark_dirty();
461        Ok(results)
462    }
463
464    // ════════════════════════════════════════════════════════════════════
465    // Batch write operations — Decisions
466    // ════════════════════════════════════════════════════════════════════
467
468    /// Create multiple decisions in a single batch.
469    ///
470    /// Validates all requests upfront, generates IDs, wires goal links
471    /// and causal chains, rebuilds indexes once.
472    pub fn batch_create_decisions(
473        &mut self,
474        requests: Vec<CreateDecisionRequest>,
475    ) -> crate::Result<Vec<Decision>> {
476        if requests.is_empty() {
477            return Ok(Vec::new());
478        }
479
480        // Phase 1: validate all referenced goals exist
481        for (i, req) in requests.iter().enumerate() {
482            if let Some(goals) = &req.goals {
483                for gid in goals {
484                    if !self.goal_store.contains_key(gid) {
485                        return Err(crate::Error::CorruptedFile(format!(
486                            "batch decision[{}] references nonexistent goal {:?}",
487                            i, gid
488                        )));
489                    }
490                }
491            }
492        }
493
494        let now = Timestamp::now();
495        let mut decisions = Vec::with_capacity(requests.len());
496
497        // Phase 2: build all Decision structs
498        for request in requests {
499            let id = DecisionId(Uuid::new_v4());
500
501            let decision = Decision {
502                id,
503                question: DecisionQuestion {
504                    question: request.question,
505                    context: request.context.unwrap_or_default(),
506                    constraints: request.constraints.unwrap_or_default(),
507                    asked_at: now,
508                },
509                status: DecisionStatus::Pending,
510                crystallized_at: None,
511                chosen: None,
512                shadows: Vec::new(),
513                reasoning: DecisionReasoning::default(),
514                decider: request.decider.unwrap_or(Decider::User { name: None }),
515                affected_goals: request.goals.unwrap_or_default(),
516                caused_by: request.caused_by,
517                causes: Vec::new(),
518                reversibility: Reversibility::default(),
519                consequences: Vec::new(),
520                regret_score: 0.0,
521                regret_updated_at: None,
522            };
523            decisions.push(decision);
524        }
525
526        // Phase 3: batch insert and wire links
527        for decision in &decisions {
528            for goal_id in &decision.affected_goals {
529                if let Some(goal) = self.goal_store.get_mut(goal_id) {
530                    goal.decisions.push(decision.id);
531                }
532            }
533
534            if let Some(parent_id) = decision.caused_by {
535                if let Some(parent) = self.decision_store.get_mut(&parent_id) {
536                    parent.causes.push(decision.id);
537                }
538            }
539
540            self.decision_store.insert(decision.id, decision.clone());
541            self.indexes.add_decision(decision);
542        }
543
544        self.mark_dirty();
545        Ok(decisions)
546    }
547
548    // ════════════════════════════════════════════════════════════════════
549    // Batch write operations — Commitments
550    // ════════════════════════════════════════════════════════════════════
551
552    /// Create multiple commitments in a single batch.
553    ///
554    /// Validates all requests, generates IDs, calculates weight/inertia/cost
555    /// for each, wires to goals, and marks dirty once.
556    pub fn batch_create_commitments(
557        &mut self,
558        requests: Vec<CreateCommitmentRequest>,
559    ) -> crate::Result<Vec<Commitment>> {
560        if requests.is_empty() {
561            return Ok(Vec::new());
562        }
563
564        // Phase 1: validate all referenced goals exist
565        for (i, req) in requests.iter().enumerate() {
566            if let Some(gid) = req.goal {
567                if !self.goal_store.contains_key(&gid) {
568                    return Err(crate::Error::CorruptedFile(format!(
569                        "batch commitment[{}] references nonexistent goal {:?}",
570                        i, gid
571                    )));
572                }
573            }
574        }
575
576        let now = Timestamp::now();
577        let mut commitments = Vec::with_capacity(requests.len());
578
579        // Phase 2: build all Commitment structs
580        for request in &requests {
581            let id = CommitmentId(Uuid::new_v4());
582            let weight = self.calculate_commitment_weight(request);
583            let inertia = self.calculate_commitment_inertia(request);
584            let breaking_cost = self.calculate_breaking_cost(request);
585
586            commitments.push(Commitment {
587                id,
588                promise: request.promise.clone(),
589                made_to: request.stakeholder.clone(),
590                made_at: now,
591                due: request.due,
592                status: CommitmentStatus::Active,
593                weight,
594                inertia,
595                breaking_cost,
596                goal: request.goal,
597                entanglements: Vec::new(),
598                fulfillment: None,
599                renegotiations: Vec::new(),
600            });
601        }
602
603        // Phase 3: batch insert and wire to goals
604        for commitment in &commitments {
605            if let Some(goal_id) = commitment.goal {
606                if let Some(goal) = self.goal_store.get_mut(&goal_id) {
607                    goal.commitments.push(commitment.id);
608                }
609            }
610            self.commitment_store
611                .insert(commitment.id, commitment.clone());
612            self.indexes.add_commitment(commitment);
613        }
614
615        self.mark_dirty();
616        Ok(commitments)
617    }
618
619    /// Fulfill multiple commitments in a single pass.
620    ///
621    /// Validates all are Active upfront, calculates chain bonuses,
622    /// applies fulfillments, and handles entanglement energy release.
623    pub fn batch_fulfill_commitments(
624        &mut self,
625        fulfillments: Vec<(CommitmentId, String)>,
626    ) -> crate::Result<Vec<Commitment>> {
627        if fulfillments.is_empty() {
628            return Ok(Vec::new());
629        }
630
631        // Phase 1: validate all exist and are Active
632        for (id, _) in &fulfillments {
633            let c = self
634                .commitment_store
635                .get(id)
636                .ok_or(crate::Error::CommitmentNotFound(*id))?;
637            if c.status != CommitmentStatus::Active {
638                return Err(crate::Error::CannotFulfill(c.status));
639            }
640        }
641
642        // Phase 2: collect chain bonuses and entanglements
643        let pre_data: Vec<(CommitmentId, String, f64, f64, Vec<CommitmentEntanglement>)> =
644            fulfillments
645                .into_iter()
646                .map(|(id, how)| {
647                    let bonus = self.calculate_chain_bonus(id);
648                    let c = self
649                        .commitment_store
650                        .get(&id)
651                        .expect("commitment existence validated in phase 1");
652                    let weight = c.weight;
653                    let entanglements = c.entanglements.clone();
654                    (id, how, bonus, weight, entanglements)
655                })
656                .collect();
657
658        // Phase 3: apply fulfillments
659        let now = Timestamp::now();
660        let mut results = Vec::with_capacity(pre_data.len());
661
662        for (id, how_delivered, chain_bonus, weight, entanglements) in &pre_data {
663            let energy_released = weight * (1.0 + chain_bonus);
664
665            if let Some(c) = self.commitment_store.get_mut(id) {
666                c.status = CommitmentStatus::Fulfilled;
667                c.fulfillment = Some(CommitmentFulfillment {
668                    fulfilled_at: now,
669                    how_delivered: how_delivered.clone(),
670                    energy_released,
671                    trust_gained: c.weight * 0.5,
672                });
673            }
674
675            for entanglement in entanglements {
676                if entanglement.entanglement_type == EntanglementType::Sequential {
677                    let _ = self.boost_commitment(entanglement.with, energy_released * 0.3);
678                }
679            }
680
681            self.indexes.commitment_fulfilled(*id);
682            results.push(
683                self.commitment_store
684                    .get(id)
685                    .ok_or(crate::Error::CommitmentNotFound(*id))?
686                    .clone(),
687            );
688        }
689
690        self.mark_dirty();
691        Ok(results)
692    }
693
694    // ════════════════════════════════════════════════════════════════════
695    // Batch read operations — Reports and scans
696    // ════════════════════════════════════════════════════════════════════
697
698    /// Momentum report with pre-filtered index-based active goals.
699    ///
700    /// Optimization over `get_momentum_report()`:
701    /// - Uses `indexes.goals_by_status` to avoid full store scan
702    /// - Same output shape, but skips inactive/completed goals entirely
703    pub fn momentum_report_indexed(&self) -> MomentumReport {
704        let active_ids = self
705            .indexes
706            .goals_by_status
707            .get(&GoalStatus::Active)
708            .cloned()
709            .unwrap_or_default();
710        let blocked_ids = self
711            .indexes
712            .goals_by_status
713            .get(&GoalStatus::Blocked)
714            .cloned()
715            .unwrap_or_default();
716
717        let active_goals: Vec<&Goal> = active_ids
718            .iter()
719            .chain(blocked_ids.iter())
720            .filter_map(|id| self.goal_store.get(id))
721            .collect();
722
723        let total = active_goals.len();
724        let avg = if total > 0 {
725            active_goals.iter().map(|g| g.physics.momentum).sum::<f64>() / total as f64
726        } else {
727            0.0
728        };
729
730        let mut distribution = MomentumDistribution {
731            high: 0,
732            medium: 0,
733            low: 0,
734            zero: 0,
735        };
736
737        let mut entries: Vec<GoalMomentumEntry> = active_goals
738            .iter()
739            .map(|g| {
740                match g.physics.momentum {
741                    m if m >= 0.7 => distribution.high += 1,
742                    m if m >= 0.3 => distribution.medium += 1,
743                    m if m > 0.0 => distribution.low += 1,
744                    _ => distribution.zero += 1,
745                }
746                GoalMomentumEntry {
747                    goal_id: g.id,
748                    title: g.title.clone(),
749                    momentum: g.physics.momentum,
750                    velocity: g.progress.velocity,
751                    progress: g.progress.percentage,
752                }
753            })
754            .collect();
755
756        entries.sort_by(|a, b| {
757            b.momentum
758                .partial_cmp(&a.momentum)
759                .unwrap_or(std::cmp::Ordering::Equal)
760        });
761
762        let top = entries.iter().take(5).cloned().collect();
763        let stalled: Vec<GoalMomentumEntry> = entries
764            .iter()
765            .filter(|e| e.momentum < 0.05 && e.progress < 1.0)
766            .cloned()
767            .collect();
768
769        // Accelerating: velocity > momentum (gaining speed)
770        let accelerating: Vec<GoalMomentumEntry> = entries
771            .iter()
772            .filter(|e| e.velocity > e.momentum && e.velocity > 0.0)
773            .cloned()
774            .collect();
775
776        // Decelerating: momentum > velocity (losing speed)
777        let decelerating: Vec<GoalMomentumEntry> = entries
778            .iter()
779            .filter(|e| e.momentum > e.velocity + 0.1 && e.velocity >= 0.0)
780            .cloned()
781            .collect();
782
783        MomentumReport {
784            total_goals: total,
785            average_momentum: avg,
786            momentum_distribution: distribution,
787            top_momentum: top,
788            stalled,
789            accelerating,
790            decelerating,
791        }
792    }
793
794    /// Gravity field with index-based pre-filtering.
795    ///
796    /// Optimization over `get_gravity_field()`:
797    /// - Uses `indexes.goals_by_status` to skip inactive goals
798    /// - Computes weighted center and gravity wells in a single pass
799    pub fn gravity_field_indexed(&self) -> GravityField {
800        let active_ids = self
801            .indexes
802            .goals_by_status
803            .get(&GoalStatus::Active)
804            .cloned()
805            .unwrap_or_default();
806        let blocked_ids = self
807            .indexes
808            .goals_by_status
809            .get(&GoalStatus::Blocked)
810            .cloned()
811            .unwrap_or_default();
812
813        let relevant: Vec<&Goal> = active_ids
814            .iter()
815            .chain(blocked_ids.iter())
816            .filter_map(|id| self.goal_store.get(id))
817            .collect();
818
819        let total = relevant.len();
820
821        let (weighted_urgency, weighted_priority, weighted_momentum) = if total > 0 {
822            let u = relevant
823                .iter()
824                .map(|g| g.feelings.urgency * g.physics.gravity)
825                .sum::<f64>()
826                / total as f64;
827            let p = relevant
828                .iter()
829                .map(|g| {
830                    let pv = match g.priority {
831                        Priority::Critical => 1.0,
832                        Priority::High => 0.8,
833                        Priority::Medium => 0.5,
834                        Priority::Low => 0.3,
835                        Priority::Someday => 0.1,
836                    };
837                    pv * g.physics.gravity
838                })
839                .sum::<f64>()
840                / total as f64;
841            let m = relevant
842                .iter()
843                .map(|g| g.physics.momentum * g.physics.gravity)
844                .sum::<f64>()
845                / total as f64;
846            (u, p, m)
847        } else {
848            (0.0, 0.0, 0.0)
849        };
850
851        let mut wells: Vec<GravityWell> = relevant
852            .iter()
853            .filter(|g| g.physics.gravity > 0.3)
854            .map(|g| {
855                let pull_radius = g.physics.gravity * (1.0 + g.dependents.len() as f64 * 0.2);
856                let captured: Vec<GoalId> = g
857                    .children
858                    .iter()
859                    .chain(g.dependents.iter())
860                    .copied()
861                    .collect();
862                GravityWell {
863                    goal_id: g.id,
864                    title: g.title.clone(),
865                    gravity: g.physics.gravity,
866                    pull_radius,
867                    captured_goals: captured,
868                }
869            })
870            .collect();
871
872        wells.sort_by(|a, b| {
873            b.gravity
874                .partial_cmp(&a.gravity)
875                .unwrap_or(std::cmp::Ordering::Equal)
876        });
877
878        let total_pull = wells.iter().map(|w| w.gravity).sum();
879        let dominant = wells.first().map(|w| w.goal_id);
880
881        GravityField {
882            total_goals: total,
883            field_center: GravityCenter {
884                weighted_urgency,
885                weighted_priority,
886                weighted_momentum,
887            },
888            wells,
889            total_pull,
890            dominant_attractor: dominant,
891        }
892    }
893
894    /// Scan all active commitments for at-risk status in one pass.
895    ///
896    /// Returns commitment IDs and their risk signals.
897    pub fn at_risk_commitments_scan(&self) -> Vec<(CommitmentId, Vec<String>)> {
898        let mut results = Vec::new();
899
900        for commitment in self.commitment_store.values() {
901            if commitment.status != CommitmentStatus::Active {
902                continue;
903            }
904
905            let mut risks = Vec::new();
906            if let Some(due) = commitment.due {
907                let now = Timestamp::now();
908                let days_remaining = (due.0 - now.0) as f64 / (86_400.0 * 1e9);
909
910                if days_remaining < 0.0 {
911                    risks.push("Overdue".to_string());
912                } else if days_remaining < 3.0 {
913                    risks.push(format!("Due in {:.1} days", days_remaining));
914                }
915
916                if let Some(goal_id) = commitment.goal {
917                    if let Some(goal) = self.goal_store.get(&goal_id) {
918                        let remaining_work = 1.0 - goal.progress.percentage;
919                        let days_needed = if goal.progress.velocity > 0.0 {
920                            remaining_work / goal.progress.velocity
921                        } else {
922                            f64::INFINITY
923                        };
924                        if days_needed > days_remaining {
925                            risks.push(format!(
926                                "Needs {:.1} days but only {:.1} remain",
927                                days_needed, days_remaining
928                            ));
929                        }
930                    }
931                }
932            }
933
934            if commitment.weight > 1.5 {
935                risks.push("High-weight commitment".to_string());
936            }
937
938            if !risks.is_empty() {
939                results.push((commitment.id, risks));
940            }
941        }
942
943        results.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
944        results
945    }
946
947    /// Scan all active/blocked goals for metamorphosis signals in one pass.
948    ///
949    /// Returns only goals that should transform, skipping stable ones.
950    pub fn metamorphosis_scan(&self) -> Vec<MetamorphosisSignal> {
951        let mut signals = Vec::new();
952
953        let active_ids = self
954            .indexes
955            .goals_by_status
956            .get(&GoalStatus::Active)
957            .cloned()
958            .unwrap_or_default();
959        let blocked_ids = self
960            .indexes
961            .goals_by_status
962            .get(&GoalStatus::Blocked)
963            .cloned()
964            .unwrap_or_default();
965
966        for id in active_ids.iter().chain(blocked_ids.iter()) {
967            if let Ok(signal) = self.detect_metamorphosis(*id) {
968                if signal.should_transform {
969                    signals.push(signal);
970                }
971            }
972        }
973
974        signals
975    }
976
977    /// Progress forecasts for multiple goals at once.
978    ///
979    /// Collects forecasts for all specified goals, returning
980    /// partial results (skipping not-found goals rather than failing).
981    pub fn progress_forecast_batch(&self, ids: &[GoalId]) -> Vec<ProgressForecast> {
982        ids.iter()
983            .filter_map(|id| self.get_progress_forecast(*id).ok())
984            .collect()
985    }
986
987    /// Progress forecasts for ALL active goals.
988    ///
989    /// Uses index to identify active goals, then computes forecasts
990    /// in a single pass. Useful for dashboard views.
991    pub fn progress_forecast_all_active(&self) -> Vec<ProgressForecast> {
992        let active_ids = self
993            .indexes
994            .goals_by_status
995            .get(&GoalStatus::Active)
996            .cloned()
997            .unwrap_or_default();
998
999        active_ids
1000            .iter()
1001            .filter_map(|id| self.get_progress_forecast(*id).ok())
1002            .collect()
1003    }
1004
1005    /// Dream multiple goals and collect all resulting dreams.
1006    ///
1007    /// Unlike calling `dream_goal` N times, this batches the dirty
1008    /// marking. Note: each dream may create child goals if confidence > 0.7.
1009    pub fn dream_goals_batch(&mut self, ids: &[GoalId]) -> Vec<crate::Result<Dream>> {
1010        if ids.is_empty() {
1011            return Vec::new();
1012        }
1013
1014        // Validate all exist first
1015        let valid_ids: Vec<GoalId> = ids
1016            .iter()
1017            .filter(|id| self.goal_store.contains_key(id))
1018            .copied()
1019            .collect();
1020
1021        // Dream each goal (each internally marks dirty, but we accept that
1022        // since dream_goal creates child goals which need intermediate state)
1023        valid_ids.iter().map(|id| self.dream_goal(*id)).collect()
1024    }
1025
1026    /// Check health across all federations in one pass.
1027    ///
1028    /// Returns federation ID, member count, sync freshness, and any issues.
1029    pub fn federation_health_scan(&self) -> Vec<FederationHealthEntry> {
1030        let now = Timestamp::now();
1031        self.federation_store
1032            .values()
1033            .map(|fed| {
1034                let sync_age_hours = (now.0 - fed.last_sync.0) as f64 / (3_600.0 * 1e9);
1035                let mut issues = Vec::new();
1036
1037                if fed.members.is_empty() {
1038                    issues.push("No members".to_string());
1039                }
1040                if sync_age_hours > 24.0 {
1041                    issues.push(format!("Last sync {:.0}h ago", sync_age_hours));
1042                }
1043                if !self.goal_store.contains_key(&fed.goal_id) {
1044                    issues.push("Goal no longer exists".to_string());
1045                }
1046
1047                FederationHealthEntry {
1048                    federation_id: fed.id,
1049                    goal_id: fed.goal_id,
1050                    member_count: fed.members.len(),
1051                    sync_age_hours,
1052                    issues,
1053                }
1054            })
1055            .collect()
1056    }
1057
1058    /// Comprehensive health scan across all active goals.
1059    ///
1060    /// Single-pass check for: stalled goals, neglected goals, goals nearing
1061    /// deadline, blocked goals, and high-momentum goals (opportunities).
1062    pub fn goal_health_scan(&self) -> GoalHealthReport {
1063        let active_ids = self
1064            .indexes
1065            .goals_by_status
1066            .get(&GoalStatus::Active)
1067            .cloned()
1068            .unwrap_or_default();
1069        let blocked_ids = self
1070            .indexes
1071            .goals_by_status
1072            .get(&GoalStatus::Blocked)
1073            .cloned()
1074            .unwrap_or_default();
1075
1076        let now = Timestamp::now();
1077        let mut stalled = Vec::new();
1078        let mut neglected = Vec::new();
1079        let mut deadline_risk = Vec::new();
1080        let mut blocked = Vec::new();
1081        let mut thriving = Vec::new();
1082
1083        for id in active_ids.iter().chain(blocked_ids.iter()) {
1084            let Some(goal) = self.goal_store.get(id) else {
1085                continue;
1086            };
1087
1088            if goal.status == GoalStatus::Blocked {
1089                blocked.push(*id);
1090            }
1091
1092            if goal.physics.momentum == 0.0 && goal.progress.percentage < 1.0 {
1093                stalled.push(*id);
1094            }
1095
1096            if goal.feelings.neglect > 0.5 {
1097                neglected.push(*id);
1098            }
1099
1100            if let Some(deadline) = goal.deadline {
1101                let days_left = (deadline.0 - now.0) as f64 / (86_400.0 * 1e9);
1102                if days_left < 7.0 && goal.progress.percentage < 0.8 {
1103                    deadline_risk.push(*id);
1104                }
1105            }
1106
1107            if goal.physics.momentum > 0.7 && goal.progress.velocity > 0.0 {
1108                thriving.push(*id);
1109            }
1110        }
1111
1112        GoalHealthReport {
1113            total_active: active_ids.len(),
1114            total_blocked: blocked_ids.len(),
1115            stalled,
1116            neglected,
1117            deadline_risk,
1118            blocked,
1119            thriving,
1120        }
1121    }
1122
1123    // ═══════════════════════════════════════════════════════════════
1124    // Invention 12: Decision Consensus — Collective Wisdom
1125    // ═══════════════════════════════════════════════════════════════
1126
1127    /// Start a consensus process for a decision with multiple stakeholders.
1128    ///
1129    /// Validates the decision exists and is in Pending or Deliberating status.
1130    /// Creates a DecisionConsensus record and transitions the decision to Deliberating.
1131    pub fn start_consensus(
1132        &mut self,
1133        decision_id: DecisionId,
1134        participants: Vec<ConsensusParticipant>,
1135    ) -> crate::Result<DecisionConsensus> {
1136        let decision = self
1137            .decision_store
1138            .get(&decision_id)
1139            .ok_or(crate::Error::DecisionNotFound(decision_id))?;
1140
1141        if decision.status != DecisionStatus::Pending
1142            && decision.status != DecisionStatus::Deliberating
1143        {
1144            return Err(crate::Error::Validation(format!(
1145                "cannot start consensus on decision in state {:?}",
1146                decision.status
1147            )));
1148        }
1149
1150        if participants.is_empty() {
1151            return Err(crate::Error::CorruptedFile(
1152                "consensus requires at least one participant".to_string(),
1153            ));
1154        }
1155
1156        let now = Timestamp::now();
1157        let consensus = DecisionConsensus {
1158            decision_id,
1159            stakeholders: participants,
1160            deliberation: Vec::new(),
1161            synthesis: None,
1162            votes: HashMap::new(),
1163            alignment_score: 0.0,
1164            status: ConsensusStatus::Open,
1165            started_at: now,
1166            crystallized_at: None,
1167        };
1168
1169        // Transition decision to Deliberating
1170        if let Some(d) = self.decision_store.get_mut(&decision_id) {
1171            d.status = DecisionStatus::Deliberating;
1172        }
1173
1174        self.consensus_store.insert(decision_id, consensus.clone());
1175        self.mark_dirty();
1176        Ok(consensus)
1177    }
1178
1179    /// Add a deliberation round to an active consensus process.
1180    ///
1181    /// Each round collects statements from stakeholders and identifies common ground.
1182    /// The alignment score is recalculated after each round.
1183    pub fn add_deliberation_round(
1184        &mut self,
1185        decision_id: DecisionId,
1186        statements: Vec<ConsensusStatement>,
1187        common_ground: Vec<CommonGround>,
1188    ) -> crate::Result<DecisionConsensus> {
1189        let consensus = self
1190            .consensus_store
1191            .get_mut(&decision_id)
1192            .ok_or(crate::Error::DecisionNotFound(decision_id))?;
1193
1194        if consensus.status == ConsensusStatus::Crystallized
1195            || consensus.status == ConsensusStatus::Deadlocked
1196        {
1197            return Err(crate::Error::CorruptedFile(
1198                "consensus already finalized".to_string(),
1199            ));
1200        }
1201
1202        let round_number = consensus.deliberation.len() + 1;
1203
1204        // Calculate alignment delta from common ground strength
1205        let cg_strength: f64 = common_ground.iter().map(|cg| cg.strength).sum::<f64>()
1206            / common_ground.len().max(1) as f64;
1207        let concession_bonus: f64 = statements
1208            .iter()
1209            .map(|s| if s.concessions.is_empty() { 0.0 } else { 0.1 })
1210            .sum::<f64>()
1211            / statements.len().max(1) as f64;
1212        let alignment_delta = (cg_strength * 0.6 + concession_bonus * 0.4).clamp(0.0, 0.3);
1213
1214        let round = DeliberationRound {
1215            round_number,
1216            statements,
1217            alignment_delta,
1218            emerged_common_ground: common_ground,
1219            recorded_at: Timestamp::now(),
1220        };
1221
1222        consensus.deliberation.push(round);
1223        consensus.alignment_score = (consensus.alignment_score + alignment_delta).clamp(0.0, 1.0);
1224        consensus.status = ConsensusStatus::Deliberating;
1225        let result = consensus.clone();
1226        self.mark_dirty();
1227        Ok(result)
1228    }
1229
1230    /// Propose a synthesis that attempts to reconcile stakeholder positions.
1231    pub fn synthesize_consensus(
1232        &mut self,
1233        decision_id: DecisionId,
1234        proposal: String,
1235        incorporates_from: Vec<StakeholderId>,
1236        addresses_concerns: Vec<String>,
1237    ) -> crate::Result<DecisionConsensus> {
1238        let consensus = self
1239            .consensus_store
1240            .get_mut(&decision_id)
1241            .ok_or(crate::Error::DecisionNotFound(decision_id))?;
1242
1243        if consensus.status == ConsensusStatus::Crystallized {
1244            return Err(crate::Error::CorruptedFile(
1245                "consensus already crystallized".to_string(),
1246            ));
1247        }
1248
1249        // Confidence based on how many stakeholders are incorporated
1250        let total = consensus.stakeholders.len().max(1) as f64;
1251        let incorporated = incorporates_from.len() as f64;
1252        let base_confidence = (incorporated / total).clamp(0.0, 1.0);
1253
1254        let synthesis = Synthesis {
1255            proposal,
1256            incorporates_from,
1257            addresses_concerns,
1258            confidence: base_confidence * 0.7 + consensus.alignment_score * 0.3,
1259            proposed_at: Timestamp::now(),
1260        };
1261
1262        consensus.synthesis = Some(synthesis);
1263        consensus.status = ConsensusStatus::Synthesizing;
1264        let result = consensus.clone();
1265        self.mark_dirty();
1266        Ok(result)
1267    }
1268
1269    /// Record a stakeholder's vote on the current synthesis.
1270    pub fn record_consensus_vote(
1271        &mut self,
1272        decision_id: DecisionId,
1273        stakeholder_id: StakeholderId,
1274        vote: String,
1275    ) -> crate::Result<DecisionConsensus> {
1276        let consensus = self
1277            .consensus_store
1278            .get_mut(&decision_id)
1279            .ok_or(crate::Error::DecisionNotFound(decision_id))?;
1280
1281        if consensus.status == ConsensusStatus::Crystallized {
1282            return Err(crate::Error::CorruptedFile(
1283                "consensus already crystallized".to_string(),
1284            ));
1285        }
1286
1287        consensus.votes.insert(stakeholder_id, vote);
1288
1289        // Transition to Voting once first vote is recorded
1290        if consensus.status != ConsensusStatus::Voting {
1291            consensus.status = ConsensusStatus::Voting;
1292        }
1293
1294        // Recalculate alignment based on vote agreement
1295        let total_stakeholders = consensus.stakeholders.len().max(1);
1296        let voted_count = consensus.votes.len();
1297        if voted_count > 0 {
1298            let mut option_counts: HashMap<&String, usize> = HashMap::new();
1299            for v in consensus.votes.values() {
1300                *option_counts.entry(v).or_insert(0) += 1;
1301            }
1302            let max_agreement = *option_counts.values().max().unwrap_or(&0);
1303            let agreement_ratio = max_agreement as f64 / total_stakeholders as f64;
1304            consensus.alignment_score =
1305                (consensus.alignment_score * 0.5 + agreement_ratio * 0.5).clamp(0.0, 1.0);
1306        }
1307
1308        let result = consensus.clone();
1309        self.mark_dirty();
1310        Ok(result)
1311    }
1312
1313    /// Get the current status of a consensus process.
1314    pub fn get_consensus_status(
1315        &self,
1316        decision_id: DecisionId,
1317    ) -> crate::Result<&DecisionConsensus> {
1318        self.consensus_store
1319            .get(&decision_id)
1320            .ok_or(crate::Error::DecisionNotFound(decision_id))
1321    }
1322
1323    /// Crystallize a consensus decision once alignment is sufficient.
1324    ///
1325    /// If alignment_score >= 0.5 (or force=true), crystallizes the decision
1326    /// and records the consensus outcome. Otherwise returns Deadlocked.
1327    pub fn crystallize_consensus(
1328        &mut self,
1329        decision_id: DecisionId,
1330        chosen_path: PathId,
1331        force: bool,
1332    ) -> crate::Result<DecisionConsensus> {
1333        let alignment = self
1334            .consensus_store
1335            .get(&decision_id)
1336            .ok_or(crate::Error::DecisionNotFound(decision_id))?
1337            .alignment_score;
1338
1339        if !force && alignment < 0.5 {
1340            if let Some(c) = self.consensus_store.get_mut(&decision_id) {
1341                c.status = ConsensusStatus::Deadlocked;
1342            }
1343            self.mark_dirty();
1344            return Ok(self
1345                .consensus_store
1346                .get(&decision_id)
1347                .expect("consensus set in deadlock branch")
1348                .clone());
1349        }
1350
1351        // Crystallize the underlying decision
1352        let rationale = self
1353            .consensus_store
1354            .get(&decision_id)
1355            .and_then(|c| c.synthesis.as_ref())
1356            .map(|s| s.proposal.clone())
1357            .unwrap_or_else(|| "Consensus crystallization".to_string());
1358
1359        self.crystallize(
1360            decision_id,
1361            chosen_path,
1362            DecisionReasoning {
1363                rationale,
1364                confidence: alignment,
1365                ..Default::default()
1366            },
1367        )?;
1368
1369        // Mark consensus as crystallized
1370        if let Some(c) = self.consensus_store.get_mut(&decision_id) {
1371            c.status = ConsensusStatus::Crystallized;
1372            c.crystallized_at = Some(Timestamp::now());
1373        }
1374        self.mark_dirty();
1375        Ok(self
1376            .consensus_store
1377            .get(&decision_id)
1378            .expect("consensus set after crystallization")
1379            .clone())
1380    }
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385    use super::*;
1386    use crate::CreateGoalRequest;
1387
1388    fn make_request(title: &str) -> CreateGoalRequest {
1389        CreateGoalRequest {
1390            title: title.to_string(),
1391            description: format!("Description for {}", title),
1392            intention: format!("Intention for {}", title),
1393            ..Default::default()
1394        }
1395    }
1396
1397    #[test]
1398    fn test_batch_create_empty() {
1399        let mut engine = PlanningEngine::in_memory();
1400        let result = engine.create_goals_batch(vec![]);
1401        assert!(result.is_ok());
1402        assert_eq!(result.unwrap().len(), 0);
1403        assert_eq!(engine.goal_count(), 0);
1404    }
1405
1406    #[test]
1407    fn test_batch_create_single() {
1408        let mut engine = PlanningEngine::in_memory();
1409        let result = engine.create_goals_batch(vec![make_request("Alpha")]);
1410        assert!(result.is_ok());
1411        let goals = result.unwrap();
1412        assert_eq!(goals.len(), 1);
1413        assert_eq!(goals[0].title, "Alpha");
1414        assert_eq!(engine.goal_count(), 1);
1415    }
1416
1417    #[test]
1418    fn test_batch_create_multiple() {
1419        let mut engine = PlanningEngine::in_memory();
1420        let requests = vec![
1421            make_request("Goal A"),
1422            make_request("Goal B"),
1423            make_request("Goal C"),
1424            make_request("Goal D"),
1425            make_request("Goal E"),
1426        ];
1427
1428        let result = engine.create_goals_batch(requests);
1429        assert!(result.is_ok());
1430        let goals = result.unwrap();
1431        assert_eq!(goals.len(), 5);
1432        assert_eq!(engine.goal_count(), 5);
1433
1434        // Verify all goals are in draft status
1435        for g in &goals {
1436            assert_eq!(g.status, GoalStatus::Draft);
1437        }
1438
1439        // Verify indexes were rebuilt (root_goals should contain all 5)
1440        assert_eq!(engine.indexes.root_goals.len(), 5);
1441    }
1442
1443    #[test]
1444    fn test_batch_create_fail_fast_validation() {
1445        let mut engine = PlanningEngine::in_memory();
1446        let requests = vec![
1447            make_request("Valid Goal"),
1448            CreateGoalRequest {
1449                title: "".to_string(), // invalid: empty title
1450                description: "desc".to_string(),
1451                intention: "intent".to_string(),
1452                ..Default::default()
1453            },
1454            make_request("Another Valid Goal"),
1455        ];
1456
1457        let result = engine.create_goals_batch(requests);
1458        assert!(result.is_err());
1459        // Fail-fast: NO goals should have been inserted
1460        assert_eq!(engine.goal_count(), 0);
1461    }
1462
1463    #[test]
1464    fn test_batch_create_with_parent() {
1465        let mut engine = PlanningEngine::in_memory();
1466
1467        // Create a parent first
1468        let parent = engine.create_goal(make_request("Parent")).unwrap();
1469
1470        // Batch create children
1471        let mut child_req = make_request("Child");
1472        child_req.parent = Some(parent.id);
1473        let result = engine.create_goals_batch(vec![child_req]);
1474        assert!(result.is_ok());
1475
1476        // Verify parent has the child linked
1477        let updated_parent = engine.goal_store.get(&parent.id).unwrap();
1478        assert_eq!(updated_parent.children.len(), 1);
1479    }
1480
1481    #[test]
1482    fn test_batch_indexes_rebuilt_once() {
1483        let mut engine = PlanningEngine::in_memory();
1484        let requests: Vec<CreateGoalRequest> = (0..10)
1485            .map(|i| make_request(&format!("Batch Goal {}", i)))
1486            .collect();
1487
1488        let result = engine.create_goals_batch(requests);
1489        assert!(result.is_ok());
1490        assert_eq!(engine.goal_count(), 10);
1491
1492        // All 10 should be in root_goals (no parents)
1493        assert_eq!(engine.indexes.root_goals.len(), 10);
1494        // All 10 should be in Draft status index
1495        let draft_count = engine
1496            .indexes
1497            .goals_by_status
1498            .get(&GoalStatus::Draft)
1499            .map(|v| v.len())
1500            .unwrap_or(0);
1501        assert_eq!(draft_count, 10);
1502    }
1503
1504    #[test]
1505    fn test_singularity_parallel_empty() {
1506        let engine = PlanningEngine::in_memory();
1507        let singularity = engine.calculate_singularity_parallel();
1508        assert!(singularity.goal_positions.is_empty());
1509        assert_eq!(singularity.center.confidence, 0.0);
1510    }
1511
1512    #[test]
1513    fn test_blockers_parallel_empty() {
1514        let engine = PlanningEngine::in_memory();
1515        let prophecies = engine.scan_blockers_parallel();
1516        assert!(prophecies.is_empty());
1517    }
1518
1519    #[test]
1520    fn test_echoes_parallel_empty() {
1521        let engine = PlanningEngine::in_memory();
1522        let echoes = engine.progress_echoes_parallel();
1523        assert!(echoes.is_empty());
1524    }
1525
1526    #[test]
1527    fn test_singularity_parallel_with_goals() {
1528        let mut engine = PlanningEngine::in_memory();
1529        let goal = engine.create_goal(make_request("Active Goal")).unwrap();
1530        engine.activate_goal(goal.id).unwrap();
1531
1532        let singularity = engine.calculate_singularity_parallel();
1533        // Should have at least one position for our active goal
1534        assert!(!singularity.goal_positions.is_empty());
1535    }
1536
1537    #[test]
1538    fn test_blockers_parallel_with_active_goal() {
1539        let mut engine = PlanningEngine::in_memory();
1540
1541        // Create a goal with a dependency (will predict blockers)
1542        let dep = engine.create_goal(make_request("Dependency")).unwrap();
1543        let mut req = make_request("Main Goal");
1544        req.dependencies = Some(vec![dep.id]);
1545        let goal = engine.create_goal(req).unwrap();
1546        engine.activate_goal(goal.id).unwrap();
1547
1548        let prophecies = engine.scan_blockers_parallel();
1549        // Should predict at least one blocker (dependency not completed)
1550        assert!(!prophecies.is_empty());
1551    }
1552
1553    #[test]
1554    fn test_blockers_parallel_sorted_by_confidence() {
1555        let mut engine = PlanningEngine::in_memory();
1556
1557        // Create multiple goals with dependencies to generate multiple prophecies
1558        let dep1 = engine.create_goal(make_request("Dep 1")).unwrap();
1559        let dep2 = engine.create_goal(make_request("Dep 2")).unwrap();
1560
1561        let mut req = make_request("Goal with deps");
1562        req.dependencies = Some(vec![dep1.id, dep2.id]);
1563        let goal = engine.create_goal(req).unwrap();
1564        engine.activate_goal(goal.id).unwrap();
1565
1566        let prophecies = engine.scan_blockers_parallel();
1567        // Verify sorted by confidence descending
1568        for w in prophecies.windows(2) {
1569            assert!(w[0].prediction_confidence >= w[1].prediction_confidence);
1570        }
1571    }
1572
1573    #[test]
1574    fn test_batch_create_preserves_priorities() {
1575        let mut engine = PlanningEngine::in_memory();
1576        let mut req_high = make_request("High Priority");
1577        req_high.priority = Some(Priority::High);
1578        let mut req_low = make_request("Low Priority");
1579        req_low.priority = Some(Priority::Low);
1580
1581        let goals = engine.create_goals_batch(vec![req_high, req_low]).unwrap();
1582        assert_eq!(goals[0].priority, Priority::High);
1583        assert_eq!(goals[1].priority, Priority::Low);
1584
1585        // Verify priority indexes
1586        assert!(engine
1587            .indexes
1588            .goals_by_priority
1589            .get(&Priority::High)
1590            .unwrap()
1591            .contains(&goals[0].id));
1592        assert!(engine
1593            .indexes
1594            .goals_by_priority
1595            .get(&Priority::Low)
1596            .unwrap()
1597            .contains(&goals[1].id));
1598    }
1599
1600    // ═══════════════════════════════════════════════════════════════
1601    // Batch activate goals
1602    // ═══════════════════════════════════════════════════════════════
1603
1604    #[test]
1605    fn test_batch_activate_empty() {
1606        let mut engine = PlanningEngine::in_memory();
1607        let result = engine.batch_activate_goals(&[]);
1608        assert!(result.is_ok());
1609        assert!(result.unwrap().is_empty());
1610    }
1611
1612    #[test]
1613    fn test_batch_activate_valid() {
1614        let mut engine = PlanningEngine::in_memory();
1615        let g1 = engine.create_goal(make_request("Goal 1")).unwrap();
1616        let g2 = engine.create_goal(make_request("Goal 2")).unwrap();
1617
1618        let result = engine.batch_activate_goals(&[g1.id, g2.id]);
1619        assert!(result.is_ok());
1620        let goals = result.unwrap();
1621        assert_eq!(goals.len(), 2);
1622        for g in &goals {
1623            assert_eq!(g.status, GoalStatus::Active);
1624        }
1625    }
1626
1627    #[test]
1628    fn test_batch_activate_fail_fast_on_invalid_state() {
1629        let mut engine = PlanningEngine::in_memory();
1630        let g1 = engine.create_goal(make_request("Goal 1")).unwrap();
1631        let g2 = engine.create_goal(make_request("Goal 2")).unwrap();
1632        // Activate g2 first so it can't be activated again
1633        engine.activate_goal(g2.id).unwrap();
1634
1635        let result = engine.batch_activate_goals(&[g1.id, g2.id]);
1636        assert!(result.is_err());
1637        // g1 should NOT have been activated (fail-fast)
1638        let g1_check = engine.goal_store.get(&g1.id).unwrap();
1639        assert_eq!(g1_check.status, GoalStatus::Draft);
1640    }
1641
1642    // ═══════════════════════════════════════════════════════════════
1643    // Batch progress goals
1644    // ═══════════════════════════════════════════════════════════════
1645
1646    #[test]
1647    fn test_batch_progress_empty() {
1648        let mut engine = PlanningEngine::in_memory();
1649        let result = engine.batch_progress_goals(vec![]);
1650        assert!(result.is_ok());
1651        assert!(result.unwrap().is_empty());
1652    }
1653
1654    #[test]
1655    fn test_batch_progress_valid() {
1656        let mut engine = PlanningEngine::in_memory();
1657        let g1 = engine.create_goal(make_request("Goal 1")).unwrap();
1658        engine.activate_goal(g1.id).unwrap();
1659        let g2 = engine.create_goal(make_request("Goal 2")).unwrap();
1660        engine.activate_goal(g2.id).unwrap();
1661
1662        let updates = vec![
1663            (g1.id, 0.5, Some("halfway".to_string())),
1664            (g2.id, 0.3, None),
1665        ];
1666        let result = engine.batch_progress_goals(updates);
1667        assert!(result.is_ok());
1668        let goals = result.unwrap();
1669        assert_eq!(goals.len(), 2);
1670        assert!((goals[0].progress.percentage - 0.5).abs() < 0.01);
1671        assert!((goals[1].progress.percentage - 0.3).abs() < 0.01);
1672    }
1673
1674    #[test]
1675    fn test_batch_progress_completion() {
1676        let mut engine = PlanningEngine::in_memory();
1677        let g = engine.create_goal(make_request("Finish Me")).unwrap();
1678        engine.activate_goal(g.id).unwrap();
1679
1680        let result = engine.batch_progress_goals(vec![(g.id, 1.0, Some("done".to_string()))]);
1681        assert!(result.is_ok());
1682        let goals = result.unwrap();
1683        // Batch progress sets percentage but doesn't auto-complete (optimized path)
1684        assert!((goals[0].progress.percentage - 1.0).abs() < 0.01);
1685    }
1686
1687    // ═══════════════════════════════════════════════════════════════
1688    // Batch create decisions
1689    // ═══════════════════════════════════════════════════════════════
1690
1691    #[test]
1692    fn test_batch_create_decisions_empty() {
1693        let mut engine = PlanningEngine::in_memory();
1694        let result = engine.batch_create_decisions(vec![]);
1695        assert!(result.is_ok());
1696        assert!(result.unwrap().is_empty());
1697    }
1698
1699    #[test]
1700    fn test_batch_create_decisions_valid() {
1701        let mut engine = PlanningEngine::in_memory();
1702        let requests = vec![
1703            CreateDecisionRequest {
1704                question: "What framework?".to_string(),
1705                ..Default::default()
1706            },
1707            CreateDecisionRequest {
1708                question: "What database?".to_string(),
1709                context: Some("Need ACID compliance".to_string()),
1710                ..Default::default()
1711            },
1712        ];
1713        let result = engine.batch_create_decisions(requests);
1714        assert!(result.is_ok());
1715        let decisions = result.unwrap();
1716        assert_eq!(decisions.len(), 2);
1717        assert_eq!(decisions[0].question.question, "What framework?");
1718        assert_eq!(decisions[1].question.question, "What database?");
1719    }
1720
1721    #[test]
1722    fn test_batch_create_decisions_with_goal_link() {
1723        let mut engine = PlanningEngine::in_memory();
1724        let goal = engine.create_goal(make_request("Main Goal")).unwrap();
1725        let requests = vec![CreateDecisionRequest {
1726            question: "How to implement?".to_string(),
1727            goals: Some(vec![goal.id]),
1728            ..Default::default()
1729        }];
1730        let result = engine.batch_create_decisions(requests);
1731        assert!(result.is_ok());
1732        let decisions = result.unwrap();
1733        assert_eq!(decisions.len(), 1);
1734    }
1735
1736    // ═══════════════════════════════════════════════════════════════
1737    // Batch create commitments
1738    // ═══════════════════════════════════════════════════════════════
1739
1740    #[test]
1741    fn test_batch_create_commitments_empty() {
1742        let mut engine = PlanningEngine::in_memory();
1743        let result = engine.batch_create_commitments(vec![]);
1744        assert!(result.is_ok());
1745        assert!(result.unwrap().is_empty());
1746    }
1747
1748    #[test]
1749    fn test_batch_create_commitments_valid() {
1750        let mut engine = PlanningEngine::in_memory();
1751        let requests = vec![
1752            CreateCommitmentRequest {
1753                promise: Promise {
1754                    description: "Deliver MVP".to_string(),
1755                    deliverables: vec!["working app".to_string()],
1756                    conditions: vec![],
1757                },
1758                stakeholder: Stakeholder {
1759                    name: "Alice".to_string(),
1760                    ..Default::default()
1761                },
1762                ..Default::default()
1763            },
1764            CreateCommitmentRequest {
1765                promise: Promise {
1766                    description: "Write docs".to_string(),
1767                    deliverables: vec!["README".to_string()],
1768                    conditions: vec![],
1769                },
1770                stakeholder: Stakeholder {
1771                    name: "Bob".to_string(),
1772                    ..Default::default()
1773                },
1774                ..Default::default()
1775            },
1776        ];
1777        let result = engine.batch_create_commitments(requests);
1778        assert!(result.is_ok());
1779        let commitments = result.unwrap();
1780        assert_eq!(commitments.len(), 2);
1781        assert_eq!(commitments[0].promise.description, "Deliver MVP");
1782        assert_eq!(commitments[1].promise.description, "Write docs");
1783    }
1784
1785    // ═══════════════════════════════════════════════════════════════
1786    // Batch fulfill commitments
1787    // ═══════════════════════════════════════════════════════════════
1788
1789    #[test]
1790    fn test_batch_fulfill_commitments_empty() {
1791        let mut engine = PlanningEngine::in_memory();
1792        let result = engine.batch_fulfill_commitments(vec![]);
1793        assert!(result.is_ok());
1794        assert!(result.unwrap().is_empty());
1795    }
1796
1797    #[test]
1798    fn test_batch_fulfill_commitments_valid() {
1799        let mut engine = PlanningEngine::in_memory();
1800        let c = engine
1801            .create_commitment(CreateCommitmentRequest {
1802                promise: Promise {
1803                    description: "Ship it".to_string(),
1804                    deliverables: vec!["release".to_string()],
1805                    conditions: vec![],
1806                },
1807                stakeholder: Stakeholder {
1808                    name: "Team".to_string(),
1809                    ..Default::default()
1810                },
1811                ..Default::default()
1812            })
1813            .unwrap();
1814
1815        let result = engine.batch_fulfill_commitments(vec![(c.id, "Shipped v1.0".to_string())]);
1816        assert!(result.is_ok());
1817        let commitments = result.unwrap();
1818        assert_eq!(commitments.len(), 1);
1819        assert_eq!(commitments[0].status, CommitmentStatus::Fulfilled);
1820    }
1821
1822    // ═══════════════════════════════════════════════════════════════
1823    // Momentum report (indexed)
1824    // ═══════════════════════════════════════════════════════════════
1825
1826    #[test]
1827    fn test_momentum_report_indexed_empty() {
1828        let engine = PlanningEngine::in_memory();
1829        let report = engine.momentum_report_indexed();
1830        assert_eq!(report.total_goals, 0);
1831        assert!((report.average_momentum - 0.0).abs() < 0.01);
1832        assert!(report.top_momentum.is_empty());
1833    }
1834
1835    #[test]
1836    fn test_momentum_report_indexed_with_active_goals() {
1837        let mut engine = PlanningEngine::in_memory();
1838        let g = engine.create_goal(make_request("Active Goal")).unwrap();
1839        engine.activate_goal(g.id).unwrap();
1840        engine
1841            .progress_goal(g.id, 0.3, Some("started".to_string()))
1842            .unwrap();
1843
1844        let report = engine.momentum_report_indexed();
1845        assert!(report.total_goals >= 1);
1846        assert!(!report.top_momentum.is_empty());
1847        // Check the entry has expected fields
1848        let entry = &report.top_momentum[0];
1849        assert_eq!(entry.goal_id, g.id);
1850        assert!(!entry.title.is_empty());
1851    }
1852
1853    // ═══════════════════════════════════════════════════════════════
1854    // Gravity field (indexed)
1855    // ═══════════════════════════════════════════════════════════════
1856
1857    #[test]
1858    fn test_gravity_field_indexed_empty() {
1859        let engine = PlanningEngine::in_memory();
1860        let field = engine.gravity_field_indexed();
1861        assert_eq!(field.total_goals, 0);
1862        assert!(field.wells.is_empty());
1863    }
1864
1865    #[test]
1866    fn test_gravity_field_indexed_with_active_goals() {
1867        let mut engine = PlanningEngine::in_memory();
1868        let mut req = make_request("Important Goal");
1869        req.priority = Some(Priority::Critical);
1870        let g = engine.create_goal(req).unwrap();
1871        engine.activate_goal(g.id).unwrap();
1872
1873        let field = engine.gravity_field_indexed();
1874        assert!(field.total_goals >= 1);
1875        assert!(!field.wells.is_empty());
1876        // Critical priority should produce high gravity
1877        assert!(field.wells[0].gravity > 0.0);
1878    }
1879
1880    // ═══════════════════════════════════════════════════════════════
1881    // At-risk commitments scan
1882    // ═══════════════════════════════════════════════════════════════
1883
1884    #[test]
1885    fn test_at_risk_commitments_scan_empty() {
1886        let engine = PlanningEngine::in_memory();
1887        let results = engine.at_risk_commitments_scan();
1888        assert!(results.is_empty());
1889    }
1890
1891    #[test]
1892    fn test_at_risk_commitments_scan_with_commitment() {
1893        let mut engine = PlanningEngine::in_memory();
1894        // Create a commitment with a past due date
1895        let c = engine
1896            .create_commitment(CreateCommitmentRequest {
1897                promise: Promise {
1898                    description: "Overdue task".to_string(),
1899                    deliverables: vec!["something".to_string()],
1900                    conditions: vec![],
1901                },
1902                stakeholder: Stakeholder {
1903                    name: "Boss".to_string(),
1904                    ..Default::default()
1905                },
1906                due: Some(Timestamp(0)), // epoch = long past
1907                ..Default::default()
1908            })
1909            .unwrap();
1910
1911        let results = engine.at_risk_commitments_scan();
1912        // Should flag the overdue commitment
1913        let found = results.iter().any(|(id, _)| *id == c.id);
1914        assert!(found, "Overdue commitment should be flagged as at-risk");
1915    }
1916
1917    // ═══════════════════════════════════════════════════════════════
1918    // Metamorphosis scan
1919    // ═══════════════════════════════════════════════════════════════
1920
1921    #[test]
1922    fn test_metamorphosis_scan_empty() {
1923        let engine = PlanningEngine::in_memory();
1924        let signals = engine.metamorphosis_scan();
1925        assert!(signals.is_empty());
1926    }
1927
1928    // ═══════════════════════════════════════════════════════════════
1929    // Progress forecast
1930    // ═══════════════════════════════════════════════════════════════
1931
1932    #[test]
1933    fn test_progress_forecast_batch_empty() {
1934        let engine = PlanningEngine::in_memory();
1935        let forecasts = engine.progress_forecast_batch(&[]);
1936        assert!(forecasts.is_empty());
1937    }
1938
1939    #[test]
1940    fn test_progress_forecast_batch_with_active_goals() {
1941        let mut engine = PlanningEngine::in_memory();
1942        let g = engine.create_goal(make_request("Forecast Me")).unwrap();
1943        engine.activate_goal(g.id).unwrap();
1944        engine
1945            .progress_goal(g.id, 0.4, Some("progressing".to_string()))
1946            .unwrap();
1947
1948        let forecasts = engine.progress_forecast_batch(&[g.id]);
1949        assert_eq!(forecasts.len(), 1);
1950        assert_eq!(forecasts[0].goal_id, g.id);
1951    }
1952
1953    #[test]
1954    fn test_progress_forecast_all_active_empty() {
1955        let engine = PlanningEngine::in_memory();
1956        let forecasts = engine.progress_forecast_all_active();
1957        assert!(forecasts.is_empty());
1958    }
1959
1960    #[test]
1961    fn test_progress_forecast_all_active_with_goals() {
1962        let mut engine = PlanningEngine::in_memory();
1963        let g1 = engine.create_goal(make_request("Active 1")).unwrap();
1964        engine.activate_goal(g1.id).unwrap();
1965        let g2 = engine.create_goal(make_request("Active 2")).unwrap();
1966        engine.activate_goal(g2.id).unwrap();
1967        // Draft goal should NOT appear
1968        let _draft = engine.create_goal(make_request("Draft")).unwrap();
1969
1970        let forecasts = engine.progress_forecast_all_active();
1971        assert_eq!(forecasts.len(), 2);
1972    }
1973
1974    // ═══════════════════════════════════════════════════════════════
1975    // Dream goals batch
1976    // ═══════════════════════════════════════════════════════════════
1977
1978    #[test]
1979    fn test_dream_goals_batch_empty() {
1980        let mut engine = PlanningEngine::in_memory();
1981        let results = engine.dream_goals_batch(&[]);
1982        assert!(results.is_empty());
1983    }
1984
1985    #[test]
1986    fn test_dream_goals_batch_valid() {
1987        let mut engine = PlanningEngine::in_memory();
1988        let g = engine.create_goal(make_request("Dream Goal")).unwrap();
1989        engine.activate_goal(g.id).unwrap();
1990
1991        let results = engine.dream_goals_batch(&[g.id]);
1992        assert_eq!(results.len(), 1);
1993        assert!(results[0].is_ok());
1994    }
1995
1996    // ═══════════════════════════════════════════════════════════════
1997    // Federation health scan
1998    // ═══════════════════════════════════════════════════════════════
1999
2000    #[test]
2001    fn test_federation_health_scan_empty() {
2002        let engine = PlanningEngine::in_memory();
2003        let results = engine.federation_health_scan();
2004        assert!(results.is_empty());
2005    }
2006
2007    // ═══════════════════════════════════════════════════════════════
2008    // Goal health scan
2009    // ═══════════════════════════════════════════════════════════════
2010
2011    #[test]
2012    fn test_goal_health_scan_empty() {
2013        let engine = PlanningEngine::in_memory();
2014        let report = engine.goal_health_scan();
2015        assert_eq!(report.total_active, 0);
2016        assert_eq!(report.total_blocked, 0);
2017        assert!(report.stalled.is_empty());
2018        assert!(report.thriving.is_empty());
2019    }
2020
2021    #[test]
2022    fn test_goal_health_scan_with_active_goals() {
2023        let mut engine = PlanningEngine::in_memory();
2024        // Active goal with progress
2025        let g1 = engine.create_goal(make_request("Active Goal")).unwrap();
2026        engine.activate_goal(g1.id).unwrap();
2027        engine
2028            .progress_goal(g1.id, 0.5, Some("going well".to_string()))
2029            .unwrap();
2030
2031        // Another active goal (no progress = stalled candidate)
2032        let g2 = engine.create_goal(make_request("Stalled Goal")).unwrap();
2033        engine.activate_goal(g2.id).unwrap();
2034
2035        let report = engine.goal_health_scan();
2036        assert!(report.total_active >= 2);
2037        // g2 has zero momentum and incomplete progress -> stalled
2038        assert!(report.stalled.contains(&g2.id));
2039    }
2040
2041    // ═══════════════════════════════════════════════════════════════
2042    // Invention 12: Decision Consensus
2043    // ═══════════════════════════════════════════════════════════════
2044
2045    fn make_decision(engine: &mut PlanningEngine, q: &str) -> Decision {
2046        engine
2047            .create_decision(CreateDecisionRequest {
2048                question: q.to_string(),
2049                ..Default::default()
2050            })
2051            .unwrap()
2052    }
2053
2054    fn make_participants() -> Vec<ConsensusParticipant> {
2055        vec![
2056            ConsensusParticipant {
2057                id: StakeholderId(uuid::Uuid::new_v4()),
2058                role: "Engineering".to_string(),
2059                initial_position: "Use Rust".to_string(),
2060                concerns: vec!["learning curve".to_string()],
2061                requirements: vec!["performance".to_string()],
2062                flexibility: 0.6,
2063            },
2064            ConsensusParticipant {
2065                id: StakeholderId(uuid::Uuid::new_v4()),
2066                role: "Product".to_string(),
2067                initial_position: "Use Go".to_string(),
2068                concerns: vec!["time to market".to_string()],
2069                requirements: vec!["quick iteration".to_string()],
2070                flexibility: 0.7,
2071            },
2072        ]
2073    }
2074
2075    #[test]
2076    fn test_start_consensus() {
2077        let mut engine = PlanningEngine::in_memory();
2078        let d = make_decision(&mut engine, "Which language?");
2079        let participants = make_participants();
2080        let result = engine.start_consensus(d.id, participants);
2081        assert!(result.is_ok());
2082        let consensus = result.unwrap();
2083        assert_eq!(consensus.decision_id, d.id);
2084        assert_eq!(consensus.stakeholders.len(), 2);
2085        assert_eq!(consensus.status, ConsensusStatus::Open);
2086        assert!((consensus.alignment_score - 0.0).abs() < f64::EPSILON);
2087        // Decision should transition to Deliberating
2088        let decision = engine.decision_store.get(&d.id).unwrap();
2089        assert_eq!(decision.status, DecisionStatus::Deliberating);
2090    }
2091
2092    #[test]
2093    fn test_start_consensus_already_crystallized() {
2094        let mut engine = PlanningEngine::in_memory();
2095        let d = make_decision(&mut engine, "Already decided");
2096        let path = DecisionPath {
2097            name: "Option A".to_string(),
2098            description: "The only option".to_string(),
2099            ..Default::default()
2100        };
2101        engine.add_option(d.id, path.clone()).unwrap();
2102        engine
2103            .crystallize(d.id, path.id, DecisionReasoning::default())
2104            .unwrap();
2105        let result = engine.start_consensus(d.id, make_participants());
2106        assert!(result.is_err());
2107    }
2108
2109    #[test]
2110    fn test_start_consensus_empty_participants() {
2111        let mut engine = PlanningEngine::in_memory();
2112        let d = make_decision(&mut engine, "No one to ask");
2113        let result = engine.start_consensus(d.id, vec![]);
2114        assert!(result.is_err());
2115    }
2116
2117    #[test]
2118    fn test_add_deliberation_round() {
2119        let mut engine = PlanningEngine::in_memory();
2120        let d = make_decision(&mut engine, "Architecture choice");
2121        let participants = make_participants();
2122        let p0 = participants[0].id;
2123        let p1 = participants[1].id;
2124        engine.start_consensus(d.id, participants).unwrap();
2125
2126        let statements = vec![
2127            ConsensusStatement {
2128                stakeholder_id: p0,
2129                position: "Rust for safety".to_string(),
2130                supporting_arguments: vec!["memory safety".to_string()],
2131                concessions: vec!["willing to consider Go for tooling".to_string()],
2132            },
2133            ConsensusStatement {
2134                stakeholder_id: p1,
2135                position: "Go for speed".to_string(),
2136                supporting_arguments: vec!["faster dev cycle".to_string()],
2137                concessions: vec![],
2138            },
2139        ];
2140        let common_ground = vec![CommonGround {
2141            description: "Both want reliability".to_string(),
2142            agreed_by: vec![p0, p1],
2143            strength: 0.8,
2144        }];
2145
2146        let result = engine.add_deliberation_round(d.id, statements, common_ground);
2147        assert!(result.is_ok());
2148        let consensus = result.unwrap();
2149        assert_eq!(consensus.deliberation.len(), 1);
2150        assert_eq!(consensus.status, ConsensusStatus::Deliberating);
2151        assert!(consensus.alignment_score > 0.0);
2152    }
2153
2154    #[test]
2155    fn test_synthesize_consensus() {
2156        let mut engine = PlanningEngine::in_memory();
2157        let d = make_decision(&mut engine, "Tech choice");
2158        let participants = make_participants();
2159        let p0 = participants[0].id;
2160        let p1 = participants[1].id;
2161        engine.start_consensus(d.id, participants).unwrap();
2162
2163        // Add a round first
2164        engine
2165            .add_deliberation_round(
2166                d.id,
2167                vec![ConsensusStatement {
2168                    stakeholder_id: p0,
2169                    position: "Propose hybrid".to_string(),
2170                    supporting_arguments: vec![],
2171                    concessions: vec!["open to alternatives".to_string()],
2172                }],
2173                vec![CommonGround {
2174                    description: "Performance matters".to_string(),
2175                    agreed_by: vec![p0, p1],
2176                    strength: 0.7,
2177                }],
2178            )
2179            .unwrap();
2180
2181        let result = engine.synthesize_consensus(
2182            d.id,
2183            "Use Rust for core, Go for tooling".to_string(),
2184            vec![p0, p1],
2185            vec!["performance".to_string(), "dev speed".to_string()],
2186        );
2187        assert!(result.is_ok());
2188        let consensus = result.unwrap();
2189        assert_eq!(consensus.status, ConsensusStatus::Synthesizing);
2190        assert!(consensus.synthesis.is_some());
2191        let syn = consensus.synthesis.unwrap();
2192        assert_eq!(syn.incorporates_from.len(), 2);
2193        assert!(syn.confidence > 0.0);
2194    }
2195
2196    #[test]
2197    fn test_record_consensus_vote() {
2198        let mut engine = PlanningEngine::in_memory();
2199        let d = make_decision(&mut engine, "Vote on approach");
2200        let participants = make_participants();
2201        let p0 = participants[0].id;
2202        let p1 = participants[1].id;
2203        engine.start_consensus(d.id, participants).unwrap();
2204
2205        let result = engine.record_consensus_vote(d.id, p0, "approve".to_string());
2206        assert!(result.is_ok());
2207        let consensus = result.unwrap();
2208        assert_eq!(consensus.status, ConsensusStatus::Voting);
2209        assert_eq!(consensus.votes.len(), 1);
2210
2211        // Second vote with same option should increase alignment
2212        let result2 = engine.record_consensus_vote(d.id, p1, "approve".to_string());
2213        assert!(result2.is_ok());
2214        let consensus2 = result2.unwrap();
2215        assert_eq!(consensus2.votes.len(), 2);
2216        // Both voted "approve" so alignment should be high
2217        assert!(consensus2.alignment_score > 0.3);
2218    }
2219
2220    #[test]
2221    fn test_get_consensus_status() {
2222        let mut engine = PlanningEngine::in_memory();
2223        let d = make_decision(&mut engine, "Status query test");
2224        engine.start_consensus(d.id, make_participants()).unwrap();
2225
2226        let result = engine.get_consensus_status(d.id);
2227        assert!(result.is_ok());
2228        assert_eq!(result.unwrap().stakeholders.len(), 2);
2229    }
2230
2231    #[test]
2232    fn test_get_consensus_status_not_found() {
2233        let engine = PlanningEngine::in_memory();
2234        let result = engine.get_consensus_status(DecisionId(uuid::Uuid::new_v4()));
2235        assert!(result.is_err());
2236    }
2237
2238    #[test]
2239    fn test_crystallize_consensus_sufficient_alignment() {
2240        let mut engine = PlanningEngine::in_memory();
2241        let d = make_decision(&mut engine, "Crystallize me");
2242        let participants = make_participants();
2243        let p0 = participants[0].id;
2244        let p1 = participants[1].id;
2245        engine.start_consensus(d.id, participants).unwrap();
2246
2247        // Add a path so crystallize can find it
2248        let path = DecisionPath {
2249            name: "Hybrid approach".to_string(),
2250            description: "Best of both".to_string(),
2251            ..Default::default()
2252        };
2253        let path_id = path.id;
2254        engine.add_option(d.id, path).unwrap();
2255
2256        // Build alignment through voting
2257        engine
2258            .record_consensus_vote(d.id, p0, "approve".to_string())
2259            .unwrap();
2260        engine
2261            .record_consensus_vote(d.id, p1, "approve".to_string())
2262            .unwrap();
2263
2264        let result = engine.crystallize_consensus(d.id, path_id, false);
2265        assert!(result.is_ok());
2266        let consensus = result.unwrap();
2267        assert_eq!(consensus.status, ConsensusStatus::Crystallized);
2268        assert!(consensus.crystallized_at.is_some());
2269        // Underlying decision should be crystallized too
2270        let decision = engine.decision_store.get(&d.id).unwrap();
2271        assert_eq!(decision.status, DecisionStatus::Crystallized);
2272    }
2273
2274    #[test]
2275    fn test_crystallize_consensus_low_alignment_deadlocks() {
2276        let mut engine = PlanningEngine::in_memory();
2277        let d = make_decision(&mut engine, "Deadlock me");
2278        engine.start_consensus(d.id, make_participants()).unwrap();
2279
2280        let path = DecisionPath {
2281            name: "Option A".to_string(),
2282            description: "Untested".to_string(),
2283            ..Default::default()
2284        };
2285        let path_id = path.id;
2286        engine.add_option(d.id, path).unwrap();
2287
2288        // No votes, alignment = 0 -> should deadlock
2289        let result = engine.crystallize_consensus(d.id, path_id, false);
2290        assert!(result.is_ok());
2291        let consensus = result.unwrap();
2292        assert_eq!(consensus.status, ConsensusStatus::Deadlocked);
2293    }
2294
2295    #[test]
2296    fn test_crystallize_consensus_force() {
2297        let mut engine = PlanningEngine::in_memory();
2298        let d = make_decision(&mut engine, "Force it");
2299        engine.start_consensus(d.id, make_participants()).unwrap();
2300
2301        let path = DecisionPath {
2302            name: "Emergency choice".to_string(),
2303            description: "No time to deliberate".to_string(),
2304            ..Default::default()
2305        };
2306        let path_id = path.id;
2307        engine.add_option(d.id, path).unwrap();
2308
2309        // Force crystallize even with 0 alignment
2310        let result = engine.crystallize_consensus(d.id, path_id, true);
2311        assert!(result.is_ok());
2312        let consensus = result.unwrap();
2313        assert_eq!(consensus.status, ConsensusStatus::Crystallized);
2314    }
2315
2316    #[test]
2317    fn test_consensus_full_lifecycle() {
2318        let mut engine = PlanningEngine::in_memory();
2319        let d = make_decision(&mut engine, "Full lifecycle test");
2320        let participants = make_participants();
2321        let p0 = participants[0].id;
2322        let p1 = participants[1].id;
2323
2324        // 1. Start
2325        let c = engine.start_consensus(d.id, participants).unwrap();
2326        assert_eq!(c.status, ConsensusStatus::Open);
2327
2328        // 2. Deliberate
2329        let c = engine
2330            .add_deliberation_round(
2331                d.id,
2332                vec![
2333                    ConsensusStatement {
2334                        stakeholder_id: p0,
2335                        position: "Prefer A".to_string(),
2336                        supporting_arguments: vec!["fast".to_string()],
2337                        concessions: vec!["B is fine too".to_string()],
2338                    },
2339                    ConsensusStatement {
2340                        stakeholder_id: p1,
2341                        position: "Prefer B".to_string(),
2342                        supporting_arguments: vec!["safe".to_string()],
2343                        concessions: vec!["A has merit".to_string()],
2344                    },
2345                ],
2346                vec![CommonGround {
2347                    description: "Both want quality".to_string(),
2348                    agreed_by: vec![p0, p1],
2349                    strength: 0.9,
2350                }],
2351            )
2352            .unwrap();
2353        assert_eq!(c.status, ConsensusStatus::Deliberating);
2354
2355        // 3. Synthesize
2356        let c = engine
2357            .synthesize_consensus(
2358                d.id,
2359                "Combine A's speed with B's safety".to_string(),
2360                vec![p0, p1],
2361                vec!["quality".to_string()],
2362            )
2363            .unwrap();
2364        assert_eq!(c.status, ConsensusStatus::Synthesizing);
2365
2366        // 4. Vote
2367        engine
2368            .record_consensus_vote(d.id, p0, "approve".to_string())
2369            .unwrap();
2370        let c = engine
2371            .record_consensus_vote(d.id, p1, "approve".to_string())
2372            .unwrap();
2373        assert_eq!(c.status, ConsensusStatus::Voting);
2374
2375        // 5. Crystallize
2376        let path = DecisionPath {
2377            name: "Combined approach".to_string(),
2378            description: "A+B synthesis".to_string(),
2379            ..Default::default()
2380        };
2381        let path_id = path.id;
2382        engine.add_option(d.id, path).unwrap();
2383
2384        let c = engine.crystallize_consensus(d.id, path_id, false).unwrap();
2385        assert_eq!(c.status, ConsensusStatus::Crystallized);
2386        assert!(c.crystallized_at.is_some());
2387    }
2388}