feat(minga-explorer): listings de items recientes en cada stat card
Iter 12. Hasta ahora minga-explorer mostraba sólo counts. Ahora cada stat card muestra un sample de los 5 primeros items: hashes truncados de nodos AST (con kind), atestaciones (content_hash ← did_short) y claves MST. - RepoSnapshot agrega 3 Vec<String/(String,String)> con recent items, cap RECENT_LIMIT=5. - load_snapshot itera los stores con filter_map(Result::ok) + take(5). Errores per-item silenciados — dashboard tolerante a corrupción puntual. - short_hash(&str) local: trunca a 12 chars (48 bits). - stat_card extendido con recent_items: &[String]. Si no vacío, agrega "recent (N de TOTAL):" + una linea por item. Tests: 2 → 4 (sanity defaults + short_hash). Beneficio: tras `minga ingest`, el explorer muestra los hashes de los nodos creados sin necesitar queries SQL. Limitación: los "recent" no son cronológicos (sled ordena lex por hash). Timeline real requiere timestamp al schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,50 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-10
|
## 2026-05-10
|
||||||
|
|
||||||
|
### feat(minga-explorer): listings de items recientes en cada stat card
|
||||||
|
Iter 12. Hasta ahora minga-explorer mostraba sólo counts (3
|
||||||
|
números). Ahora cada stat card muestra también un sample de los
|
||||||
|
items dentro: hashes truncados de los 5 primeros nodos AST
|
||||||
|
(con su `kind`), atestaciones (`content_hash ← did_short`) y
|
||||||
|
claves MST. Mucho más útil para debugging que el "tengo N items".
|
||||||
|
|
||||||
|
Cambios en `minga-explorer`:
|
||||||
|
- **`RepoSnapshot` extendido** con 3 nuevos `Vec<...>`:
|
||||||
|
- `recent_nodes: Vec<(String, String)>` — `(hash_short, kind)`.
|
||||||
|
- `recent_attestations: Vec<(String, String)>` —
|
||||||
|
`(content_hash_short, did_short)`.
|
||||||
|
- `recent_mst_keys: Vec<String>` — `hash_short`.
|
||||||
|
- Cap de 5 items por sección via `RECENT_LIMIT` const.
|
||||||
|
- **`load_snapshot` itera los stores** y toma los primeros 5
|
||||||
|
items via `iter().filter_map(Result::ok).take(RECENT_LIMIT)`.
|
||||||
|
Errores per-item se silencian (`filter_map`) — el dashboard
|
||||||
|
muestra lo que pueda; un par de items corruptos no debería
|
||||||
|
tirar el panel.
|
||||||
|
- **`short_hash(&str)` helper local**: trunca un hex a sus
|
||||||
|
primeros 12 chars (48 bits, distintivo dentro de un repo
|
||||||
|
single-machine).
|
||||||
|
- **`stat_card` extendido**: nuevo arg `recent_items: &[String]`.
|
||||||
|
Si no está vacío, agrega un sub-header `"recent (N de TOTAL):"`
|
||||||
|
+ una linea por item. Cada line es texto pequeño (`px(11)`)
|
||||||
|
con el color principal del theme — visualmente queda como
|
||||||
|
monospace listing aunque no use mono font (no hay todavía
|
||||||
|
en el theme).
|
||||||
|
|
||||||
|
Tests: 2 → **4** (+2 sanity de los nuevos defaults + del
|
||||||
|
`short_hash`).
|
||||||
|
|
||||||
|
Beneficio operativo:
|
||||||
|
- Después de `minga ingest archivo.rs`, el explorer muestra
|
||||||
|
inmediatamente los hashes de los nodos AST creados, qué `kind`
|
||||||
|
tienen, y las atestaciones firmadas — sin necesitar `minga
|
||||||
|
status` o queries SQL.
|
||||||
|
- "5 de 247" da contexto del crecimiento sin overwhelm de
|
||||||
|
listing completo.
|
||||||
|
|
||||||
|
Limitación documentada: los "recent" no son cronológicos — sled
|
||||||
|
ordena lexicográfico por hash. Para timeline real, agregar
|
||||||
|
timestamp al schema (cambio breaking del store, scope futuro).
|
||||||
|
|
||||||
### feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh
|
### feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh
|
||||||
Iter 11. Cierra el último frente identificado: integración del
|
Iter 11. Cierra el último frente identificado: integración del
|
||||||
módulo Minga (VCS semántico P2P) al ecosistema GUI. Antes Minga
|
módulo Minga (VCS semántico P2P) al ecosistema GUI. Antes Minga
|
||||||
|
|||||||
@@ -59,14 +59,27 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot de counts del repo. Se reemplaza completo en cada
|
/// Cuántos items recientes mostrar por sección. Los stores no
|
||||||
/// refresh — los stores no diff fácilmente, y los counts son
|
/// tienen orden cronológico (sled ordena lexicográfico por hash);
|
||||||
/// baratos (sled tracks size).
|
/// los "recent" acá son simplemente los primeros del iter — sirve
|
||||||
|
/// como sample, no como log temporal. Para timeline real haría
|
||||||
|
/// falta agregar timestamp al schema.
|
||||||
|
const RECENT_LIMIT: usize = 5;
|
||||||
|
|
||||||
|
/// Snapshot de counts + sample de items recientes. Reemplaza el
|
||||||
|
/// completo en cada refresh — los stores no diff fácilmente y los
|
||||||
|
/// counts son baratos (sled tracks size).
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
struct RepoSnapshot {
|
struct RepoSnapshot {
|
||||||
nodes: usize,
|
nodes: usize,
|
||||||
attestations: usize,
|
attestations: usize,
|
||||||
mst_keys: usize,
|
mst_keys: usize,
|
||||||
|
/// Sample de nodos: `(hash_short, kind)`.
|
||||||
|
recent_nodes: Vec<(String, String)>,
|
||||||
|
/// Sample de atestaciones: `(content_hash_short, did_short)`.
|
||||||
|
recent_attestations: Vec<(String, String)>,
|
||||||
|
/// Sample de claves MST: `hash_short`.
|
||||||
|
recent_mst_keys: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Explorer {
|
struct Explorer {
|
||||||
@@ -133,11 +146,61 @@ fn load_snapshot(repo_path: &std::path::Path) -> Result<RepoSnapshot, String> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let repo = PersistentRepo::open(&inner).map_err(|e| format!("open: {e}"))?;
|
let repo = PersistentRepo::open(&inner).map_err(|e| format!("open: {e}"))?;
|
||||||
Ok(RepoSnapshot {
|
|
||||||
nodes: repo.nodes.len(),
|
// Counts: cheap (sled tracks size).
|
||||||
attestations: repo.attestations.len(),
|
let nodes = repo.nodes.len();
|
||||||
mst_keys: repo.mst.len(),
|
let attestations = repo.attestations.len();
|
||||||
|
let mst_keys = repo.mst.len();
|
||||||
|
|
||||||
|
// Samples: tomar los primeros RECENT_LIMIT items del iter.
|
||||||
|
// Errores per-item se silencian (filter_map) porque el dashboard
|
||||||
|
// muestra lo que pueda; un par de items corruptos no debería
|
||||||
|
// tirar el panel entero.
|
||||||
|
let recent_nodes: Vec<(String, String)> = repo
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
|
.map(|(hash, stored)| (short_hash(&hash.to_string()), stored.kind))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let recent_attestations: Vec<(String, String)> = repo
|
||||||
|
.attestations
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
|
.map(|att| {
|
||||||
|
(
|
||||||
|
short_hash(&att.content.to_string()),
|
||||||
|
short_hash(&att.author.to_string()),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let recent_mst_keys: Vec<String> = repo
|
||||||
|
.mst
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
|
.map(|h| short_hash(&h.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(RepoSnapshot {
|
||||||
|
nodes,
|
||||||
|
attestations,
|
||||||
|
mst_keys,
|
||||||
|
recent_nodes,
|
||||||
|
recent_attestations,
|
||||||
|
recent_mst_keys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trunca un hex string a sus primeros 12 chars. Convención cross-app
|
||||||
|
/// para mostrar hashes/dids/contenthash compactos sin perder
|
||||||
|
/// distintividad práctica (12 hex = 48 bits, colisión improbable
|
||||||
|
/// dentro de un repo single-machine).
|
||||||
|
fn short_hash(s: &str) -> String {
|
||||||
|
s.chars().take(12).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Explorer {
|
impl Render for Explorer {
|
||||||
@@ -189,7 +252,20 @@ impl Render for Explorer {
|
|||||||
.text_color(text_dim)
|
.text_color(text_dim)
|
||||||
.text_size(px(13.))
|
.text_size(px(13.))
|
||||||
.child("Esperando primer refresh…"),
|
.child("Esperando primer refresh…"),
|
||||||
Some(snap) => div()
|
Some(snap) => {
|
||||||
|
let node_items: Vec<String> = snap
|
||||||
|
.recent_nodes
|
||||||
|
.iter()
|
||||||
|
.map(|(h, k)| format!("{h} {k}"))
|
||||||
|
.collect();
|
||||||
|
let attestation_items: Vec<String> = snap
|
||||||
|
.recent_attestations
|
||||||
|
.iter()
|
||||||
|
.map(|(h, did)| format!("{h} ← {did}"))
|
||||||
|
.collect();
|
||||||
|
let mst_items: Vec<String> = snap.recent_mst_keys.clone();
|
||||||
|
|
||||||
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap(px(8.))
|
.gap(px(8.))
|
||||||
@@ -203,6 +279,7 @@ impl Render for Explorer {
|
|||||||
accent_nodes,
|
accent_nodes,
|
||||||
text,
|
text,
|
||||||
text_dim,
|
text_dim,
|
||||||
|
&node_items,
|
||||||
))
|
))
|
||||||
.child(stat_card(
|
.child(stat_card(
|
||||||
cx,
|
cx,
|
||||||
@@ -212,6 +289,7 @@ impl Render for Explorer {
|
|||||||
accent_attestations,
|
accent_attestations,
|
||||||
text,
|
text,
|
||||||
text_dim,
|
text_dim,
|
||||||
|
&attestation_items,
|
||||||
))
|
))
|
||||||
.child(stat_card(
|
.child(stat_card(
|
||||||
cx,
|
cx,
|
||||||
@@ -221,7 +299,9 @@ impl Render for Explorer {
|
|||||||
accent_mst,
|
accent_mst,
|
||||||
text,
|
text,
|
||||||
text_dim,
|
text_dim,
|
||||||
)),
|
&mst_items,
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
@@ -236,7 +316,9 @@ impl Render for Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Card visual para una estadística del dashboard. Border-l por
|
/// Card visual para una estadística del dashboard. Border-l por
|
||||||
/// kind, label arriba + número grande + descripción abajo.
|
/// kind, label arriba + número grande + descripción + listing de
|
||||||
|
/// items recientes (puede estar vacío). Items se renderean en
|
||||||
|
/// `monospace`-look (text_size chico) — útil para hashes/dids.
|
||||||
fn stat_card(
|
fn stat_card(
|
||||||
cx: &mut Context<Explorer>,
|
cx: &mut Context<Explorer>,
|
||||||
label: &str,
|
label: &str,
|
||||||
@@ -245,8 +327,9 @@ fn stat_card(
|
|||||||
accent: gpui::Rgba,
|
accent: gpui::Rgba,
|
||||||
text: gpui::Hsla,
|
text: gpui::Hsla,
|
||||||
text_dim: gpui::Hsla,
|
text_dim: gpui::Hsla,
|
||||||
|
recent_items: &[String],
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
card_themed(cx)
|
let mut card = card_themed(cx)
|
||||||
.border_l_4()
|
.border_l_4()
|
||||||
.border_color(accent)
|
.border_color(accent)
|
||||||
.child(
|
.child(
|
||||||
@@ -266,7 +349,33 @@ fn stat_card(
|
|||||||
.text_color(text_dim)
|
.text_color(text_dim)
|
||||||
.text_size(px(11.))
|
.text_size(px(11.))
|
||||||
.child(SharedString::from(description.to_string())),
|
.child(SharedString::from(description.to_string())),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
if !recent_items.is_empty() {
|
||||||
|
// Header de la sub-section.
|
||||||
|
card = card.child(
|
||||||
|
div()
|
||||||
|
.mt(px(6.))
|
||||||
|
.text_color(text_dim)
|
||||||
|
.text_size(px(10.))
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"recent ({} de {}):",
|
||||||
|
recent_items.len(),
|
||||||
|
value
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
// Una linea por item.
|
||||||
|
for it in recent_items {
|
||||||
|
card = card.child(
|
||||||
|
div()
|
||||||
|
.text_color(text)
|
||||||
|
.text_size(px(11.))
|
||||||
|
.child(SharedString::from(it.clone())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
card
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -294,10 +403,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_default_is_zeros() {
|
fn snapshot_default_is_zeros_and_empty_lists() {
|
||||||
let s = RepoSnapshot::default();
|
let s = RepoSnapshot::default();
|
||||||
assert_eq!(s.nodes, 0);
|
assert_eq!(s.nodes, 0);
|
||||||
assert_eq!(s.attestations, 0);
|
assert_eq!(s.attestations, 0);
|
||||||
assert_eq!(s.mst_keys, 0);
|
assert_eq!(s.mst_keys, 0);
|
||||||
|
assert!(s.recent_nodes.is_empty());
|
||||||
|
assert!(s.recent_attestations.is_empty());
|
||||||
|
assert!(s.recent_mst_keys.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_hash_takes_first_12_chars() {
|
||||||
|
let s = "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd";
|
||||||
|
assert_eq!(short_hash(s), "a1b2c3d4e5f6");
|
||||||
|
assert_eq!(short_hash(s).len(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_hash_handles_empty_or_shorter() {
|
||||||
|
assert_eq!(short_hash(""), "");
|
||||||
|
assert_eq!(short_hash("abc"), "abc");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user