pub mod invoke;
mod prompts;
mod spec_compare;
use anyhow::Result;
pub use invoke::{
FileApiChange, FileBehavioralChange, LlmCompositionChange, LlmConstantRenamePattern,
LlmInterfaceRenameMapping, LlmSuffixRename,
};
use semver_analyzer_core::{
BehaviorAnalyzer, BreakingVerdict, ChangedFunction, FunctionSpec, TestDiff,
};
pub struct LlmBehaviorAnalyzer {
llm_command: String,
timeout_secs: u64,
}
impl LlmBehaviorAnalyzer {
pub fn new(llm_command: &str) -> Self {
Self {
llm_command: llm_command.to_string(),
timeout_secs: 120,
}
}
pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = timeout_secs;
self
}
fn run_llm(&self, prompt: &str) -> Result<String> {
tracing::debug!(prompt_bytes = prompt.len(), "sending LLM prompt");
let result = invoke::run_llm_command(&self.llm_command, prompt, self.timeout_secs);
match &result {
Ok(response) => {
tracing::debug!(
response_bytes = response.len(),
response_tail = %&response[response.len().saturating_sub(200)..],
"LLM response received"
);
}
Err(e) => {
tracing::debug!(%e, "LLM command failed");
}
}
result
}
}
impl LlmBehaviorAnalyzer {
pub fn analyze_file_diff(
&self,
file_path: &str,
diff_content: &str,
changed_functions: &[ChangedFunction],
test_diff: Option<&str>,
) -> Result<(
Vec<FileBehavioralChange>,
Vec<FileApiChange>,
Vec<invoke::LlmCompositionChange>,
)> {
let prompt = prompts::build_file_behavioral_prompt(
file_path,
diff_content,
changed_functions,
test_diff,
);
let response = self.run_llm(&prompt)?;
let (beh, api) = invoke::parse_file_behavioral_response(&response)?;
let comp = invoke::parse_composition_from_file_response(&response).unwrap_or_default();
Ok((beh, api, comp))
}
pub fn analyze_composition_patterns(
&self,
file_path: &str,
diff_content: &str,
) -> Result<Vec<LlmCompositionChange>> {
let prompt = prompts::build_composition_pattern_prompt(file_path, diff_content);
let response = self.run_llm(&prompt)?;
invoke::parse_composition_pattern_response(&response)
}
pub fn infer_constant_renames(
&self,
removed_sample: &[&str],
added_sample: &[&str],
package_name: &str,
from_ref: &str,
to_ref: &str,
) -> Result<Vec<LlmConstantRenamePattern>> {
let prompt = prompts::build_constant_rename_prompt(
removed_sample,
added_sample,
package_name,
from_ref,
to_ref,
);
let response = self.run_llm(&prompt)?;
invoke::parse_constant_rename_response(&response)
}
pub fn infer_component_hierarchy(
&self,
family_name: &str,
files_content: &str,
related_components: Option<&str>,
) -> Result<std::collections::HashMap<String, Vec<semver_analyzer_core::ExpectedChild>>> {
let prompt = prompts::build_hierarchy_inference_prompt(
family_name,
files_content,
related_components,
);
let response = self.run_llm(&prompt)?;
invoke::parse_hierarchy_response(&response)
}
pub fn infer_suffix_renames(
&self,
removed_suffixes: &[&str],
added_suffixes: &[&str],
) -> Result<Vec<invoke::LlmSuffixRename>> {
let prompt = prompts::build_suffix_rename_prompt(removed_suffixes, added_suffixes);
let response = self.run_llm(&prompt)?;
invoke::parse_suffix_rename_response(&response)
}
pub fn infer_interface_renames(
&self,
removed: &[(&str, &[String])],
added: &[(&str, &[String])],
package_name: &str,
from_ref: &str,
to_ref: &str,
) -> Result<Vec<LlmInterfaceRenameMapping>> {
let prompt =
prompts::build_interface_rename_prompt(removed, added, package_name, from_ref, to_ref);
let response = self.run_llm(&prompt)?;
invoke::parse_interface_rename_response(&response)
}
}
impl BehaviorAnalyzer for LlmBehaviorAnalyzer {
fn infer_spec(&self, function_body: &str, signature: &str) -> Result<FunctionSpec> {
let prompt = prompts::build_spec_inference_prompt(function_body, signature);
let response = self.run_llm(&prompt)?;
invoke::parse_function_spec(&response)
}
fn infer_spec_with_test_context(
&self,
function_body: &str,
signature: &str,
test_context: &TestDiff,
) -> Result<FunctionSpec> {
let prompt =
prompts::build_spec_inference_with_test_prompt(function_body, signature, test_context);
let response = self.run_llm(&prompt)?;
invoke::parse_function_spec(&response)
}
fn specs_are_breaking(
&self,
old: &FunctionSpec,
new: &FunctionSpec,
) -> Result<BreakingVerdict> {
let tier1 = spec_compare::structural_compare(old, new);
if tier1.is_breaking || tier1.confidence >= 0.80 {
return Ok(tier1);
}
if !old.notes.is_empty() || !new.notes.is_empty() {
let prompt = prompts::build_spec_comparison_prompt(old, new);
let response = self.run_llm(&prompt)?;
return invoke::parse_breaking_verdict(&response);
}
Ok(tier1)
}
fn check_propagation(
&self,
caller_body: &str,
caller_signature: &str,
callee_name: &str,
evidence_description: &str,
) -> Result<bool> {
let prompt = prompts::build_propagation_check_prompt(
caller_body,
caller_signature,
callee_name,
evidence_description,
);
let response = self.run_llm(&prompt)?;
invoke::parse_propagation_result(&response)
}
}