use std::sync::Arc;
use axum::{extract::State, response::Json};
use serde::Deserialize;
use crate::service::events::{AnalyzerAppState, ApiError};
#[derive(Debug, Deserialize)]
pub struct DeepAnalyzeRequest {
pub index_id: String,
#[serde(default)]
pub report: Option<crate::core::ReviewReport>,
#[serde(default)]
pub model: Option<String>,
}
pub async fn deep_analyze_handler(
State(state): State<Arc<AnalyzerAppState>>,
Json(req): Json<DeepAnalyzeRequest>,
) -> Result<Json<crate::core::DeepAnalysisReport>, ApiError> {
if req.index_id.trim().is_empty() {
return Err(ApiError::bad_request("missing required 'index_id' field"));
}
let effective_model = req
.model
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or(&state.llm_model);
let uses_bedrock = effective_model.starts_with(crate::core::explain::BEDROCK_MODEL_PREFIX);
let api_key = if uses_bedrock {
None
} else {
let key = state.api_key.as_deref().filter(|s| !s.is_empty());
if key.is_none() {
return Err(ApiError::bad_request(
"OPENROUTER_API_KEY is not configured on the daemon; \
set OPENROUTER_API_KEY in the environment and restart the daemon, \
or use a bedrock/<model-id> model instead",
));
}
key
};
let report = match req.report {
Some(r) => r,
None => synthesise_review_from_index(&state, &req.index_id).await?,
};
let frameworks = lookup_frameworks(&state, &req.index_id);
let model_override = req.model.as_deref();
let report = crate::core::deep_analysis(
&req.index_id,
report,
frameworks,
api_key,
model_override.or(Some(&state.llm_model)),
)
.await
.map_err(|e| match e {
crate::core::DeepAnalysisError::MissingApiKey => ApiError::bad_request(format!("{e}")),
crate::core::DeepAnalysisError::BedrockAuth => ApiError::bad_request(format!("{e}")),
crate::core::DeepAnalysisError::Chat(_) => ApiError::bad_gateway(format!("{e}")),
})?;
Ok(Json(report))
}
async fn synthesise_review_from_index(
state: &AnalyzerAppState,
index_id: &str,
) -> Result<crate::core::ReviewReport, ApiError> {
let chunks = state.search.get_chunks(index_id).await.map_err(|e| {
ApiError::bad_gateway(format!("get_chunks({index_id}) for deep analysis: {e:#}"))
})?;
Ok(synthesise_review_from_chunks(&chunks))
}
pub(crate) fn synthesise_review_from_chunks(
chunks: &[crate::types::CodeChunk],
) -> crate::core::ReviewReport {
use crate::core::complexity::{compute_complexity_for, detect_smells};
use crate::core::review::{FileReview, ReviewComplexity, ReviewSource, SmellHit};
use crate::types::complexity::CodeSmell;
use std::collections::BTreeMap;
fn project(s: &CodeSmell) -> (&'static str, &'static str) {
match s {
CodeSmell::LongFunction { .. } => ("long_method", "medium"),
CodeSmell::DeepNesting { .. } => ("deep_nesting", "high"),
CodeSmell::TooManyParams { .. } => ("too_many_params", "medium"),
CodeSmell::MissingDocstring => ("missing_docstring", "low"),
}
}
let mut by_file: BTreeMap<String, Vec<&crate::types::CodeChunk>> = BTreeMap::new();
for c in chunks {
by_file.entry(c.file.clone()).or_default().push(c);
}
let mut files: Vec<FileReview> = Vec::with_capacity(by_file.len());
let mut total_smells = 0usize;
let mut total_lines = 0usize;
for (path, group) in by_file {
let joined: String = group
.iter()
.map(|c| c.content.as_str())
.collect::<Vec<_>>()
.join("\n");
let lang = super::lang_for_extension(&path);
let metrics = compute_complexity_for(&joined, lang);
let raw_smells = detect_smells(&joined);
let smells: Vec<SmellHit> = raw_smells
.iter()
.map(|s| {
let (category, severity) = project(s);
SmellHit {
category: category.to_string(),
line: group.first().map(|c| c.start_line as u32).unwrap_or(0),
severity: severity.to_string(),
}
})
.collect();
total_smells += smells.len();
total_lines += joined.lines().count();
files.push(FileReview {
path,
grade: metrics.grade,
complexity: ReviewComplexity {
cyclomatic: metrics.cyclomatic,
cognitive: metrics.cognitive,
},
smells,
recommendations: Vec::new(),
source: ReviewSource::NewFile,
});
}
let overall_grade = files
.iter()
.map(|f| f.grade)
.max()
.unwrap_or(crate::types::ComplexityGrade::A);
let summary = format!(
"{} file(s) synthesised from index corpus; {} smell(s); overall grade {}",
files.len(),
total_smells,
overall_grade
);
crate::core::ReviewReport {
files,
overall_grade,
changed_lines: total_lines,
smell_count: total_smells,
summary,
}
}
pub(crate) fn lookup_frameworks(state: &AnalyzerAppState, index_id: &str) -> Vec<String> {
use std::collections::BTreeSet;
let Ok(hits) = state
.facts
.query(Some(index_id), Some("uses_framework"), None)
else {
return Vec::new();
};
let mut names: BTreeSet<String> = BTreeSet::new();
for fact in hits {
names.insert(fact.object);
}
names.into_iter().collect()
}