Skip to main content

lean_ctx/core/gateway/adapters/
codebase_pack.rs

1//! codebase-pack adapter (#1097): Repomix `pack_codebase` → a lean-ctx archive
2//! handle.
3//!
4//! Repomix returns a one-shot summary (`directoryStructure`, totals) plus an
5//! `outputId` the agent later reads via `read_repomix_output`/
6//! `grep_repomix_output`. This adapter additionally persists the verbatim pack
7//! into the content-addressed archive, so the agent can retrieve any slice
8//! through the *single* lean-ctx path (`ctx_expand`) — the repomix `outputId`
9//! stays surfaced for grep. lean-ctx becomes the unified retrieval layer.
10//!
11//! Deterministic (#498): the archive id is a content hash and the returned
12//! summary is a pure function of the pack output.
13
14use crate::core::tokens::count_tokens;
15
16/// Repomix tools whose result carries a packed `outputId`.
17const PACK_TOOLS: [&str; 2] = ["pack_codebase", "pack_remote_repository"];
18
19/// If `text` is a Repomix pack result, archive it verbatim and return a compact
20/// summary + a `ctx_expand` retrieval handle. Returns `None` for non-pack tools,
21/// non-JSON output, or when archiving is unavailable (caller falls back).
22#[must_use]
23pub fn transform(server: &str, tool: &str, text: &str, budget_tokens: usize) -> Option<String> {
24    if !PACK_TOOLS.contains(&tool) {
25        return None;
26    }
27    let v: serde_json::Value = serde_json::from_str(text.trim()).ok()?;
28    let output_id = v.get("outputId").and_then(serde_json::Value::as_str)?;
29
30    let id = crate::core::archive::store(&format!("gateway:{server}::{tool}"), tool, text, None)?;
31    let tokens = count_tokens(text);
32
33    let mut summary = String::new();
34    if let Some(dir) = v
35        .get("directoryStructure")
36        .and_then(serde_json::Value::as_str)
37    {
38        summary.push_str("directoryStructure:\n");
39        summary.push_str(dir.trim_end());
40        summary.push('\n');
41    }
42    for key in ["totalFiles", "totalTokens", "totalCharacters"] {
43        if let Some(n) = v.get(key) {
44            summary.push_str(&format!("{key}: {n}\n"));
45        }
46    }
47    // Keep the (possibly large) directory tree within budget; deterministic.
48    let summary = super::super::postprocess::compress::to_budget(&summary, budget_tokens);
49
50    Some(format!(
51        "{summary}\nrepomix outputId: {output_id} \
52         (grep via: ctx_tools call {server}::grep_repomix_output)\n{}",
53        crate::core::archive::format_hint(&id, text.len(), tokens)
54    ))
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    fn pack_json() -> String {
62        serde_json::json!({
63            "outputId": "rmx_abc123",
64            "directoryStructure": "src/\n  main.rs\n  lib.rs\n",
65            "totalFiles": 2,
66            "totalTokens": 1234
67        })
68        .to_string()
69    }
70
71    #[test]
72    fn ignores_non_pack_tools() {
73        assert!(transform("repomix", "grep_repomix_output", &pack_json(), 2000).is_none());
74    }
75
76    #[test]
77    fn ignores_non_json() {
78        assert!(transform("repomix", "pack_codebase", "not json", 2000).is_none());
79    }
80
81    #[test]
82    fn pack_becomes_handle_with_outputid() {
83        let _lock = crate::core::data_dir::test_env_lock();
84        let tmp = tempfile::tempdir().unwrap();
85        crate::test_env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
86        crate::test_env::set_var("LEAN_CTX_ARCHIVE", "1");
87
88        let out = transform("repomix", "pack_codebase", &pack_json(), 2000).expect("transform");
89        assert!(out.contains("rmx_abc123"), "surfaces repomix outputId");
90        assert!(
91            out.contains("ctx_expand"),
92            "offers lean-ctx retrieval handle"
93        );
94        assert!(
95            out.contains("directoryStructure"),
96            "keeps the structure summary"
97        );
98
99        // Deterministic across calls (#498).
100        let again = transform("repomix", "pack_codebase", &pack_json(), 2000).unwrap();
101        assert_eq!(out, again);
102
103        crate::test_env::remove_var("LEAN_CTX_ARCHIVE");
104        crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
105    }
106}