Skip to content

Commit cb1db93

Browse files
committed
feat(differ): add SymbolDiff types and AstDiffer for structural diffs
Introduces ChangeDetail enum (15 variants) and AstDiffer that compares old/new tree-sitter nodes for parameter, return type, visibility, async, and body changes. Struct/enum diffing stubbed for future implementation. Body comparison uses whitespace-stripped character streams.
1 parent e45cffb commit cb1db93

4 files changed

Lines changed: 561 additions & 0 deletions

File tree

src/domain/diff.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-FileCopyrightText: 2026 Sephyi <me@sephy.io>
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
4+
5+
use std::path::PathBuf;
6+
7+
/// Structured description of how a symbol changed between old and new versions.
8+
#[derive(Debug, Clone)]
9+
#[allow(dead_code)]
10+
pub struct SymbolDiff {
11+
pub name: String,
12+
pub file: PathBuf,
13+
pub line: usize,
14+
pub parent_scope: Option<String>,
15+
pub changes: Vec<ChangeDetail>,
16+
}
17+
18+
/// A single semantic change within a symbol.
19+
#[derive(Debug, Clone, PartialEq)]
20+
#[allow(dead_code)]
21+
pub enum ChangeDetail {
22+
ParamAdded(String),
23+
ParamRemoved(String),
24+
ParamTypeChanged {
25+
name: String,
26+
old_type: String,
27+
new_type: String,
28+
},
29+
ReturnTypeChanged {
30+
old: String,
31+
new: String,
32+
},
33+
VisibilityChanged {
34+
old: Option<String>,
35+
new: Option<String>,
36+
},
37+
AttributeAdded(String),
38+
AttributeRemoved(String),
39+
AsyncChanged(bool),
40+
GenericChanged {
41+
old: String,
42+
new: String,
43+
},
44+
BodyModified {
45+
additions: usize,
46+
deletions: usize,
47+
},
48+
BodyUnchanged,
49+
FieldAdded(String),
50+
FieldRemoved(String),
51+
FieldTypeChanged {
52+
name: String,
53+
old_type: String,
54+
new_type: String,
55+
},
56+
}
57+
58+
#[allow(dead_code)]
59+
impl SymbolDiff {
60+
/// Format as a concise one-line description for the LLM prompt.
61+
#[must_use]
62+
pub fn format_oneline(&self) -> String {
63+
let scope = self
64+
.parent_scope
65+
.as_ref()
66+
.map(|s| format!("{s}::"))
67+
.unwrap_or_default();
68+
let changes: Vec<String> = self.changes.iter().map(|c| c.format_short()).collect();
69+
format!(" {scope}{}(): {}", self.name, changes.join(", "))
70+
}
71+
}
72+
73+
#[allow(dead_code)]
74+
impl ChangeDetail {
75+
#[must_use]
76+
pub fn format_short(&self) -> String {
77+
match self {
78+
Self::ParamAdded(p) => format!("+param {p}"),
79+
Self::ParamRemoved(p) => format!("-param {p}"),
80+
Self::ParamTypeChanged {
81+
name,
82+
old_type,
83+
new_type,
84+
} => {
85+
format!("param {name} {old_type} \u{2192} {new_type}")
86+
}
87+
Self::ReturnTypeChanged { old, new } => format!("return {old} \u{2192} {new}"),
88+
Self::VisibilityChanged { old, new } => format!(
89+
"visibility {} \u{2192} {}",
90+
old.as_deref().unwrap_or("private"),
91+
new.as_deref().unwrap_or("private")
92+
),
93+
Self::AttributeAdded(a) => format!("+attr {a}"),
94+
Self::AttributeRemoved(a) => format!("-attr {a}"),
95+
Self::AsyncChanged(is_async) => {
96+
if *is_async {
97+
"+async".into()
98+
} else {
99+
"-async".into()
100+
}
101+
}
102+
Self::GenericChanged { old, new } => format!("generics {old} \u{2192} {new}"),
103+
Self::BodyModified {
104+
additions,
105+
deletions,
106+
} => format!("body modified (+{additions} -{deletions})"),
107+
Self::BodyUnchanged => "signature only".into(),
108+
Self::FieldAdded(f) => format!("+field {f}"),
109+
Self::FieldRemoved(f) => format!("-field {f}"),
110+
Self::FieldTypeChanged {
111+
name,
112+
old_type,
113+
new_type,
114+
} => {
115+
format!("field {name} {old_type} \u{2192} {new_type}")
116+
}
117+
}
118+
}
119+
}

src/domain/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod change;
66
mod commit;
77
mod context;
8+
pub mod diff;
89
mod symbol;
910

1011
pub use change::*;

0 commit comments

Comments
 (0)