lean-ctx-engine 3.12.2

LeanCTX | OSS library: the open-source runtime for the Context Gateway for AI agents behind the lean-ctx CLI, MCP server and proxy integrations.
Documentation
//! Recovery-hint policy + grammar.
//!
//! lean-ctx compression is fully reversible, but agents only act on that if we
//! *show* the escape hatch — otherwise they re-read compressed output
//! line-by-line (the "too compressed" complaint). The proactive `RECOVER` rule
//! ([`crate::core::rules_canonical::RECOVER`]) teaches the vocabulary once in the
//! system prompt; this module is the reactive half: it resolves the effective
//! [`RecoveryHints`] tier and owns the canonical, **non-MCP-first** phrasings so
//! every compressed-output site (`ctx_read`, `ctx_shell` tee, archive / firewall
//! / spill handles) renders identical, byte-stable wording.
//!
//! Determinism (#498): the tier is a pure function of profile + config + env,
//! never of per-call session state, and every phrasing is a pure function of its
//! inputs (paths/ids are content-addressed). The footer is therefore "deduped by
//! mode" — only lossy/compressed views carry it; escalating to `full`/`raw` (the
//! recovery action itself) drops it — rather than by fragile session state that
//! would break the byte-stability guards.

use crate::core::config::{Config, RecoveryHints};

/// Header line of the `Full`-tier compressed-view footer. Single source of truth:
/// also surfaced verbatim in `tdd_schema` so the published schema and the runtime
/// affordance can never drift.
pub(crate) const COMPACT_VIEW_HEADER: &str =
    "[lean-ctx: compact view — nothing lost, full source on request]";

/// Resolve the effective recovery tier for the current call.
///
/// Resolution order:
/// 1. `LEAN_CTX_RECOVERY_HINTS` env (`off|minimal|full`) — ops / test override.
/// 2. Active profile: `output_hints.compressed_hint = Some(true)` →
///    [`RecoveryHints::Full`] (the `exploration` / `review` profiles);
///    `Some(false)` → [`RecoveryHints::Off`] (explicit profile opt-out).
/// 3. Global `config.recovery_hints` (default [`RecoveryHints::Minimal`]).
#[must_use]
pub(crate) fn tier() -> RecoveryHints {
    if let Some(t) = RecoveryHints::from_env() {
        return t;
    }
    match crate::core::profiles::active_profile()
        .output_hints
        .compressed_hint
    {
        Some(true) => RecoveryHints::Full,
        Some(false) => RecoveryHints::Off,
        None => Config::load().recovery_hints,
    }
}

/// Footer appended to a compressed `ctx_read` view. Leads with the MCP-free path
/// ("read the file directly") so orgs that forbid MCP still have a route, then
/// the `ctx_*` shortcuts. `None` when the tier is `Off`.
#[must_use]
pub fn read_footer(file_path: &str) -> Option<String> {
    match tier() {
        RecoveryHints::Off => None,
        // Points at the one-call follow-ups (a symbol body or a line window)
        // before `full`: a compact view re-read in full costs more than the
        // full read the view replaced.
        RecoveryHints::Minimal => Some(format!(
            "[lean-ctx] full source: read \"{file_path}\" directly (no MCP)  ·  exact part: ctx_read same path, symbol=\"Name\" or mode=\"lines:N-M\""
        )),
        // The header line is the SSOT [`COMPACT_VIEW_HEADER`] (also in `tdd_schema`);
        // the ladder now leads with the native path before the MCP shortcuts.
        RecoveryHints::Full => Some(format!(
            "{COMPACT_VIEW_HEADER}\n  full: read \"{file_path}\" directly (no MCP)  ·  ctx_read(\"{file_path}\", mode=\"full\")  ·  exact bytes: ctx_read(\"{file_path}\", raw=true)"
        )),
    }
}

/// Canonical recovery clause for a content-addressed handle (archive / firewall /
/// spill / `ctx_shell` tee). Always names the on-disk path first (MCP-free), then
/// the `ctx_expand(id=...)` shortcut. Unlike [`read_footer`] this is *functional*
/// (it points at where the bytes live), so it is not tier-gated — but `Off`
/// collapses it to the bare path so a `recovery_hints=off` operator still gets the
/// pointer without the coaching.
#[must_use]
pub fn handle_clause(id: &str, on_disk_path: Option<&str>) -> String {
    if on_disk_path.is_some_and(|path| {
        crate::core::protected_store::enforce_protected_store_path(std::path::Path::new(path))
            .is_err()
    }) {
        return format!("full: ctx_expand(id=\"{id}\")");
    }
    match (tier(), on_disk_path) {
        (RecoveryHints::Off, Some(p)) => format!("full: {p}"),
        (RecoveryHints::Off, None) => format!("full: ctx_expand(id=\"{id}\")"),
        (_, Some(p)) => format!("full: read {p} directly (no MCP)  ·  or ctx_expand(id=\"{id}\")"),
        (_, None) => format!("full: ctx_expand(id=\"{id}\")  ·  or read the shown path"),
    }
}

/// The pointer a truncation carries to the bytes it cut (K25). The id is the
/// content-addressed archive id, so the line is a pure function of the
/// original text and stays byte-stable across repeated calls (#498).
#[must_use]
pub(crate) fn original_pointer(id: &str) -> String {
    format!("[Original available via ctx_expand id={id}]")
}

