feat(shuma): disparo de patrones por estructura del directorio

shuma-infer: el directorio de una ocurrencia es ahora el cwd de su
último comando (el dir donde realmente operó el patrón, ya hechos los
cd). predict_next devuelve también el índice del patrón.

shuma-shell: la predicción se filtra por marcadores de proyecto
(.git, Cargo.toml, package.json, go.mod…). El shell deriva la
condición de disparo de un patrón —los marcadores comunes a sus
directorios— y sólo lo anticipa en un cwd que comparta esa estructura.
Así el ghost no sugiere `cargo build` en un directorio sin Cargo.toml.

Cierra la visión: del scripting a la intención, con precisión.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 19:34:39 +00:00
parent 05ccab64f2
commit 9d8f45a9f8
2 changed files with 65 additions and 11 deletions
+51 -2
View File
@@ -39,6 +39,28 @@ const HISTORY: usize = 80;
/// Líneas de salida visibles por comando (modo launcher liviano).
const OUTPUT_LINES: usize = 16;
/// Archivos/directorios que delatan la estructura de un proyecto.
const PROJECT_MARKERS: &[&str] = &[
".git",
"Cargo.toml",
"package.json",
"go.mod",
"Makefile",
"pyproject.toml",
"pom.xml",
"build.gradle",
];
/// Marcadores de proyecto presentes en `dir`.
fn markers_in(dir: &str) -> Vec<String> {
let base = std::path::Path::new(dir);
PROJECT_MARKERS
.iter()
.filter(|m| base.join(m).exists())
.map(|m| m.to_string())
.collect()
}
/// Segundo Unix actual.
fn unix_now() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
@@ -403,6 +425,21 @@ impl Shell {
}
}
/// Condición de disparo de un patrón: los marcadores de proyecto
/// comunes a todos los directorios donde corrió.
fn pattern_trigger(&self, p: &shuma_infer::EmergingPattern) -> Vec<String> {
let mut dirs = p.directories.iter();
let Some(first) = dirs.next() else {
return Vec::new();
};
let mut common = markers_in(first);
for d in dirs {
let here = markers_in(d);
common.retain(|m| here.contains(m));
}
common
}
/// La secuencia que el motor predice como continuación, si la hay.
fn predicted_sequence(&self) -> Option<String> {
if self.patterns.is_empty() {
@@ -410,8 +447,20 @@ impl Shell {
}
let records = self.infer_records();
let tail = &records[records.len().saturating_sub(6)..];
let next = shuma_infer::predict_next(tail, &self.patterns)?;
(!next.is_empty()).then(|| next.join(" && "))
let (pi, next) = shuma_infer::predict_next(tail, &self.patterns)?;
if next.is_empty() {
return None;
}
// Disparo por estructura: no anticipar un patrón en un directorio
// que no comparte su forma (no sugerir `cargo` sin `Cargo.toml`).
let trigger = self.pattern_trigger(&self.patterns[pi]);
if !trigger.is_empty() {
let here = markers_in(self.session.cwd());
if !trigger.iter().all(|m| here.contains(m)) {
return None;
}
}
Some(next.join(" && "))
}
/// Calcula el sufijo fantasma del prompt: el resto de la línea que el