use crate::report_types::LLMVerdict;
use crate::types::FindingTier;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SemanticEvidence {
pub(crate) start_line: usize,
pub(crate) end_line: usize,
pub(crate) quote: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SemanticMethodReview {
pub(crate) tier: FindingTier,
pub(crate) pattern: String,
pub(crate) intent: String,
pub(crate) reason: String,
pub(crate) evidence: Vec<SemanticEvidence>,
pub(crate) necessity_check: String,
}
const SEMANTIC_PATTERNS: &[&str] = &[
"intent_hidden",
"duplicated_decision_paths",
"ceremonial_logic",
"speculative_defense",
"needless_indirection",
"difficult_state_transition",
"semantic_mismatch",
"unnecessarily_complicated",
];
pub(crate) fn parse_result_fields(
result: &serde_json::Value,
) -> (FindingTier, String, String, Option<bool>, Option<bool>) {
let smelly = result
.get("smelly")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let tier = match result.get("tier").and_then(|v| v.as_str()) {
Some("slop") => FindingTier::Slop,
Some("kinda_slop") => FindingTier::KindaSlop,
Some("clean") => FindingTier::Clean,
_ if smelly => FindingTier::Slop,
_ => FindingTier::Clean,
};
let evidence = result
.get("evidence")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let reason = result
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let cohesive = result.get("cohesive").and_then(|v| v.as_bool());
let name_accurate = result.get("name_accurate").and_then(|v| v.as_bool());
(tier, evidence, reason, cohesive, name_accurate)
}
pub(crate) fn validate_file_review(result: &serde_json::Value) -> Result<(), String> {
let smelly = result
.get("smelly")
.and_then(|value| value.as_bool())
.ok_or_else(|| "file verdict is missing boolean smelly".to_string())?;
let tier = result
.get("tier")
.and_then(|value| value.as_str())
.ok_or_else(|| "file verdict is missing string tier".to_string())?;
let tier_is_smelly = match tier {
"slop" | "kinda_slop" => true,
"clean" => false,
other => return Err(format!("invalid file verdict tier: {other}")),
};
if smelly != tier_is_smelly {
return Err("file verdict smelly and tier disagree".to_string());
}
if !result
.get("evidence")
.is_some_and(serde_json::Value::is_string)
{
return Err("file verdict is missing string evidence".to_string());
}
Ok(())
}
pub(crate) fn parse_semantic_method_review(
result: &serde_json::Value,
source: &str,
method_start_line: usize,
method_end_line: usize,
) -> Result<SemanticMethodReview, String> {
let tier = match result.get("tier").and_then(|value| value.as_str()) {
Some("slop") => FindingTier::Slop,
Some("kinda_slop") => FindingTier::KindaSlop,
Some("clean") => FindingTier::Clean,
Some(other) => return Err(format!("invalid semantic tier: {other}")),
None => return Err("semantic verdict is missing tier".to_string()),
};
let smelly = result
.get("smelly")
.and_then(|value| value.as_bool())
.ok_or_else(|| "semantic verdict is missing smelly".to_string())?;
if smelly != !matches!(tier, FindingTier::Clean) {
return Err("semantic smelly and tier disagree".to_string());
}
let string_field = |name: &str| {
result
.get(name)
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| format!("semantic verdict is missing non-empty {name}"))
};
let pattern = string_field("pattern")?;
let intent = string_field("intent")?;
let reason = result
.get("reason")
.and_then(|value| value.as_str())
.map(str::trim)
.unwrap_or("")
.to_string();
let necessity_check = string_field("necessity_check")?;
if matches!(tier, FindingTier::Clean) {
if pattern != "none" {
return Err("clean semantic verdict must use pattern `none`".to_string());
}
} else if !SEMANTIC_PATTERNS.contains(&pattern.as_str()) {
return Err(format!("unknown semantic slop pattern: {pattern}"));
}
if !matches!(tier, FindingTier::Clean) && reason.is_empty() {
return Err("non-clean semantic verdict must include a reason".to_string());
}
let entries = result
.get("evidence")
.and_then(|value| value.as_array())
.ok_or_else(|| "semantic verdict is missing evidence array".to_string())?;
if matches!(tier, FindingTier::Clean) {
return Ok(SemanticMethodReview {
tier,
pattern,
intent,
reason,
evidence: Vec::new(),
necessity_check,
});
}
let source_lines = source.lines().collect::<Vec<_>>();
let mut evidence = Vec::with_capacity(entries.len());
for entry in entries {
let object = entry
.as_object()
.ok_or_else(|| "semantic evidence entry is not an object".to_string())?;
let start_line = object
.get("start_line")
.and_then(|value| value.as_u64())
.and_then(|value| usize::try_from(value).ok())
.ok_or_else(|| "semantic evidence has invalid start_line".to_string())?;
let end_line = object
.get("end_line")
.and_then(|value| value.as_u64())
.and_then(|value| usize::try_from(value).ok())
.ok_or_else(|| "semantic evidence has invalid end_line".to_string())?;
let quote = object
.get("quote")
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| "semantic evidence has an empty quote".to_string())?;
let (start_line, end_line) = canonical_evidence_range(
source,
&source_lines,
method_start_line,
method_end_line,
start_line,
end_line,
"e,
)?;
evidence.push(SemanticEvidence {
start_line,
end_line,
quote,
});
}
if !matches!(tier, FindingTier::Clean) && evidence.is_empty() {
return Err("non-clean semantic verdict must include evidence".to_string());
}
Ok(SemanticMethodReview {
tier,
pattern,
intent,
reason,
evidence,
necessity_check,
})
}
fn canonical_evidence_range(
source: &str,
source_lines: &[&str],
method_start_line: usize,
method_end_line: usize,
declared_start_line: usize,
declared_end_line: usize,
quote: &str,
) -> Result<(usize, usize), String> {
if declared_start_line >= method_start_line
&& declared_end_line >= declared_start_line
&& declared_end_line <= method_end_line
{
let relative_start = declared_start_line - method_start_line;
let relative_end = declared_end_line - method_start_line;
if relative_end < source_lines.len() {
let span_source = source_lines[relative_start..=relative_end].join("\n");
if evidence_matches_source(&span_source, quote) {
return Ok((declared_start_line, declared_end_line));
}
}
}
let locations = source
.match_indices(quote)
.filter_map(|(offset, _)| {
let start_line =
method_start_line + source[..offset].bytes().filter(|b| *b == b'\n').count();
let end_line = start_line + quote.bytes().filter(|b| *b == b'\n').count();
(start_line >= method_start_line && end_line <= method_end_line)
.then_some((start_line, end_line))
})
.collect::<Vec<_>>();
if let [location] = locations.as_slice() {
return Ok(*location);
}
if locations.len() > 1 {
return Err(format!(
"semantic evidence quote is ambiguous and does not identify one source span: lines {declared_start_line}-{declared_end_line}"
));
}
let normalized_locations =
normalized_evidence_locations(source, method_start_line, method_end_line, quote);
match normalized_locations.as_slice() {
[location] => Ok(*location),
[] => Err(format!(
"semantic evidence quote does not belong to its declared line range: lines {declared_start_line}-{declared_end_line}"
)),
_ => Err(format!(
"semantic evidence quote is ambiguous and does not identify one source span: lines {declared_start_line}-{declared_end_line}"
)),
}
}
fn normalized_evidence_locations(
source: &str,
method_start_line: usize,
method_end_line: usize,
quote: &str,
) -> Vec<(usize, usize)> {
let source_chars = source
.lines()
.enumerate()
.flat_map(|(line_offset, line)| {
line.chars()
.filter(|character| !character.is_whitespace())
.map(move |character| (character, method_start_line + line_offset))
})
.collect::<Vec<_>>();
let quote_chars = quote
.chars()
.filter(|character| !character.is_whitespace())
.collect::<Vec<_>>();
if quote_chars.is_empty() {
return Vec::new();
}
source_chars
.windows(quote_chars.len())
.filter_map(|window| {
window
.iter()
.map(|(character, _)| *character)
.eq(quote_chars.iter().copied())
.then(|| {
let start_line = window.first().map(|(_, line)| *line)?;
let end_line = window.last().map(|(_, line)| *line)?;
(start_line >= method_start_line && end_line <= method_end_line)
.then_some((start_line, end_line))
})
.flatten()
})
.collect()
}
pub(crate) fn build_semantic_method_verdict(
review: &SemanticMethodReview,
file_path: &str,
method_name: &str,
loc: usize,
start_line: usize,
end_line: usize,
) -> LLMVerdict {
let reason = if matches!(review.tier, FindingTier::Clean) {
review.reason.clone()
} else {
format!(
"{}: {}",
semantic_pattern_label(&review.pattern),
review.reason
)
};
let evidence = review
.evidence
.iter()
.map(|entry| entry.quote.as_str())
.collect::<Vec<_>>()
.join("\n---\n");
LLMVerdict {
verdict_type: "method".to_string(),
file_path: file_path.to_string(),
method_name: Some(method_name.to_string()),
check_type: "method".to_string(),
smelly: !matches!(review.tier, FindingTier::Clean),
tier: review.tier,
cohesive: None,
name_accurate: None,
evidence,
reason,
loc,
start_line,
end_line,
}
}
fn semantic_pattern_label(pattern: &str) -> &str {
match pattern {
"intent_hidden" => "intent is hidden",
"duplicated_decision_paths" => "duplicated decision paths",
"ceremonial_logic" => "ceremonial logic",
"speculative_defense" => "speculative defensive machinery",
"needless_indirection" => "needless indirection",
"difficult_state_transition" => "state transition is difficult to follow",
"semantic_mismatch" => "method meaning does not match its implementation",
"unnecessarily_complicated" => "simple job is unnecessarily complicated",
_ => pattern,
}
}
pub(crate) fn build_file_verdict(result: &serde_json::Value, file_path: &str) -> LLMVerdict {
let (tier, evidence, reason, cohesive, name_accurate) = parse_result_fields(result);
LLMVerdict {
verdict_type: "file".to_string(),
file_path: file_path.to_string(),
method_name: None,
check_type: "file".to_string(),
smelly: !matches!(tier, FindingTier::Clean),
tier,
cohesive,
name_accurate,
evidence,
reason,
loc: 0,
start_line: 0,
end_line: 0,
}
}
pub(crate) fn evidence_matches_source(source: &str, evidence: &str) -> bool {
let trimmed = evidence.trim();
if trimmed.is_empty() {
return false;
}
if source.contains(trimmed) {
return true;
}
fn strip_whitespace(text: &str) -> String {
text.chars().filter(|ch| !ch.is_whitespace()).collect()
}
let normalized_source = strip_whitespace(source);
let normalized_evidence = strip_whitespace(trimmed);
!normalized_evidence.is_empty() && normalized_source.contains(&normalized_evidence)
}
#[cfg(test)]
mod tests {
use super::{evidence_matches_source, parse_semantic_method_review, validate_file_review};
use crate::types::FindingTier;
#[test]
fn evidence_match_accepts_exact_substrings() {
assert!(evidence_matches_source(
"def demo(value):\n return value\n",
"def demo(value):"
));
}
#[test]
fn evidence_match_accepts_whitespace_variants() {
assert!(evidence_matches_source(
"def extract_python_signatures(items):\n return 1\n",
"def extract_python_signatures( items ) :"
));
}
#[test]
fn evidence_match_rejects_missing_text() {
assert!(!evidence_matches_source(
"def demo(value):\n return value\n",
"def other(value):"
));
}
#[test]
fn file_review_rejects_unknown_tier_instead_of_promoting_it_to_slop() {
let result = serde_json::json!({
"smelly": true,
"tier": "maybe",
"evidence": "return value",
"reason": "unclear",
"cohesive": true,
"name_accurate": true
});
let error = validate_file_review(&result).expect_err("unknown tiers must fail closed");
assert!(error.contains("invalid file verdict tier"));
}
#[test]
fn file_review_rejects_smelly_tier_mismatch() {
let result = serde_json::json!({
"smelly": true,
"tier": "clean",
"evidence": "return value",
"reason": "unclear",
"cohesive": true,
"name_accurate": true
});
let error = validate_file_review(&result).expect_err("inconsistent verdicts must fail");
assert!(error.contains("smelly and tier disagree"));
}
#[test]
fn semantic_review_requires_concrete_pattern_and_exact_evidence() {
let source = "def load(value):\n normalized = value.strip()\n return normalized\n";
let result = serde_json::json!({
"smelly": true,
"tier": "slop",
"pattern": "ceremonial_logic",
"intent": "Normalize and return the value.",
"reason": "The temporary normalization layer adds no distinct behavior.",
"necessity_check": "The method can return the same expression directly.",
"evidence": [{
"start_line": 11,
"end_line": 11,
"quote": "normalized = value.strip()"
}]
});
let review = parse_semantic_method_review(&result, source, 10, 12).unwrap();
assert_eq!(review.tier, FindingTier::Slop);
assert_eq!(review.pattern, "ceremonial_logic");
assert_eq!(review.evidence.len(), 1);
}
#[test]
fn semantic_review_canonicalizes_unique_whitespace_variant_evidence() {
let source = "def load(value):\n normalized = value.strip()\n return normalized\n";
let result = serde_json::json!({
"smelly": true,
"tier": "kinda_slop",
"pattern": "ceremonial_logic",
"intent": "Normalize and return the value.",
"reason": "The temporary normalization layer adds no distinct behavior.",
"necessity_check": "The method can return the same expression directly.",
"evidence": [{
"start_line": 10,
"end_line": 10,
"quote": "normalized = value . strip ( )"
}]
});
let review = parse_semantic_method_review(&result, source, 10, 12).unwrap();
assert_eq!(review.evidence[0].start_line, 11);
assert_eq!(review.evidence[0].end_line, 11);
}
#[test]
fn semantic_review_rejects_evidence_that_is_not_in_the_method() {
let result = serde_json::json!({
"smelly": true,
"tier": "slop",
"pattern": "intent_hidden",
"intent": "Return the value.",
"reason": "The implementation hides a direct operation.",
"necessity_check": "No extra machinery is required.",
"evidence": [{
"start_line": 1,
"end_line": 1,
"quote": "not in source"
}]
});
let error = parse_semantic_method_review(&result, "return value", 1, 1).unwrap_err();
assert!(error.contains("does not belong to its declared line range"));
}
#[test]
fn semantic_review_canonicalizes_a_unique_quote_with_wrong_line_numbers() {
let result = serde_json::json!({
"smelly": true,
"tier": "slop",
"pattern": "ceremonial_logic",
"intent": "Return the normalized value.",
"reason": "The temporary is unnecessary.",
"necessity_check": "The expression can be returned directly.",
"evidence": [{
"start_line": 90,
"end_line": 90,
"quote": "normalized = value.strip()"
}]
});
let review = parse_semantic_method_review(
&result,
"def load(value):\n normalized = value.strip()\n return normalized\n",
10,
12,
)
.unwrap();
assert_eq!(review.evidence[0].start_line, 11);
assert_eq!(review.evidence[0].end_line, 11);
}
#[test]
fn clean_semantic_review_discards_non_finding_evidence() {
let result = serde_json::json!({
"smelly": false,
"tier": "clean",
"pattern": "none",
"intent": "Return the value.",
"reason": "The method directly performs its stated job.",
"necessity_check": "There is no unnecessary machinery.",
"evidence": [{
"start_line": 1,
"end_line": 1,
"quote": "return value"
}]
});
let review = parse_semantic_method_review(&result, "return value", 1, 1).unwrap();
assert!(review.evidence.is_empty());
}
}