lean-ctx 3.8.10

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
//! Cross-channel rule deduplication policy (#684).
//!
//! lean-ctx publishes its guidance through several "channels":
//!   * per-client global rule files (`~/.cursor/rules/lean-ctx.mdc`, …),
//!   * the shared project `AGENTS.md` (Cursor, Codex and other agents all
//!     auto-load it),
//!   * the MCP server `instructions` block (sent on every `initialize`).
//!
//! Several agents read more than one channel, so the *same* guidance can be
//! billed two or three times per session. This module centralises the policy
//! that decides, per client, which channel is the single canonical carrier — so
//! the writers (`compression` inject, hooks), the repair command
//! (`lean-ctx rules dedup`) and the honest accounting (`doctor overhead`) all
//! agree on one source of truth.

use std::path::Path;

/// Markers of the heavy compression / output-style block — the per-turn payload
/// that actually drives cross-channel duplication.
pub const COMPRESSION_BLOCK_START: &str = "<!-- lean-ctx-compression -->";
pub const COMPRESSION_BLOCK_END: &str = "<!-- /lean-ctx-compression -->";

/// The agents that auto-load the shared project `AGENTS.md`. Kept in sync with
/// `doctor::overhead::collect_rules_files`, which attributes `AGENTS.md` to the
/// same set.
pub const AGENTS_MD_READERS: &[&str] = &["cursor", "codex"];

/// True when `content` carries a *full* lean-ctx payload — the canonical rule
/// set (the `RULES_MARKER` header) or the compression/output-style block —
/// rather than just the lightweight `<!-- lean-ctx -->` cross-reference pointer.
///
/// A pointer-only file (a thinned `AGENTS.md` / `.cursorrules` that merely says
/// "the full rules live in the canonical file") does not duplicate guidance and
/// must not be counted as a second source for its client.
pub fn carries_full_rules(content: &str) -> bool {
    content.contains(crate::rules_inject::RULES_MARKER) || content.contains(COMPRESSION_BLOCK_START)
}

/// True when `content` contains a lean-ctx block but only the lightweight
/// pointer (no canonical rules, no compression payload).
pub fn is_pointer_only(content: &str) -> bool {
    content.contains("<!-- lean-ctx") && !carries_full_rules(content)
}

fn file_has_compression(path: &Path) -> bool {
    std::fs::read_to_string(path).is_ok_and(|c| c.contains(COMPRESSION_BLOCK_START))
}

/// Cursor auto-loads `~/.cursor/rules/lean-ctx.mdc`; it is "covered" for the
/// compression payload once that canonical file carries the block.
pub fn cursor_compression_covered(home: &Path) -> bool {
    file_has_compression(&home.join(".cursor/rules/lean-ctx.mdc"))
}

/// Codex's per-user config dir (`~/.codex`, or `$CODEX_HOME`).
fn codex_dir(home: &Path) -> std::path::PathBuf {
    crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"))
}

/// Codex is present on this machine when its config dir exists.
pub fn codex_present(home: &Path) -> bool {
    codex_dir(home).exists()
}

/// Codex auto-loads `~/.codex/AGENTS.md`; covered once it carries the block.
pub fn codex_compression_covered(home: &Path) -> bool {
    file_has_compression(&codex_dir(home).join("AGENTS.md"))
}

/// Decide whether the shared project `AGENTS.md` may drop its compression block
/// (keeping only the `<!-- lean-ctx -->` pointer). Safe ⇔ EVERY `AGENTS.md`
/// reader present on this machine already receives the compression payload from
/// its own canonical file.
///
/// Conservative by construction (#684, "thin only if covered"): if any reader
/// would lose the guidance, `AGENTS.md` stays the full carrier.
pub fn agents_md_can_thin(home: &Path) -> bool {
    if !cursor_compression_covered(home) {
        return false;
    }
    if codex_present(home) && !codex_compression_covered(home) {
        return false;
    }
    true
}

