1use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct OscillationConfig {
36 pub cycles: usize,
38 pub min_ideas_per_diverge: usize,
40 pub max_ideas_to_converge: usize,
42 pub divergent_dimensions: Vec<DivergentDimension>,
44 pub convergent_criteria: Vec<ConvergentCriterion>,
46 pub track_lineage: bool,
48}
49
50impl Default for OscillationConfig {
51 fn default() -> Self {
52 Self {
53 cycles: 3,
54 min_ideas_per_diverge: 5,
55 max_ideas_to_converge: 3,
56 divergent_dimensions: vec![
57 DivergentDimension::Fluency,
58 DivergentDimension::Flexibility,
59 DivergentDimension::Originality,
60 DivergentDimension::Elaboration,
61 ],
62 convergent_criteria: vec![
63 ConvergentCriterion::Feasibility,
64 ConvergentCriterion::Impact,
65 ConvergentCriterion::Novelty,
66 ConvergentCriterion::Alignment,
67 ],
68 track_lineage: true,
69 }
70 }
71}
72
73impl OscillationConfig {
74 pub fn gigathink() -> Self {
76 Self {
77 cycles: 3,
78 min_ideas_per_diverge: 10,
79 max_ideas_to_converge: 5,
80 divergent_dimensions: vec![
81 DivergentDimension::Fluency,
82 DivergentDimension::Flexibility,
83 DivergentDimension::Originality,
84 DivergentDimension::Elaboration,
85 DivergentDimension::Analogical,
86 DivergentDimension::Contrarian,
87 ],
88 convergent_criteria: vec![
89 ConvergentCriterion::Feasibility,
90 ConvergentCriterion::Impact,
91 ConvergentCriterion::Novelty,
92 ConvergentCriterion::Alignment,
93 ],
94 track_lineage: true,
95 }
96 }
97
98 pub fn quick() -> Self {
100 Self {
101 cycles: 2,
102 min_ideas_per_diverge: 5,
103 max_ideas_to_converge: 2,
104 ..Default::default()
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum DivergentDimension {
112 Fluency,
114 Flexibility,
116 Originality,
118 Elaboration,
120 Analogical,
122 Contrarian,
124 Combinatorial,
126 Inversion,
128}
129
130impl DivergentDimension {
131 pub fn prompt_guidance(&self) -> &'static str {
133 match self {
134 Self::Fluency => "Generate as many ideas as possible, without filtering",
135 Self::Flexibility => {
136 "Generate ideas from different perspectives, domains, and categories"
137 }
138 Self::Originality => "Generate unusual, surprising, or unconventional ideas",
139 Self::Elaboration => "Add specific details, mechanisms, and implementation paths",
140 Self::Analogical => "Draw ideas from analogous domains and transfer insights",
141 Self::Contrarian => "Challenge assumptions and generate opposing viewpoints",
142 Self::Combinatorial => "Combine and recombine existing ideas in new ways",
143 Self::Inversion => "Invert the problem - what would make it worse? Then reverse",
144 }
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum ConvergentCriterion {
151 Feasibility,
153 Impact,
155 Novelty,
157 Alignment,
159 Risk,
161 ResourceCost,
163 TimeToValue,
165 ComparativeAdvantage,
167}
168
169impl ConvergentCriterion {
170 pub fn evaluation_question(&self) -> &'static str {
172 match self {
173 Self::Feasibility => "How implementable is this idea given current constraints?",
174 Self::Impact => "What magnitude of positive change would this create?",
175 Self::Novelty => "How unique is this compared to existing approaches?",
176 Self::Alignment => "How well does this serve the stated goals?",
177 Self::Risk => "What could go wrong and how severe would it be?",
178 Self::ResourceCost => "What resources (time, money, effort) are required?",
179 Self::TimeToValue => "How quickly can this start delivering value?",
180 Self::ComparativeAdvantage => "Why is this better than the alternatives?",
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct Idea {
188 pub id: usize,
190 pub content: String,
192 pub dimension: DivergentDimension,
194 pub parent_id: Option<usize>,
196 pub cycle: usize,
198 pub scores: Vec<CriterionScore>,
200 pub priority: f32,
202 pub survived: bool,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct CriterionScore {
209 pub criterion: ConvergentCriterion,
210 pub score: f32, pub rationale: String,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct DivergentPhase {
217 pub cycle: usize,
218 pub ideas_generated: Vec<Idea>,
219 pub dimension_coverage: Vec<(DivergentDimension, usize)>,
220 pub fluency_score: f32, pub flexibility_score: f32, }
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ConvergentPhase {
227 pub cycle: usize,
228 pub ideas_evaluated: usize,
229 pub ideas_selected: Vec<usize>, pub elimination_rationale: Vec<String>,
231 pub selection_rationale: Vec<String>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct OscillationResult {
237 pub problem: String,
239 pub all_ideas: Vec<Idea>,
241 pub divergent_phases: Vec<DivergentPhase>,
243 pub convergent_phases: Vec<ConvergentPhase>,
245 pub final_ideas: Vec<Idea>,
247 pub synthesis: String,
249 pub metrics: OscillationMetrics,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct OscillationMetrics {
256 pub total_ideas: usize,
258 pub surviving_ideas: usize,
260 pub survival_rate: f32,
262 pub avg_fluency: f32,
264 pub flexibility_score: f32,
266 pub originality_score: f32,
268 pub cycles_completed: usize,
270}
271
272impl OscillationResult {
273 pub fn ideas_from_cycle(&self, cycle: usize) -> Vec<&Idea> {
275 self.all_ideas.iter().filter(|i| i.cycle == cycle).collect()
276 }
277
278 pub fn ideas_by_dimension(&self, dim: DivergentDimension) -> Vec<&Idea> {
280 self.all_ideas
281 .iter()
282 .filter(|i| i.dimension == dim)
283 .collect()
284 }
285
286 pub fn get_lineage(&self, idea_id: usize) -> Vec<&Idea> {
288 let mut lineage = vec![];
289 let mut current_id = Some(idea_id);
290
291 while let Some(id) = current_id {
292 if let Some(idea) = self.all_ideas.iter().find(|i| i.id == id) {
293 lineage.push(idea);
294 current_id = idea.parent_id;
295 } else {
296 break;
297 }
298 }
299
300 lineage.reverse();
301 lineage
302 }
303}
304
305pub struct OscillationPrompts;
307
308impl OscillationPrompts {
309 pub fn diverge(
311 problem: &str,
312 dimensions: &[DivergentDimension],
313 prior_ideas: &[String],
314 ) -> String {
315 let dimension_guidance: String = dimensions
316 .iter()
317 .enumerate()
318 .map(|(i, d)| format!("{}. {:?}: {}", i + 1, d, d.prompt_guidance()))
319 .collect::<Vec<_>>()
320 .join("\n");
321
322 let prior = if prior_ideas.is_empty() {
323 "None yet - this is the first cycle.".to_string()
324 } else {
325 prior_ideas
326 .iter()
327 .enumerate()
328 .map(|(i, idea)| format!("{}. {}", i + 1, idea))
329 .collect::<Vec<_>>()
330 .join("\n")
331 };
332
333 format!(
334 r#"DIVERGENT THINKING PHASE - Generate Ideas
335
336PROBLEM: {problem}
337
338Use these thinking dimensions to generate diverse ideas:
339{dimension_guidance}
340
341PRIOR IDEAS (to build on or differentiate from):
342{prior}
343
344Generate at least 5 ideas, covering multiple dimensions.
345For each idea, specify:
346- IDEA: [The core idea]
347- DIMENSION: [Which dimension it came from]
348- ELABORATION: [Key details, mechanism, or implementation]
349
350Be creative, be bold, defer judgment. Quantity over quality in this phase.
351
352Format each idea clearly:
353IDEA 1:
354- Content: ...
355- Dimension: ...
356- Elaboration: ..."#,
357 problem = problem,
358 dimension_guidance = dimension_guidance,
359 prior = prior
360 )
361 }
362
363 pub fn converge(
365 problem: &str,
366 ideas: &[String],
367 criteria: &[ConvergentCriterion],
368 max_select: usize,
369 ) -> String {
370 let criteria_list: String = criteria
371 .iter()
372 .enumerate()
373 .map(|(i, c)| format!("{}. {:?}: {}", i + 1, c, c.evaluation_question()))
374 .collect::<Vec<_>>()
375 .join("\n");
376
377 let ideas_list: String = ideas
378 .iter()
379 .enumerate()
380 .map(|(i, idea)| format!("IDEA {}: {}", i + 1, idea))
381 .collect::<Vec<_>>()
382 .join("\n\n");
383
384 format!(
385 r#"CONVERGENT THINKING PHASE - Evaluate and Select
386
387PROBLEM: {problem}
388
389IDEAS TO EVALUATE:
390{ideas_list}
391
392EVALUATION CRITERIA:
393{criteria_list}
394
395For each idea, score it on each criterion (0.0 to 1.0) with a brief rationale.
396
397Then SELECT the top {max_select} ideas to carry forward.
398Explain why you're eliminating the others.
399
400Format:
401EVALUATION:
402Idea 1: [criterion scores and rationale]
403Idea 2: [criterion scores and rationale]
404...
405
406SELECTED (top {max_select}):
4071. Idea X - [why this was chosen]
4082. Idea Y - [why this was chosen]
409...
410
411ELIMINATED:
412- Idea Z - [why eliminated]
413..."#,
414 problem = problem,
415 ideas_list = ideas_list,
416 criteria_list = criteria_list,
417 max_select = max_select
418 )
419 }
420
421 pub fn synthesize(problem: &str, final_ideas: &[String]) -> String {
423 let ideas_formatted: String = final_ideas
424 .iter()
425 .enumerate()
426 .map(|(i, idea)| format!("{}. {}", i + 1, idea))
427 .collect::<Vec<_>>()
428 .join("\n");
429
430 format!(
431 r#"SYNTHESIS PHASE - Integrate Best Ideas
432
433PROBLEM: {problem}
434
435FINAL SELECTED IDEAS:
436{ideas_formatted}
437
438Create a coherent synthesis that:
4391. Integrates the best elements of each idea
4402. Resolves any tensions between them
4413. Provides a unified approach
4424. Identifies implementation priorities
443
444SYNTHESIS:
445[Provide a 2-3 paragraph synthesis]
446
447KEY TAKEAWAYS:
4481. [Most important insight]
4492. [Second most important insight]
4503. [Third most important insight]
451
452RECOMMENDED APPROACH:
453[Concise action plan]"#,
454 problem = problem,
455 ideas_formatted = ideas_formatted
456 )
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn test_config_default() {
466 let config = OscillationConfig::default();
467 assert_eq!(config.cycles, 3);
468 assert!(config.min_ideas_per_diverge >= 5);
469 }
470
471 #[test]
472 fn test_gigathink_config() {
473 let config = OscillationConfig::gigathink();
474 assert_eq!(config.min_ideas_per_diverge, 10);
475 assert!(config
476 .divergent_dimensions
477 .contains(&DivergentDimension::Analogical));
478 assert!(config
479 .divergent_dimensions
480 .contains(&DivergentDimension::Contrarian));
481 }
482
483 #[test]
484 fn test_divergent_dimensions() {
485 let dim = DivergentDimension::Fluency;
486 assert!(dim.prompt_guidance().contains("many"));
487
488 let dim = DivergentDimension::Originality;
489 assert!(dim.prompt_guidance().contains("unusual"));
490 }
491
492 #[test]
493 fn test_convergent_criteria() {
494 let crit = ConvergentCriterion::Feasibility;
495 assert!(crit.evaluation_question().contains("implement"));
496
497 let crit = ConvergentCriterion::Impact;
498 assert!(crit.evaluation_question().contains("change"));
499 }
500
501 #[test]
502 fn test_oscillation_result_lineage() {
503 let result = OscillationResult {
504 problem: "Test".into(),
505 all_ideas: vec![
506 Idea {
507 id: 0,
508 content: "Root idea".into(),
509 dimension: DivergentDimension::Fluency,
510 parent_id: None,
511 cycle: 0,
512 scores: vec![],
513 priority: 0.8,
514 survived: true,
515 },
516 Idea {
517 id: 1,
518 content: "Child idea".into(),
519 dimension: DivergentDimension::Elaboration,
520 parent_id: Some(0),
521 cycle: 1,
522 scores: vec![],
523 priority: 0.9,
524 survived: true,
525 },
526 Idea {
527 id: 2,
528 content: "Grandchild idea".into(),
529 dimension: DivergentDimension::Originality,
530 parent_id: Some(1),
531 cycle: 2,
532 scores: vec![],
533 priority: 0.95,
534 survived: true,
535 },
536 ],
537 divergent_phases: vec![],
538 convergent_phases: vec![],
539 final_ideas: vec![],
540 synthesis: "".into(),
541 metrics: OscillationMetrics {
542 total_ideas: 3,
543 surviving_ideas: 3,
544 survival_rate: 1.0,
545 avg_fluency: 1.0,
546 flexibility_score: 1.0,
547 originality_score: 0.8,
548 cycles_completed: 3,
549 },
550 };
551
552 let lineage = result.get_lineage(2);
553 assert_eq!(lineage.len(), 3);
554 assert_eq!(lineage[0].id, 0);
555 assert_eq!(lineage[1].id, 1);
556 assert_eq!(lineage[2].id, 2);
557 }
558
559 #[test]
560 fn test_metrics() {
561 let metrics = OscillationMetrics {
562 total_ideas: 30,
563 surviving_ideas: 5,
564 survival_rate: 5.0 / 30.0,
565 avg_fluency: 10.0,
566 flexibility_score: 0.85,
567 originality_score: 0.75,
568 cycles_completed: 3,
569 };
570
571 assert!(metrics.survival_rate < 0.2);
572 assert_eq!(metrics.cycles_completed, 3);
573 }
574}