# Semantic Diff *Why diffing trees beats diffing lines — and what that unlocks.* Every version control system in widespread use today answers the same question: "what changed between two versions of this file?" Git, Mercurial, SVN, Perforce — they all answer it by looking at lines of text. A line that exists in version A but not in version B is a deletion. A line that exists in B but not in A is an addition. Anything more subtle than that — a rename, a reformat, a reorder — is inferred later, if at all, by heuristics. Aura answers the same question differently. It parses both versions into an abstract syntax tree using tree-sitter, then diffs the trees. The atomic unit of change is not a line — it is a node. A function. An expression. A type. A binding. This is not a cosmetic improvement. It changes what a diff can see, what a merge can resolve, and what a pre-commit hook can enforce. > A text diff tells you which characters moved. A semantic diff tells you which ideas moved. ## The three cases text diff gets wrong Git's line-based diff fails predictably in three scenarios. Each of them is a daily occurrence in real codebases. ### Reformatting You run `rustfmt` or `prettier` on a file. No behavior changed. No function was added, removed, or altered in meaning. But the line-by-line view explodes into a sea of red and green. ```rust // Before fn calculate(x: i32, y: i32) -> i32 { x + y } // After rustfmt fn calculate(x: i32, y: i32) -> i32 { x + y } ``` Git sees: one line deleted, three lines added. Aura sees: no change. The AST is identical — same function node, same parameters, same body expression. ### Variable renames inside a function You rename a local variable from `usr` to `user` for clarity. The function's signature is unchanged. Its behavior is unchanged. ```typescript // Before function greet(usr: User) { console.log(`Hello, ${usr.name}`); return usr.id; } // After function greet(user: User) { console.log(`Hello, ${user.name}`); return user.id; } ``` Git's diff: three lines changed. Aura's diff: one rename of a binding, all references updated in lockstep. The reviewer sees a single, coherent refactor instead of a scatter of edits. ### Function reordering You move `helper()` above `main()` because you prefer the file organized that way. No code changed. Git shows the entire function deleted from one place and added to another. Aura recognizes this as a reorder. The function node still exists with the same [content hash](/function-level-identity). Its position in the file changed; its identity did not. ## Git vs Aura, side by side | Scenario | Git diff shows | Aura diff shows | |---|---|---| | Reformat with `rustfmt` | Every reformatted line as changed | No change | | Rename `usr` → `user` in one function | N scattered line edits | One rename, N automatic references | | Move `helper()` above `main()` | Full delete + full add | One reorder | | Rename file `auth.rs` → `login.rs` | New file + deleted file | One move, history preserved | | Extract 10 lines into a new function | 10 deletions + a function add | One extract, cross-referenced | | Change `i32` to `i64` in a signature | One line diff | Type change propagated through the call graph | The right column is what a careful human reviewer does in their head when reading the left column. Aura does it in the tool so the reviewer does not have to. ## What semantic diff enables Once the unit of change is a node, several things that are hard or impossible with text diffs become natural. **Rename-proof history.** A function renamed from `login()` to `authenticate()` keeps the same content hash (modulo the identifier). Aura links the two and preserves the entire commit history through the rename. No `git log --follow` dance. See [rename-proof identity](/rename-proof-identity). **Intent validation.** The pre-commit hook can look at the AST delta — "you deleted two functions, added one, and modified three" — and compare that to your [logged intent](/intent-tracking) — "refactored retry logic to use exponential backoff." If the delta contradicts the intent, the commit is flagged. A text diff cannot do this; "you changed 47 lines" is too coarse to validate against a sentence of English. **Safer merges.** When two branches both edit the same function, a text merge sees a line-by-line conflict. An AST merge can often resolve it — if one branch renamed a variable and the other added a `null` check, those changes are on different AST nodes and compose cleanly. See [AST merge](/ast-merge). **Surgical rewind.** Because every function has a stable identity, Aura can revert a single function to its last safe state without touching anything else in the file. `aura rewind authenticate` is a one-node operation, not a cherry-pick dance. ## The anatomy of a semantic diff A semantic diff in Aura is a set of typed operations over AST nodes, not a sequence of line edits. At a conceptual level: ```json { "operations": [ { "op": "modify", "kind": "function", "name": "retry_request", "body_hash_before": "a7c1...", "body_hash_after": "d92f..." }, { "op": "rename", "kind": "function", "from": "login", "to": "authenticate" }, { "op": "move", "kind": "function", "name": "helper", "from": "utils.rs", "to": "helpers/mod.rs" }, { "op": "add", "kind": "function", "name": "validate_token" } ] } ``` This is the representation a reviewer actually wants. It answers the review questions directly: what functions changed behavior, what got renamed, what moved, what is new. No mental translation from lines to meaning. ## How Aura computes the diff The high-level pipeline, kept abstract because the internals evolve: 1. Parse each version with the appropriate tree-sitter grammar. Every supported language — Rust, TypeScript, Python, Go, and others — reduces to the same uniform AST interface. 2. Normalize the tree. Whitespace, trivia, and formatting-only tokens are stripped from the comparison; they never contribute to identity. 3. Assign content hashes to every named node — functions, classes, methods, top-level constants. See [content-addressed logic](/content-addressed-logic). 4. Run a tree edit-distance algorithm tuned for program structure. It produces node-level operations: add, delete, modify, rename, move, reorder. 5. Attach semantic metadata — impacted callers, changed signatures, touched types — so downstream tools (review, merge, intent validation) have the information they need. Steps 1–3 guarantee that cosmetic edits are invisible. Step 4 does the structural matching. Step 5 is what makes the result useful for humans and for the [pre-commit hook](/pre-commit-hook-explained). ## A worked example Consider this change on a Rust file: ```rust // Before pub fn process(data: &[u8]) -> Result { let s = std::str::from_utf8(data)?; Ok(s.to_uppercase()) } pub fn helper() -> i32 { 42 } // After pub fn helper() -> i32 { 42 } pub fn process(input: &[u8]) -> Result { let text = std::str::from_utf8(input)?; Ok(text.to_uppercase()) } ``` A line-based diff shows both functions as heavily modified. A semantic diff shows: - `helper` — reordered, no body change. - `process` — parameter renamed `data` → `input`, local renamed `s` → `text`, behavior unchanged. A reviewer can approve this in five seconds. A text diff forces them to re-read both functions to confirm they did not change. ## What semantic diff does not try to do Aura is not a formal verifier. Two functions with different ASTs may be behaviorally equivalent (`x + 1` vs `1 + x`), and Aura does not try to prove that. Conversely, two functions with identical ASTs are guaranteed to behave identically — a guarantee a text diff cannot offer. Aura also does not replace your language's type checker or your tests. It tells you precisely what changed at the structural level; it is up to the compiler and the test suite to tell you whether those changes were correct. What Aura offers is a faithful, reviewable description of the change — one that matches the way programmers actually think about their code. ## Why this is the foundation Every other idea in Aura — [function-level identity](/function-level-identity), [intent tracking](/intent-tracking), [AST merge](/ast-merge), [shadow branches](/shadow-branches), the [pre-commit hook](/pre-commit-hook-explained) — rests on semantic diff. Once the diff knows what changed in the program (not in the text), everything downstream becomes tractable. You can validate intent because you can describe intent at the right granularity. You can rewind a function because you can identify a function across commits. You can merge cleanly because you can reason about changes as operations on a tree. Line-based diffs were the right answer in 1972, when storage was tight and parsers were expensive. They are the wrong primitive for 2026, when every language has a fast incremental parser and every codebase has a thousand collaborators — human and machine. ## Related For how Aura uses AST diffs to merge branches without textual conflicts, see [AST merge](/ast-merge). For the stable identifiers that make all of this coherent across commits, see [function-level identity](/function-level-identity) and [content-addressed logic](/content-addressed-logic).