feat(badu): app demo — cuaderno con grafo de enlaces y gravedad

CLI que siembra un cuaderno (cocina/jardín/oficina), imprime el grafo
de wiki-links (forward/backlinks, huérfanas, colgantes) y los
clústeres por gravedad semántica + vecinos + layout 2D.
cargo run -p badu.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 16:43:42 +00:00
parent d0a175a90a
commit ea079a0b23
5 changed files with 166 additions and 4 deletions
Generated
+8
View File
@@ -1363,6 +1363,14 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "badu"
version = "0.1.0"
dependencies = [
"badu-core",
"badu-gravity",
]
[[package]] [[package]]
name = "badu-core" name = "badu-core"
version = "0.1.0" version = "0.1.0"
+1
View File
@@ -242,6 +242,7 @@ members = [
"crates/apps/dominium", "crates/apps/dominium",
"crates/apps/fana", "crates/apps/fana",
"crates/apps/agorapura", "crates/apps/agorapura",
"crates/apps/badu",
] ]
[workspace.package] [workspace.package]
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "badu"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "badu — demostración de la toma de notas: un cuaderno con wiki-links, backlinks, enlaces colgantes y clústeres por gravedad semántica."
[[bin]]
name = "badu"
path = "src/main.rs"
[dependencies]
badu-core = { path = "../../modules/badu/badu-core" }
badu-gravity = { path = "../../modules/badu/badu-gravity" }
+134
View File
@@ -0,0 +1,134 @@
//! `badu` — demostración del cuaderno de notas.
//!
//! Siembra un cuaderno personal, imprime el grafo de wiki-links
//! (forward-links, backlinks, huérfanas, enlaces colgantes) y luego la
//! gravedad semántica: los clústeres por afinidad y los vecinos más
//! cercanos de una nota.
//!
//! Los vectores semánticos van a mano —tres tópicos: cocina, jardín,
//! oficina— para que el clustering se vea con claridad. En la app real
//! los produce `verbo`. Corre con `cargo run -p badu`.
use badu_core::{NoteId, NoteStore};
use badu_gravity::{GravityConfig, SemanticField};
/// Vector de tópico con un leve sesgo — notas del mismo tema quedan
/// afines sin ser idénticas.
fn topic(base: [f32; 3], nudge: f32) -> Vec<f32> {
vec![base[0] + nudge, base[1] + nudge * 0.3, base[2] - nudge * 0.2]
}
fn main() {
let cocina = [1.0, 0.0, 0.0];
let jardin = [0.0, 1.0, 0.0];
let oficina = [0.0, 0.0, 1.0];
let mut store = NoteStore::new();
let mut field = SemanticField::new();
// (título, cuerpo, etiquetas, vector de tópico)
let seed: [(&str, &str, &[&str], Vec<f32>); 7] = [
(
"Índice",
"mi cuaderno: [[Recetas de la abuela]], [[Jardín]] y [[Oficina]]",
&["meta"],
topic(cocina, 0.0),
),
(
"Recetas de la abuela",
"sopa de auyama; ver también [[Lista del mercado]]",
&["cocina"],
topic(cocina, 0.05),
),
(
"Lista del mercado",
"auyama, cilantro, pan; vuelve al [[Índice]]",
&["cocina"],
topic(cocina, 0.10),
),
(
"Jardín",
"riego semanal; las [[Semillas de cilantro]] van en marzo",
&["jardín"],
topic(jardin, 0.05),
),
(
"Semillas de cilantro",
"germinan en diez días",
&["jardín"],
topic(jardin, 0.10),
),
(
"Oficina",
"[[Reunión del lunes]] y pendientes varios",
&["trabajo"],
topic(oficina, 0.05),
),
(
"Diario sin enlaces",
"una nota suelta, no la enlaza nadie y enlaza a [[Algo Perdido]]",
&["personal"],
topic(oficina, 0.50),
),
];
let mut ids: Vec<(NoteId, String)> = Vec::new();
for (title, body, tags, vector) in seed {
let tags = tags.iter().map(|t| t.to_string()).collect();
let id = store.create(title, body, tags, 1_700_000_000);
field.insert(id, vector);
ids.push((id, title.to_string()));
}
let name = |id: NoteId| {
ids.iter()
.find(|(i, _)| *i == id)
.map(|(_, n)| n.as_str())
.unwrap_or("?")
};
println!("\n badu · cuaderno de notas — {} notas\n", store.len());
println!(" grafo de enlaces:");
for note in store.iter() {
let fwd: Vec<&str> = store.forward_links(note.id).into_iter().map(name).collect();
let back: Vec<&str> = store.backlinks(note.id).into_iter().map(name).collect();
println!(" «{}»", note.title);
println!(" enlaza a : {}", fmt_list(&fwd));
println!(" backlinks : {}", fmt_list(&back));
}
let orphans: Vec<&str> = store.orphans().iter().map(|n| n.title.as_str()).collect();
println!("\n notas huérfanas (sin backlinks): {}", fmt_list(&orphans));
let dangling_owned = store.dangling_links();
let dangling: Vec<&str> = dangling_owned.iter().map(|s| s.as_str()).collect();
println!(" enlaces colgantes (destino inexistente): {}", fmt_list(&dangling));
println!("\n gravedad semántica — clústeres (afinidad ≥ 0.85):");
for (n, cluster) in field.clusters(0.85).iter().enumerate() {
let titles: Vec<&str> = cluster.iter().map(|id| name(*id)).collect();
println!(" grupo {}: {}", n + 1, fmt_list(&titles));
}
let pivot = ids[1].0; // "Recetas de la abuela"
println!("\n vecinos más afines a «{}»:", name(pivot));
for (id, score) in field.nearest(pivot, 3) {
println!(" {:.3} {}", score, name(id));
}
let layout = field.gravity_layout(&GravityConfig::default());
println!("\n layout 2D por gravedad ({} posiciones):", layout.len());
for p in &layout {
println!(" ({:7.1}, {:7.1}) {}", p.x, p.y, name(p.id));
}
println!();
}
/// Formatea una lista de nombres, o `—` si está vacía.
fn fmt_list(items: &[&str]) -> String {
if items.is_empty() {
"".to_string()
} else {
items.join(", ")
}
}
+6 -4
View File
@@ -35,7 +35,9 @@ notas por afinidad de significado.
## Estado ## Estado
`core` + `gravity` implementados y verdes (29 tests). **Pendiente**: las `core` + `gravity` implementados y verdes (29 tests). Demo CLI en
4 lentes visuales (lista, grafo, gravedad espacial, línea de tiempo), `apps/badu` (`cargo run -p badu`): cuaderno sembrado con grafo de
los «Susurros» (sugerencias vía `verbo`) y el frontend GPUI — enlaces + clústeres por gravedad. **Pendiente**: las 4 lentes visuales
separabilidad UI estricta, el núcleo ya es agnóstico. (lista, grafo, gravedad espacial, línea de tiempo), los «Susurros»
(sugerencias vía `verbo`) y el frontend GPUI — separabilidad UI
estricta, el núcleo ya es agnóstico.