/// Archive the full `original` of a truncated view and return its
/// [`original_pointer`]. `None` when the archive may not hold it: archive off,
/// or protection active and the text has no source authority — the archive
/// itself refuses then, and the truncation keeps its existing notice. The
/// `LEAN_CTX_MINIMAL` escape hatch stores nothing either (#1540).
#[must_use]
pub(crate) fn archive_original(tool: &str, label: &str, original: &str) -> Option<String> {
    if Config::minimal_escape_hatch() {
        return None;
    }
    let to_store = crate::core::redaction::redact_text_if_enabled(original);
    crate::core::archive::store(tool, label, &to_store, None).map(|id| original_pointer(&id))
}

/// `truncated` followed by the pointer to `original`, when it can be archived.
#[must_use]
pub fn with_original_pointer(tool: &str, label: &str, original: &str, truncated: String) -> String {
    match archive_original(tool, label, original) {
        Some(pointer) => format!("{truncated}\n{pointer}"),
        None => truncated,
    }
}

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

    /// K25 / #498: the same original always yields the same pointer, and the
    /// pointer resolves to the original through `ctx_expand`'s store.
    #[test]
    fn original_pointer_is_content_addressed_and_resolves() {
        let _data = crate::core::data_dir::isolated_data_dir();
        let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
        let original = "line\n".repeat(400);
        let first = archive_original("ctx_test", "label", &original).expect("archived");
        let second = archive_original("ctx_test", "label", &original).expect("archived");
        assert_eq!(first, second, "byte-stable pointer");
        let id = first
            .strip_prefix("[Original available via ctx_expand id=")
            .and_then(|rest| rest.strip_suffix(']'))
            .expect("pointer grammar");
        assert_eq!(
            crate::core::archive::retrieve(id).as_deref(),
            Some(original.as_str())
        );
        let other = archive_original("ctx_test", "label", "different").expect("archived");
        assert_ne!(first, other);
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn protected_runtime_recovery_does_not_recommend_raw_file_access() {
        let isolated = crate::core::data_dir::isolated_data_dir();
        let artifact = isolated.path().join("tee/output.log");
        let path = artifact.to_str().unwrap();
        let _pin = crate::core::protected_store::pin_synthetic_session(isolated.path()).unwrap();
        let hint = handle_clause("fixture-archive", Some(path));
        assert!(hint.contains("ctx_expand"));
        assert!(!hint.contains("directly"));
        assert!(!hint.contains(path));
    }
    use crate::core::data_dir::test_env_lock;

    /// The env override forces a tier regardless of profile/config — the knob ops
    /// and tests use to pin behaviour.
    #[test]
    fn env_override_pins_tier() {
        let _lock = test_env_lock();
        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "off");
        assert_eq!(tier(), RecoveryHints::Off);
        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "full");
        assert_eq!(tier(), RecoveryHints::Full);
        crate::test_env::remove_var("LEAN_CTX_RECOVERY_HINTS");
    }

    #[test]
    fn read_footer_leads_with_native_path_and_respects_off() {
        let _lock = test_env_lock();
        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "off");
        assert!(
            read_footer("src/x.rs").is_none(),
            "off suppresses the footer"
        );

        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "minimal");
        let minimal = read_footer("src/x.rs").expect("minimal emits a footer");
        assert!(minimal.lines().count() == 1, "minimal is a single line");
        // Non-MCP path must come before the MCP shortcut.
        let native = minimal.find("read \"src/x.rs\" directly").unwrap();
        let mcp = minimal.find("ctx_read").unwrap();
        assert!(minimal.contains("symbol=") && minimal.contains("lines:N-M"));
        assert!(native < mcp, "native path must precede the MCP route");

        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "full");
        let full = read_footer("src/x.rs").expect("full emits a footer");
        assert!(full.contains("raw=true") && !full.contains("ctx_retrieve"));
        assert!(
            full.find("read \"src/x.rs\" directly").unwrap() < full.find("raw=true").unwrap(),
            "full ladder still leads with the native path"
        );
        crate::test_env::remove_var("LEAN_CTX_RECOVERY_HINTS");
    }

    /// Determinism (#498): the footer/clause are pure functions of their inputs,
    /// so repeated calls are byte-identical (provider prompt caching depends on it).
    #[test]
    fn footer_and_clause_are_byte_stable() {
        let _lock = test_env_lock();
        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "minimal");
        assert_eq!(read_footer("src/a.rs"), read_footer("src/a.rs"));
        assert_eq!(
            handle_clause("id1", Some("/tmp/t.log")),
            handle_clause("id1", Some("/tmp/t.log"))
        );
        crate::test_env::remove_var("LEAN_CTX_RECOVERY_HINTS");
    }

    #[test]
    fn handle_clause_is_non_mcp_first() {
        let _lock = test_env_lock();
        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "minimal");
        let with_path = handle_clause("abc123", Some("/tmp/tee/run.log"));
        assert!(
            with_path.find("/tmp/tee/run.log").unwrap() < with_path.find("ctx_expand").unwrap(),
            "on-disk path must precede ctx_expand"
        );
        let no_path = handle_clause("abc123", None);
        assert!(no_path.contains("ctx_expand(id=\"abc123\")"));

        crate::test_env::set_var("LEAN_CTX_RECOVERY_HINTS", "off");
        assert_eq!(
            handle_clause("abc123", Some("/tmp/tee/run.log")),
            "full: /tmp/tee/run.log",
            "off keeps the bare functional pointer, drops the coaching"
        );
        crate::test_env::remove_var("LEAN_CTX_RECOVERY_HINTS");
    }
}