/// For the MCP `instructions` block: does `client_name` already auto-load the
/// compression payload from a rule file? If so, repeating the output-style
/// block in the per-session instructions is pure cross-channel duplication and
/// can be dropped (the file copy governs).
pub fn client_autoloads_compression(client_name: &str, home: &Path) -> bool {
    let lower = client_name.to_lowercase();
    if lower.is_empty() {
        return false;
    }
    if lower.contains("cursor") {
        return cursor_compression_covered(home);
    }
    if lower.contains("codex") {
        return codex_present(home) && codex_compression_covered(home);
    }
    false
}

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

    const FULL_HEADER: &str = "# lean-ctx — Context Engineering Layer";
    const COMPRESSION: &str =
        "<!-- lean-ctx-compression -->\nOUTPUT STYLE\n<!-- /lean-ctx-compression -->\n";
    const POINTER: &str = "<!-- lean-ctx -->\n## lean-ctx\nFull rules: ~/.cursor/rules/lean-ctx.mdc\n<!-- /lean-ctx -->\n";

    #[test]
    fn full_rules_detected_for_canonical_header_and_compression() {
        assert!(carries_full_rules(&format!("{FULL_HEADER}\nbody\n")));
        assert!(carries_full_rules(COMPRESSION));
        assert!(carries_full_rules(&format!("{POINTER}{COMPRESSION}")));
    }

    #[test]
    fn pointer_only_block_is_not_full() {
        assert!(!carries_full_rules(POINTER));
        assert!(is_pointer_only(POINTER));
    }

    #[test]
    fn plain_user_content_is_neither_full_nor_pointer() {
        let user = "# My project rules\njust some notes\n";
        assert!(!carries_full_rules(user));
        assert!(!is_pointer_only(user));
    }

    #[test]
    fn cursor_coverage_follows_mdc_block() {
        let tmp = tempfile::tempdir().unwrap();
        let home = tmp.path();
        assert!(!cursor_compression_covered(home));

        std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
        std::fs::write(
            home.join(".cursor/rules/lean-ctx.mdc"),
            format!("{FULL_HEADER}\n{COMPRESSION}"),
        )
        .unwrap();
        assert!(cursor_compression_covered(home));
    }

    #[test]
    fn agents_md_thins_only_when_cursor_covered_and_no_uncovered_codex() {
        // Serialize CODEX_HOME mutation (tests share the process environment).
        let _guard = crate::core::data_dir::test_env_lock();
        let tmp = tempfile::tempdir().unwrap();
        let home = tmp.path();

        // No canonical mdc yet → AGENTS.md must stay the carrier.
        crate::test_env::set_var("CODEX_HOME", home.join(".codex"));
        assert!(!agents_md_can_thin(home));

        // Cursor covered, codex absent → safe to thin (the common case).
        // CODEX_HOME points at this isolated home so a real `~/.codex` on the
        // test machine cannot leak in.
        std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
        std::fs::write(
            home.join(".cursor/rules/lean-ctx.mdc"),
            format!("{FULL_HEADER}\n{COMPRESSION}"),
        )
        .unwrap();
        assert!(agents_md_can_thin(home));

        // Codex present but uncovered → must NOT thin (codex would lose it).
        std::fs::create_dir_all(home.join(".codex")).unwrap();
        assert!(codex_present(home));
        assert!(!agents_md_can_thin(home));

        // Codex now covered by its own global AGENTS.md → safe to thin again.
        std::fs::write(home.join(".codex/AGENTS.md"), COMPRESSION).unwrap();
        assert!(agents_md_can_thin(home));
        crate::test_env::remove_var("CODEX_HOME");
    }

    #[test]
    fn client_autoloads_compression_is_client_aware() {
        let tmp = tempfile::tempdir().unwrap();
        let home = tmp.path();
        std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
        std::fs::write(
            home.join(".cursor/rules/lean-ctx.mdc"),
            format!("{FULL_HEADER}\n{COMPRESSION}"),
        )
        .unwrap();

        assert!(client_autoloads_compression("Cursor", home));
        assert!(client_autoloads_compression("cursor-vscode", home));
        // Empty / unknown clients never auto-load a file copy.
        assert!(!client_autoloads_compression("", home));
        assert!(!client_autoloads_compression("some-other-agent", home));
    }
}