ctxpack 0.1.0

Configuration-driven source packing for LLM context, with a versioning protocol for incremental patch/replace
use super::diff::DiffHunk;
use std::fmt::Write as _;

pub fn generate_patch(
    fid: u32,
    generation: u32,
    pid: u32,
    hunks: &[DiffHunk],
    prefix: &str,
    anchor_interval: u32,
) -> String {
    let mut out = String::with_capacity(hunks.len() * 96 + 64);
    let _ =
        writeln!(out, "<{p}:patch fid=\"{fid}\" gen=\"{generation}\" pid=\"{pid}\">", p = prefix);

    for hunk in hunks {
        let anchor_line = nearest_anchor_line(hunk.old_start, anchor_interval);

        let _ = writeln!(out, "@@ anchor:{anchor_line} @@");

        for ctx in &hunk.context_before {
            let _ = writeln!(out, " {ctx}");
        }
        for rem in &hunk.removes {
            out.push_str(rem);
            out.push('\n');
        }
        for add in &hunk.adds {
            out.push_str(add);
            out.push('\n');
        }
        for ctx in &hunk.context_after {
            let _ = writeln!(out, " {ctx}");
        }
    }

    let _ = write!(out, "</{p}:patch>", p = prefix);
    out
}

fn nearest_anchor_line(line: u32, anchor_interval: u32) -> u32 {
    if line == 0 {
        return 1;
    }
    if anchor_interval == 0 {
        return line;
    }

    let base = (line / anchor_interval) * anchor_interval;

    base.max(1)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::version::diff::{DiffHunk, compute_line_diff};

    #[test]
    fn test_generate_patch_basic() {
        let old = "line1\nline2\nline3\n";
        let new = "line1\nchanged\nline3\n";
        let hunks = compute_line_diff(old, new);
        let patch = generate_patch(1, 0, 1, &hunks, "ctx", 10);
        assert!(patch.contains("<ctx:patch fid=\"1\" gen=\"0\" pid=\"1\">"));
        assert!(patch.contains("@@ anchor:"));
        assert!(patch.contains("-line2") || patch.contains("line2"));
        assert!(patch.contains("+changed") || patch.contains("changed"));
        assert!(patch.contains("</ctx:patch>"));
    }

    #[test]
    fn test_generate_patch_no_anchor() {
        let hunks = vec![DiffHunk {
            old_start: 5,
            removes: vec!["-old line".to_string()],
            adds: vec!["+new line".to_string()],
            context_before: vec!["ctx".to_string()],
            context_after: vec![],
        }];
        let patch = generate_patch(2, 1, 3, &hunks, "ctx", 0);
        assert!(patch.contains("fid=\"2\" gen=\"1\" pid=\"3\""));
        assert!(patch.contains("@@ anchor:5 @@"));
    }

    #[test]
    fn test_anchor_line_boundaries() {
        assert_eq!(nearest_anchor_line(1, 10), 1);
        assert_eq!(nearest_anchor_line(9, 10), 1);
        assert_eq!(nearest_anchor_line(10, 10), 10);
        assert_eq!(nearest_anchor_line(11, 10), 10);
        assert_eq!(nearest_anchor_line(20, 10), 20);
        assert_eq!(nearest_anchor_line(20, 0), 20);
    }
}