diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs index 60a2330..4c18fc0 100644 --- a/crates/apps/shuma-shell/src/main.rs +++ b/crates/apps/shuma-shell/src/main.rs @@ -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 { + 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 { + 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 { 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 diff --git a/crates/modules/shuma/shuma-infer/src/lib.rs b/crates/modules/shuma/shuma-infer/src/lib.rs index 405bb39..076e08b 100644 --- a/crates/modules/shuma/shuma-infer/src/lib.rs +++ b/crates/modules/shuma/shuma-infer/src/lib.rs @@ -168,9 +168,11 @@ fn build_pattern( }) .collect(); + // El directorio "de trabajo" de una ocurrencia: el cwd de su último + // comando — para entonces todos los `cd` ya se hicieron. let mut directories: Vec = Vec::new(); for &s in starts { - let d = &history[s].cwd; + let d = &history[s + len - 1].cwd; if !directories.contains(d) { directories.push(d.clone()); } @@ -240,13 +242,15 @@ pub fn detect_patterns(history: &[CommandRecord], cfg: &InferConfig) -> Vec Option> { +) -> Option<(usize, Vec)> { let bins: Vec<&str> = recent.iter().map(|r| r.binary.as_str()).collect(); - let mut best: Option<(usize, &EmergingPattern)> = None; - for p in patterns { + // best = (longitud del prefijo coincidente, índice del patrón). + let mut best: Option<(usize, usize)> = None; + for (pi, p) in patterns.iter().enumerate() { // Tiene que quedar al menos un paso por predecir. let max_k = p.signature.len().saturating_sub(1).min(bins.len()); for k in (1..=max_k).rev() { @@ -259,13 +263,13 @@ pub fn predict_next( .eq(tail.iter().copied()); if prefix_matches { if best.map(|(bk, _)| k > bk).unwrap_or(true) { - best = Some((k, p)); + best = Some((k, pi)); } break; } } } - best.map(|(k, p)| p.example[k..].to_vec()) + best.map(|(k, pi)| (pi, patterns[pi].example[k..].to_vec())) } #[cfg(test)] @@ -379,7 +383,8 @@ mod tests { ok("git pull", "/b"), ]; let p = &detect_patterns(&history, &InferConfig::default())[0]; - assert_eq!(p.directories, vec!["/home", "/work"]); + // El directorio de cada ocurrencia es el de su último comando. + assert_eq!(p.directories, vec!["/a", "/b"]); } #[test] @@ -415,7 +420,7 @@ mod tests { let patterns = pattern_world(); // El usuario acaba de hacer `cd` → se predicen los pasos que faltan. let recent = vec![ok("cd /nuevo", "/h")]; - let next = predict_next(&recent, &patterns).unwrap(); + let (_, next) = predict_next(&recent, &patterns).unwrap(); assert_eq!(next, vec!["git pull", "cargo build"]); } @@ -423,7 +428,7 @@ mod tests { fn prediction_shrinks_as_the_pattern_advances() { let patterns = pattern_world(); let recent = vec![ok("cd /nuevo", "/h"), ok("git pull", "/nuevo")]; - let next = predict_next(&recent, &patterns).unwrap(); + let (_, next) = predict_next(&recent, &patterns).unwrap(); assert_eq!(next, vec!["cargo build"]); }