bctx-weave 0.1.29

bctx-weave — FilterMesh lens pipeline, CLI interception, domain compression
Documentation
use super::{
    apply_stack, clarity::ClarityLens, depth::DepthLens, focus::FocusLens, harmonic::HarmonicLens,
    narrow::NarrowLens, refract::RefractLens, wide::WideLens, LensContext, LensOutput,
};

/// Named read modes that map to fixed lens stacks.
///
/// Set via `BCTX_MODE=<name>` env var or `bctx read --mode <name>`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadMode {
    /// Default: let the FilterMesh pick the lens stack (no override).
    Auto,
    /// Passthrough: no compression, exact bytes forwarded.
    Full,
    /// Wide lens only: expand context for structural overview.
    Map,
    /// Refract lens only: AST signatures, strip all bodies.
    Signatures,
    /// Depth + Clarity: preserve structure while cleaning noise.
    Diff,
    /// Clarity + Narrow: maximum token reduction.
    Aggressive,
    /// Harmonic lens: entropy-based semantic clustering.
    Entropy,
    /// Focus lens: task-conditioned relevance filtering.
    Task,
    /// Depth lens: preserve imports, type signatures, doc comments.
    Reference,
    /// Narrow lens only: budget-based head+tail truncation for large outputs.
    Truncate,
    /// Extract specific line range. Inclusive 1-based indices.
    Lines { start: usize, end: usize },
}

impl ReadMode {
    /// Parse from a string such as `"signatures"` or `"lines:10-50"`.
    pub fn parse(s: &str) -> Option<Self> {
        match s.trim().to_ascii_lowercase().as_str() {
            "auto" => Some(Self::Auto),
            "full" => Some(Self::Full),
            "map" | "wide" => Some(Self::Map),
            "signatures" | "refract" => Some(Self::Signatures),
            "narrow" | "truncate" => Some(Self::Truncate),
            "diff" | "depth" => Some(Self::Diff),
            "aggressive" | "clarity" => Some(Self::Aggressive),
            "entropy" | "harmonic" => Some(Self::Entropy),
            "task" | "focus" => Some(Self::Task),
            "reference" => Some(Self::Reference),
            other if other.starts_with("lines:") => {
                let range = &other["lines:".len()..];
                let mut parts = range.splitn(2, '-');
                let start: usize = parts.next()?.parse().ok()?;
                let end: usize = parts.next()?.parse().ok()?;
                if start == 0 || end < start {
                    return None;
                }
                Some(Self::Lines { start, end })
            }
            _ => None,
        }
    }

