|
| 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 | +} |
0 commit comments