pub mod invoke;
mod prompts;
mod spec_compare;
use anyhow::Result;
pub use invoke::{
FileApiChange, FileBehavioralChange, LlmConstantRenamePattern, LlmInterfaceRenameMapping,
LlmSuffixRename,
};
use semver_analyzer_core::{
BehaviorAnalyzer, BreakingVerdict, ChangedFunction, FunctionSpec, LlmCategoryDefinition,
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>,
categories: &[LlmCategoryDefinition],
) -> Result<(Vec<FileBehavioralChange>, Vec<FileApiChange>)> {
let prompt = prompts::build_file_behavioral_prompt(
file_path,
diff_content,
changed_functions,
test_diff,
categories,
);
let response = self.run_llm(&prompt)?;
invoke::parse_file_behavioral_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_hierarchy_from_prompt(
&self,
prompt: &str,
) -> Result<std::collections::HashMap<String, Vec<semver_analyzer_core::ExpectedChild>>> {
let response = self.run_llm(prompt)?;
invoke::parse_hierarchy_response(&response)
}
pub fn infer_suffix_renames_from_prompt(
&self,
prompt: &str,
) -> Result<Vec<invoke::LlmSuffixRename>> {
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)
}
}