feat(badu): app demo — cuaderno con grafo de enlaces y gravedad
1//! `badu` — demostración del cuaderno de notas.
2//!
3//! Siembra un cuaderno personal, imprime el grafo de wiki-links
4//! (forward-links, backlinks, huérfanas, enlaces colgantes) y luego la
5//! gravedad semántica: los clústeres por afinidad y los vecinos más
6//! cercanos de una nota.
7//!
8//! Los vectores semánticos van a mano —tres tópicos: cocina, jardín,
9//! oficina— para que el clustering se vea con claridad. En la app real
10//! los produce `verbo`. Corre con `cargo run -p badu`.
11
12use badu_core::{NoteId, NoteStore};
13use badu_gravity::{GravityConfig, SemanticField};
14
15/// Vector de tópico con un leve sesgo — notas del mismo tema quedan
16/// afines sin ser idénticas.
17fn topic(base: [f32; 3], nudge: f32) -> Vec<f32> {
18 vec![base[0] + nudge, base[1] + nudge * 0.3, base[2] - nudge * 0.2]
19}
20
21fn main() {
22 let cocina = [1.0, 0.0, 0.0];
23 let jardin = [0.0, 1.0, 0.0];
24 let oficina = [0.0, 0.0, 1.0];
25
26 let mut store = NoteStore::new();
27 let mut field = SemanticField::new();
28
29 // (título, cuerpo, etiquetas, vector de tópico)
30 let seed: [(&str, &str, &[&str], Vec<f32>); 7] = [
31 (
32 "Índice",
33 "mi cuaderno: [[Recetas de la abuela]], [[Jardín]] y [[Oficina]]",
34 &["meta"],
35 topic(cocina, 0.0),
36 ),
37 (
38 "Recetas de la abuela",
39 "sopa de auyama; ver también [[Lista del mercado]]",
40 &["cocina"],
41 topic(cocina, 0.05),
42 ),
43 (
44 "Lista del mercado",
45 "auyama, cilantro, pan; vuelve al [[Índice]]",
46 &["cocina"],
47 topic(cocina, 0.10),
48 ),
49 (
50 "Jardín",
51 "riego semanal; las [[Semillas de cilantro]] van en marzo",
52 &["jardín"],
53 topic(jardin, 0.05),
54 ),
55 (
56 "Semillas de cilantro",
57 "germinan en diez días",
58 &["jardín"],
59 topic(jardin, 0.10),
60 ),
61 (
62 "Oficina",
63 "[[Reunión del lunes]] y pendientes varios",
64 &["trabajo"],
65 topic(oficina, 0.05),
66 ),
67 (
68 "Diario sin enlaces",
69 "una nota suelta, no la enlaza nadie y enlaza a [[Algo Perdido]]",
70 &["personal"],
71 topic(oficina, 0.50),
72 ),
73 ];
74
75 let mut ids: Vec<(NoteId, String)> = Vec::new();
76 for (title, body, tags, vector) in seed {
77 let tags = tags.iter().map(|t| t.to_string()).collect();
78 let id = store.create(title, body, tags, 1_700_000_000);
79 field.insert(id, vector);
80 ids.push((id, title.to_string()));
81 }
82
83 let name = |id: NoteId| {
84 ids.iter()
85 .find(|(i, _)| *i == id)
86 .map(|(_, n)| n.as_str())
87 .unwrap_or("?")
88 };
89
90 println!("\n badu · cuaderno de notas — {} notas\n", store.len());
91
92 println!(" grafo de enlaces:");
93 for note in store.iter() {
94 let fwd: Vec<&str> = store.forward_links(note.id).into_iter().map(name).collect();
95 let back: Vec<&str> = store.backlinks(note.id).into_iter().map(name).collect();
96 println!(" «{}»", note.title);
97 println!(" enlaza a : {}", fmt_list(&fwd));
98 println!(" backlinks : {}", fmt_list(&back));
99 }
100
101 let orphans: Vec<&str> = store.orphans().iter().map(|n| n.title.as_str()).collect();
102 println!("\n notas huérfanas (sin backlinks): {}", fmt_list(&orphans));
103 let dangling_owned = store.dangling_links();
104 let dangling: Vec<&str> = dangling_owned.iter().map(|s| s.as_str()).collect();
105 println!(" enlaces colgantes (destino inexistente): {}", fmt_list(&dangling));
106
107 println!("\n gravedad semántica — clústeres (afinidad ≥ 0.85):");
108 for (n, cluster) in field.clusters(0.85).iter().enumerate() {
109 let titles: Vec<&str> = cluster.iter().map(|id| name(*id)).collect();
110 println!(" grupo {}: {}", n + 1, fmt_list(&titles));
111 }
112
113 let pivot = ids[1].0; // "Recetas de la abuela"
114 println!("\n vecinos más afines a «{}»:", name(pivot));
115 for (id, score) in field.nearest(pivot, 3) {
116 println!(" {:.3} {}", score, name(id));
117 }
118
119 let layout = field.gravity_layout(&GravityConfig::default());
120 println!("\n layout 2D por gravedad ({} posiciones):", layout.len());
121 for p in &layout {
122 println!(" ({:7.1}, {:7.1}) {}", p.x, p.y, name(p.id));
123 }
124 println!();
125}
126
127/// Formatea una lista de nombres, o `—` si está vacía.
128fn fmt_list(items: &[&str]) -> String {
129 if items.is_empty() {
130 "—".to_string()
131 } else {
132 items.join(", ")
133 }
134}