cargo-context-core 0.5.0

Core engine for cargo-context: assembles, budgets, and scrubs Rust project context packs for LLMs.
Documentation
use std::collections::HashSet;
use std::path::Path;

use crate::collect::{self, Diagnostics, Diff, EntryPoints, RelatedTests, WorkspaceMap};
use crate::scrub::Scrubber;
use crate::tokenize::Tokenizer;

use super::Section;

pub(super) fn mk_section(name: &str, content: &str, tokenizer: &Tokenizer) -> Section {
    Section {
        name: name.into(),
        content: content.into(),
        token_estimate: tokenizer.count(content),
    }
}

pub(super) fn project_name(root: Option<&std::path::Path>) -> String {
    root.and_then(|p| p.file_name())
        .map(|n| n.to_string_lossy().into_owned())
        .unwrap_or_else(|| "unknown".to_string())
}

pub(crate) fn lang_for_path(path: &Path) -> &'static str {
    match path.extension().and_then(|e| e.to_str()) {
        Some("rs") => "rust",
        Some("toml") => "toml",
        Some("yaml" | "yml") => "yaml",
        Some("json") => "json",
        Some("md") => "markdown",
        Some("sh" | "bash") => "bash",
        Some("py") => "python",
        Some("ts") => "typescript",
        Some("js") => "javascript",
        _ => "",
    }
}

fn path_matches_suffix(haystack: &Path, needle: &Path) -> bool {
    let h = haystack.to_string_lossy();
    let n = needle.to_string_lossy();
    h.ends_with(n.as_ref()) || n.ends_with(h.as_ref())
}

pub(super) fn render_tests(rt: &RelatedTests) -> String {
    let mut out = String::new();
    out.push_str(&format!("{} related test file(s).\n\n", rt.files.len()));
    for f in &rt.files {
        let kind = match f.kind {
            collect::TestKind::Integration => "integration",
            collect::TestKind::UnitInline => "unit (inline)",
        };
        let reason = if f.matched_stems.is_empty() {
            String::new()
        } else {
            format!(" — matched: `{}`", f.matched_stems.join("`, `"))
        };
        out.push_str(&format!(
            "### `{}` — {} / {} ({} tests){}\n",
            f.path.display(),
            f.crate_name,
            kind,
            f.functions.len(),
            reason,
        ));
        for fun in &f.functions {
            out.push_str(&format!("- `{}`\n", fun.signature.trim()));
        }
        out.push('\n');
    }
    out
}

pub(super) fn render_entry(ep: &EntryPoints) -> String {
    let mut out = String::new();
    out.push_str(&format!("{} entry file(s).\n\n", ep.files.len()));
    for f in &ep.files {
        let kind = match f.kind {
            collect::EntryKind::Main => "main",
            collect::EntryKind::Lib => "lib",
        };
        let tag = if f.parse_failed { " (unparsed)" } else { "" };
        out.push_str(&format!(
            "### `{}` — {} / {} ({} lines){}\n",
            f.path.display(),
            f.crate_name,
            kind,
            f.raw_line_count,
            tag,
        ));
        out.push_str("```rust\n");
        out.push_str(&f.rendered);
        if !f.rendered.ends_with('\n') {
            out.push('\n');
        }
        out.push_str("```\n\n");
    }
    out
}

pub(super) fn render_map(m: WorkspaceMap) -> String {
    let mut out = String::new();
    if let Some(root) = &m.root_package {
        out.push_str(&format!("- Root package: `{root}`\n"));
    }
    let members = m.member_names();
    if !members.is_empty() {
        out.push_str(&format!("- Workspace members ({}): ", members.len()));
        out.push_str(&members.join(", "));
        out.push('\n');
    }
    let deps = m.external_dep_names();
    if !deps.is_empty() {
        let preview: Vec<&str> = deps.iter().take(12).copied().collect();
        out.push_str(&format!(
            "- Key dependencies: {}{}\n",
            preview.join(", "),
            if deps.len() > preview.len() {
                format!(" (+{} more)", deps.len() - preview.len())
            } else {
                String::new()
            }
        ));
    }
    out
}

pub(super) fn render_diff_ordered(
    d: &Diff,
    error_files: &[std::path::PathBuf],
    scrubber: &Scrubber,
) -> String {
    let error_set: HashSet<&Path> = error_files.iter().map(|p| p.as_path()).collect();

    let mut files: Vec<&collect::FileDiff> = d.files.iter().collect();
    files.sort_by_key(|f| {
        let has_error = error_set.contains(f.path.as_path())
            || error_files.iter().any(|e| path_matches_suffix(&f.path, e));
        (!has_error, f.path.to_string_lossy().into_owned())
    });

    let errored_count = files
        .iter()
        .filter(|f| {
            error_set.contains(f.path.as_path())
                || error_files.iter().any(|e| path_matches_suffix(&f.path, e))
        })
        .count();
    let path_redacted_count = files
        .iter()
        .filter(|f| scrubber.is_path_redacted(&f.path))
        .count();

    let mut out = String::new();
    let mut header = format!("{} file(s) changed", d.files.len());
    if errored_count > 0 {
        header.push_str(&format!(
            "; {errored_count} touched by compiler errors (shown first)"
        ));
    }
    if path_redacted_count > 0 {
        header.push_str(&format!("; {path_redacted_count} redacted by path rules"));
    }
    out.push_str(&format!("{header}.\n\n"));

    for f in files {
        let status = format!("{:?}", f.status).to_lowercase();
        let error_marker = if error_set.contains(f.path.as_path())
            || error_files.iter().any(|e| path_matches_suffix(&f.path, e))
        {
            ""
        } else {
            ""
        };
        let redacted = scrubber.is_path_redacted(&f.path);
        let redact_marker = if redacted { " 🔒" } else { "" };

        out.push_str(&format!(
            "### `{}` — {status}{error_marker}{redact_marker}\n",
            f.path.display()
        ));
        if let Some(old) = &f.old_path {
            out.push_str(&format!("- Renamed from `{}`\n", old.display()));
        }
        if redacted {
            out.push_str(&format!(
                "[REDACTED FILE: {}{} hunk(s) elided by scrub.yaml path rules]\n",
                f.path.display(),
                f.hunks.len()
            ));
        } else {
            for h in &f.hunks {
                out.push_str(&format!(
                    "```diff\n@@ -{},{} +{},{} @@\n{}```\n",
                    h.old_start, h.old_lines, h.new_start, h.new_lines, h.body
                ));
            }
        }
        out.push('\n');
    }
    out
}

pub(super) fn render_diagnostics(d: &Diagnostics) -> String {
    let mut out = String::new();
    let err_count = d
        .diagnostics
        .iter()
        .filter(|x| x.level == crate::collect::DiagLevel::Error)
        .count();
    out.push_str(&format!(
        "Build {}; {} diagnostic(s), {} error(s).\n\n",
        if d.success { "succeeded" } else { "failed" },
        d.diagnostics.len(),
        err_count,
    ));
    for diag in &d.diagnostics {
        let code = diag.code.as_deref().unwrap_or("");
        out.push_str(&format!(
            "- **{:?}** {}: {}\n",
            diag.level, code, diag.message
        ));
        if let Some(file) = diag.primary_file()
            && let Some(span) = diag.spans.iter().find(|s| s.is_primary)
        {
            out.push_str(&format!(
                "  at `{}:{}:{}`\n",
                file.display(),
                span.line_start,
                span.col_start
            ));
        }
    }
    out
}