use std::sync::Arc;
use forge::budget::estimator::TokenEstimator;
use forge::signal::compactor;
use regex::Regex;
use super::Recipe;
use crate::lenses::clarity::ClarityLens;
use crate::lenses::depth::DepthLens;
use crate::lenses::focus::FocusLens;
use crate::lenses::narrow::NarrowLens;
use crate::lenses::wide::WideLens;
use crate::lenses::{Lens, LensContext};
use crate::mesh::node::{FallbackPolicy, MeshNode};
pub struct CompiledRecipe {
pub recipe: Recipe,
pub matcher: Regex,
strip_res: Vec<Regex>,
replace_res: Vec<(Regex, String)>,
}
impl CompiledRecipe {
pub fn compile(recipe: Recipe) -> anyhow::Result<Self> {
let matcher = Regex::new(&recipe.match_command)?;
let mut strip_res = Vec::new();
for pat in &recipe.strip_lines {
strip_res.push(Regex::new(pat)?);
}
let mut replace_res = Vec::new();
for rule in &recipe.replace {
replace_res.push((Regex::new(&rule.from)?, rule.to.clone()));
}
Ok(Self {
recipe,
matcher,
strip_res,
replace_res,
})
}
pub fn matches(&self, command: &str) -> bool {
self.matcher.is_match(command)
}
pub fn apply(&self, raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let mut lines: Vec<String> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty() && !self.strip_res.iter().any(|re| re.is_match(t))
})
.map(|l| l.to_string())
.collect();
for line in &mut lines {
for (re, rep) in &self.replace_res {
*line = re.replace_all(line, rep.as_str()).into_owned();
}
}
if let Some(budget) = self.recipe.budget_tokens {
while !lines.is_empty() {
let current = lines.join("\n");
if TokenEstimator::count_nonblocking(¤t) <= budget {
break;
}
lines.pop();
}
}
let result = lines.join("\n");
if result.trim().is_empty() {
if let Some(msg) = &self.recipe.on_empty {
return msg.clone();
}
}
result
}
pub fn to_mesh_node(self) -> MeshNode {
let arc = Arc::new(self);
let name = arc.recipe.name.clone();
let lens_stack: Vec<Box<dyn Lens>> = arc
.recipe
.lens
.iter()
.filter_map(|l| lens_by_name(l.as_str()))
.collect();
let arc_match = Arc::clone(&arc);
let matcher: crate::mesh::node::MatcherFn = Box::new(move |prog: &str, args: &[String]| {
let full = format!("{} {}", prog, args.join(" "));
arc_match.matches(full.trim())
});
let arc_pre = Arc::clone(&arc);
let preprocessor: crate::mesh::node::PreprocessorFn =
Box::new(move |raw: &str, _ctx: &LensContext| arc_pre.apply(raw));
MeshNode {
name,
matcher,
lens_stack,
fallback: FallbackPolicy::Passthrough,
preprocessor: Some(preprocessor),
}
}
}
fn lens_by_name(name: &str) -> Option<Box<dyn Lens>> {
match name {
"clarity" => Some(Box::new(ClarityLens)),
"focus" => Some(Box::new(FocusLens)),
"narrow" => Some(Box::new(NarrowLens)),
"depth" => Some(Box::new(DepthLens)),
"wide" => Some(Box::new(WideLens)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recipe::RecipeReplace;
fn make_recipe(
strip_lines: Vec<String>,
replace: Vec<RecipeReplace>,
budget: Option<usize>,
on_empty: Option<String>,
) -> CompiledRecipe {
CompiledRecipe::compile(Recipe {
name: "test".into(),
match_command: "my-tool build".into(),
lens: vec![],
budget_tokens: budget,
strip_lines,
replace,
on_empty,
scan_secrets: true,
})
.unwrap()
}
#[test]
fn strips_matching_lines() {
let c = make_recipe(vec!["^Downloading.*".into()], vec![], None, None);
let raw = "Downloading foo.tar.gz\nDownloading bar.tar.gz\nInstalled 2 packages\n";
let out = c.apply(raw);
assert!(!out.contains("Downloading"), "{out}");
assert!(out.contains("Installed"), "{out}");
}
#[test]
fn applies_replace_rules() {
let c = make_recipe(
vec![],
vec![RecipeReplace {
from: r"error\[E\d+\]".into(),
to: "ERROR".into(),
}],
None,
None,
);
let raw = "error[E0001]: something went wrong\n";
let out = c.apply(raw);
assert!(out.contains("ERROR: something went wrong"), "{out}");
assert!(!out.contains("E0001"), "{out}");
}
#[test]
fn respects_budget_tokens() {
let c = make_recipe(vec![], vec![], Some(10), None);
let raw: String = (0..50)
.map(|i| format!("line {i} with padding text\n"))
.collect();
let out = c.apply(&raw);
assert!(TokenEstimator::count_nonblocking(&out) <= 10, "{out}");
}
#[test]
fn on_empty_fallback() {
let c = make_recipe(
vec![".*".into()],
vec![],
None,
Some("my-tool: completed successfully".into()),
);
let raw = "some noisy line that gets stripped\n";
let out = c.apply(raw);
assert_eq!(out, "my-tool: completed successfully");
}
#[test]
fn matcher_checks_full_command() {
let c = CompiledRecipe::compile(Recipe {
name: "x".into(),
match_command: r"^my-tool build".into(),
lens: vec![],
budget_tokens: None,
strip_lines: vec![],
replace: vec![],
on_empty: None,
scan_secrets: true,
})
.unwrap();
assert!(c.matches("my-tool build --release"));
assert!(!c.matches("other-tool build"));
}
#[test]
fn to_mesh_node_roundtrip() {
let recipe = Recipe {
name: "roundtrip".into(),
match_command: "mytool run".into(),
lens: vec!["clarity".into()],
budget_tokens: None,
strip_lines: vec!["^DEBUG.*".into()],
replace: vec![],
on_empty: None,
scan_secrets: true,
};
let node = CompiledRecipe::compile(recipe).unwrap().to_mesh_node();
assert_eq!(node.name, "roundtrip");
assert!((node.matcher)("mytool", &["run".into(), "--flag".into()]));
assert!(!(node.matcher)("other", &["run".into()]));
let ctx = LensContext::new(2000);
let out = node.preprocessor.as_ref().unwrap()("DEBUG: verbose\nResult: ok\n", &ctx);
assert!(!out.contains("DEBUG"), "{out}");
assert!(out.contains("Result"), "{out}");
}
}