udiffx 0.1.29

Parse and apply LLM-optimized unified diff + XML file changes
Documentation

udiffx

Parse and apply an AI-optimized “file changes” envelope that carries multiple file operations in a single block, using unified diff patches for updates.

This crate is designed for LLM output that needs to be machine-parsable and efficient for large files with small edits.

Doc for LLM

Concept, FILE_CHANGES

A response contains one root container:

  • <FILE_CHANGES> ... </FILE_CHANGES>

Inside it, you can mix multiple directives:

  • <FILE_NEW file_path="..."> ... </FILE_NEW>
  • <FILE_PATCH file_path="..."> ... </FILE_PATCH> (unified diff content)
  • <FILE_RENAME from_path="..." to_path="..." />
  • <FILE_DELETE file_path="..." />

Notes:

  • Tags are XML-like but not intended to be strictly XML-compliant.
  • The parser is tag-based. It extracts only the above tags, and content does not need XML escaping.
  • Self-closing tags like <FILE_DELETE ... /> are supported.

API overview

The crate exposes two main operations:

  • Extract: parse the first <FILE_CHANGES> block from a string.
  • Apply: execute the extracted directives against a base directory.

Key public types:

  • FileChanges, an iterable list of directives.
  • FileDirective, one directive (new, patch, rename, delete, fail).
  • ApplyChangesStatus, per-directive success and error reporting.
  • Error / Result<T>, the crate error type and alias.

Gathering file context

Use load_files_context to gather files matching globs and format them for an LLM context.

use udiffx::{load_files_context, Result};

fn main() -> Result<()> {
    let base_dir = "./my-project";
    let globs = &["src/**/*.rs", "Cargo.toml"];
    
    if let Some(context) = load_files_context(base_dir, globs)? {
        println!("{context}");
    }

    Ok(())
}

Output format:

<FILE_CONTENT path="Cargo.toml">
... content ...
</FILE_CONTENT>

<FILE_CONTENT path="src/main.rs">
... content ...
</FILE_CONTENT>

Extracting changes from text

Use extract_file_changes to parse a model response or any input string.

use udiffx::{extract_file_changes, Result};

fn main() -> Result<()> {
    let input = r#"
Some text...

<FILE_CHANGES>
<FILE_NEW file_path="src/hello.rs">
pub fn hello() { println!("Hello"); }
</FILE_NEW>

<FILE_DELETE file_path="old.txt" />
</FILE_CHANGES>
"#;

    let (changes, _extruded) = extract_file_changes(input, false)?;

    if changes.is_empty() {
        println!("No changes found");
        return Ok(());
    }

    for d in &changes {
        println!("{d:?}");
    }

    Ok(())
}

extract_content parameter:

  • extract_content = false parses tags and returns extruded = None.
  • extract_content = true also returns the input with the extracted <FILE_CHANGES> block removed as Some(String).

Applying changes to disk

Use apply_file_changes to execute directives relative to a base directory.

  • All file paths are treated as relative to base_dir.
  • The crate performs basic path safety checks to ensure operations stay within base_dir.
  • Patch application uses diffy (unified diff parsing and application).
use simple_fs::SPath;
use udiffx::{apply_file_changes, extract_file_changes, Result};

fn main() -> Result<()> {
    let base_dir = SPath::new("./my-project");

    let input = r#"
<FILE_CHANGES>
<FILE_PATCH file_path="src/main.rs">
@@ -1,3 +1,3 @@
-fn main() { println!("Hello"); }
+fn main() { println!("Hello, world"); }
</FILE_PATCH>
</FILE_CHANGES>
"#;

    let (changes, _) = extract_file_changes(input, false)?;
    let status = apply_file_changes(&base_dir, changes)?;

    for d in status.items {
        if d.success() {
            println!("OK   {} {}", d.kind(), d.file_path());
        } else {
            println!(
                "FAIL {} {}: {}",
                d.kind(),
                d.file_path(),
                d.error_msg().unwrap_or("unknown error")
            );
        }
    }

    Ok(())
}

Directive behavior

  • FILE_NEW: creates or overwrites a file. Parent directories are created.
  • FILE_PATCH: reads the target file, applies a unified diff, and writes the result back.
    • When a patch contains multiple hunks, each hunk is applied independently. If some hunks fail, the successfully applied hunks are still written. The directive is considered successful if at least one hunk applies. Per-hunk failure details are available in DirectiveStatus.error_hunks.
  • FILE_RENAME: renames or moves from_path to to_path.
  • FILE_DELETE: removes a file or directory recursively.

If extraction fails for a directive (unknown tag, missing attribute, etc.), the directive is represented as:

  • FileDirective::Fail { kind, file_path, error_msg }

When applying, Fail directives always yield an error for that directive and are reported via ApplyChangesInfo.

Format tips for LLM output

  • Always emit exactly one <FILE_CHANGES> block when you intend to apply changes.
  • Prefer FILE_PATCH for small edits to large files.
  • Use self-closing tags for rename and delete when convenient:
    • <FILE_RENAME from_path="a" to_path="b" />
    • <FILE_DELETE file_path="path" />

System prompt (optional)

The crate includes the recommended system instructions for LLMs to ensure they output the correct format. This is available via the prompt feature.

[dependencies]
udiffx = { version = "0.1", features = ["prompt"] }
use udiffx::prompt;

let instructions = prompt();
// Pass this to your LLM system message.

License

MIT OR Apache-2.0