    /// One-line description of what this mode does.
    pub fn description(&self) -> &'static str {
        match self {
            Self::Auto => "Let the FilterMesh auto-select the best lens stack for the command",
            Self::Full => "No compression — forward exact bytes unchanged",
            Self::Map => "Structural overview: expand context, show directory/module layout",
            Self::Signatures => "AST signatures only — strip all function bodies, keep types",
            Self::Diff => "Diff-optimised: preserve structure and added/removed markers",
            Self::Aggressive => "Maximum token reduction via Clarity + Narrow pipeline",
            Self::Entropy => {
                "Entropy-scored semantic clustering — keeps highest-information chunks"
            }
            Self::Task => "Task-conditioned relevance filter — keep lines relevant to current goal",
            Self::Reference => "Import/type/doc layer — ideal for building reference context",
            Self::Truncate => "Budget-based head+tail truncation — fits any output to token limit",
            Self::Lines { .. } => "Extract an exact line range (1-based, inclusive)",
        }
    }

    /// Concrete use-case hint shown in `bctx modes`.
    pub fn use_case(&self) -> &'static str {
        match self {
            Self::Auto => "Default — bctx picks the right mode based on the command",
            Self::Full => "Debugging bctx itself; piping to another tool that needs raw output",
            Self::Map => "Understanding an unfamiliar repo; building project structure context",
            Self::Signatures => "Passing an API surface to an LLM; code review context preamble",
            Self::Diff => "Reviewing a PR; analysing git diff output",
            Self::Aggressive => "Token budget is tight; output is noisy logs or test output",
            Self::Entropy => "Long prose/markdown files where most content is boilerplate",
            Self::Task => "Agent has a specific task hint; filter output to what's relevant",
            Self::Reference => "Building a context bundle of types and imports for an LLM prompt",
            Self::Truncate => "Long log output or huge files that must fit a fixed token budget",
            Self::Lines { .. } => "Focussing on a known region of a file (e.g. a failing function)",
        }
    }

    /// Human-readable lens stack applied.
    pub fn lens_stack(&self) -> &'static str {
        match self {
            Self::Auto => "FilterMesh (dynamic)",
            Self::Full => "none (passthrough)",
            Self::Map => "Wide",
            Self::Signatures => "Refract",
            Self::Diff => "Depth → Clarity",
            Self::Aggressive => "Clarity → Narrow",
            Self::Entropy => "Harmonic",
            Self::Task => "Focus",
            Self::Reference => "Depth",
            Self::Truncate => "Narrow",
            Self::Lines { .. } => "line-range extractor",
        }
    }

    /// Rough savings estimate shown in the modes table.
    pub fn savings_estimate(&self) -> &'static str {
        match self {
            Self::Auto => "varies",
            Self::Full => "0%",
            Self::Map => "0% (full context)",
            Self::Signatures => "60–85%",
            Self::Diff => "30–55%",
            Self::Aggressive => "70–90%",
            Self::Entropy => "50–75%",
            Self::Task => "40–70%",
            Self::Reference => "35–60%",
            Self::Truncate => "20–80% (budget-dependent)",
            Self::Lines { .. } => "depends on range",
        }
    }

    /// All named (non-parameterised) modes in display order.
    pub fn all_named() -> &'static [ReadMode] {
        use std::sync::OnceLock;
        static ALL: OnceLock<Vec<ReadMode>> = OnceLock::new();
        ALL.get_or_init(|| {
            vec![
                ReadMode::Auto,
                ReadMode::Full,
                ReadMode::Map,
                ReadMode::Signatures,
                ReadMode::Diff,
                ReadMode::Aggressive,
                ReadMode::Entropy,
                ReadMode::Task,
                ReadMode::Reference,
                ReadMode::Truncate,
            ]
        })
    }

    /// Display name for the mode (used in SavesReport annotations).
    pub fn name(&self) -> String {
        match self {
            Self::Auto => "auto".into(),
            Self::Full => "full".into(),
            Self::Map => "map".into(),
            Self::Signatures => "signatures".into(),
            Self::Diff => "diff".into(),
            Self::Aggressive => "aggressive".into(),
            Self::Entropy => "entropy".into(),
            Self::Task => "task".into(),
            Self::Reference => "reference".into(),
            Self::Truncate => "narrow".into(),
            Self::Lines { start, end } => format!("lines:{start}-{end}"),
        }
    }

    /// Apply this mode to raw input, returning a `LensOutput`.
    pub fn apply(&self, input: &str, ctx: &LensContext) -> LensOutput {
        match self {
            Self::Auto => LensOutput::passthrough(input),
            Self::Full => LensOutput::passthrough(input),
            Self::Map => apply_stack(&[Box::new(WideLens)], input, ctx),
            Self::Signatures => apply_stack(&[Box::new(RefractLens)], input, ctx),
            Self::Diff => apply_stack(&[Box::new(DepthLens), Box::new(ClarityLens)], input, ctx),
            Self::Aggressive => {
                apply_stack(&[Box::new(ClarityLens), Box::new(NarrowLens)], input, ctx)
            }
            Self::Entropy => apply_stack(&[Box::new(HarmonicLens)], input, ctx),
            Self::Task => apply_stack(&[Box::new(FocusLens)], input, ctx),
            Self::Reference => apply_stack(&[Box::new(DepthLens)], input, ctx),
            Self::Truncate => apply_stack(&[Box::new(NarrowLens)], input, ctx),
            Self::Lines { start, end } => extract_lines(input, *start, *end),
        }
    }
}

fn extract_lines(input: &str, start: usize, end: usize) -> LensOutput {
    use forge::budget::estimator::TokenEstimator;
    let tokens_before = TokenEstimator::count_nonblocking(input);
    let extracted: String = input
        .lines()
        .enumerate()
        .filter(|(i, _)| {
            let line_no = i + 1;
            line_no >= start && line_no <= end
        })
        .map(|(_, line)| line)
        .collect::<Vec<_>>()
        .join("\n");
    let tokens_after = TokenEstimator::count_nonblocking(&extracted);
    LensOutput {
        content: extracted,
        tokens_before,
        tokens_after,
        applied: vec!["lines".into()],
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_all_named_modes() {
        assert_eq!(ReadMode::parse("auto"), Some(ReadMode::Auto));
        assert_eq!(ReadMode::parse("full"), Some(ReadMode::Full));
        assert_eq!(ReadMode::parse("map"), Some(ReadMode::Map));
        assert_eq!(ReadMode::parse("signatures"), Some(ReadMode::Signatures));
        assert_eq!(ReadMode::parse("diff"), Some(ReadMode::Diff));
        assert_eq!(ReadMode::parse("aggressive"), Some(ReadMode::Aggressive));
        assert_eq!(ReadMode::parse("entropy"), Some(ReadMode::Entropy));
        assert_eq!(ReadMode::parse("task"), Some(ReadMode::Task));
        assert_eq!(ReadMode::parse("reference"), Some(ReadMode::Reference));
        assert_eq!(ReadMode::parse("narrow"), Some(ReadMode::Truncate));
        assert_eq!(ReadMode::parse("truncate"), Some(ReadMode::Truncate));
    }

    #[test]
    fn parse_lines_mode() {
        assert_eq!(
            ReadMode::parse("lines:10-50"),
            Some(ReadMode::Lines { start: 10, end: 50 })
        );
        assert_eq!(
            ReadMode::parse("lines:1-1"),
            Some(ReadMode::Lines { start: 1, end: 1 })
        );
    }

    #[test]
    fn parse_lines_invalid_returns_none() {
        assert_eq!(ReadMode::parse("lines:0-10"), None); // zero start
        assert_eq!(ReadMode::parse("lines:50-10"), None); // end < start
        assert_eq!(ReadMode::parse("lines:abc"), None);
    }

    #[test]
    fn parse_unknown_returns_none() {
        assert_eq!(ReadMode::parse("unknown"), None);
        assert_eq!(ReadMode::parse(""), None);
    }

    #[test]
    fn parse_is_case_insensitive() {
        assert_eq!(ReadMode::parse("SIGNATURES"), Some(ReadMode::Signatures));
        assert_eq!(ReadMode::parse("Aggressive"), Some(ReadMode::Aggressive));
    }

    #[test]
    fn name_round_trips() {
        let modes = [
            ReadMode::Auto,
            ReadMode::Full,
            ReadMode::Map,
            ReadMode::Signatures,
            ReadMode::Diff,
            ReadMode::Aggressive,
            ReadMode::Entropy,
            ReadMode::Task,
            ReadMode::Reference,
            ReadMode::Truncate,
            ReadMode::Lines { start: 5, end: 20 },
        ];
        for mode in &modes {
            let name = mode.name();
            // All named modes except Lines round-trip through parse
            if !matches!(mode, ReadMode::Lines { .. }) {
                assert_eq!(
                    ReadMode::parse(&name),
                    Some(mode.clone()),
                    "failed for {name}"
                );
            }
        }
    }

    #[test]
    fn lines_mode_extracts_correct_range() {
        let input = "line1\nline2\nline3\nline4\nline5";
        let ctx = LensContext::new(2000);
        let out = ReadMode::Lines { start: 2, end: 4 }.apply(input, &ctx);
        assert_eq!(out.content, "line2\nline3\nline4");
    }

    #[test]
    fn lines_mode_out_of_bounds_clamps_gracefully() {
        let input = "line1\nline2";
        let ctx = LensContext::new(2000);
        let out = ReadMode::Lines { start: 1, end: 100 }.apply(input, &ctx);
        assert_eq!(out.content, "line1\nline2");
    }

    #[test]
    fn full_mode_is_passthrough() {
        let input = "hello world";
        let ctx = LensContext::new(2000);
        let out = ReadMode::Full.apply(input, &ctx);
        assert_eq!(out.content, input);
        assert_eq!(out.applied, Vec::<String>::new());
    }
}