use std::collections::HashMap;
use crate::state::AphroditeState;
const RECURSIVE_DEPTH: usize = 5;
const CCR_PREFIX: &str = "<<<CCR:";
const CCR_SUFFIX: &str = ">>>";
fn parse_marker_hash(marker: &str) -> Option<String> {
let inner = marker.strip_prefix(CCR_PREFIX)?.strip_suffix(CCR_SUFFIX)?;
inner.split('|').next().map(|h| h.to_string())
}
fn find_markers(content: &str) -> Vec<(String, String)> {
let mut markers = Vec::new();
let mut search_from = 0;
while let Some(start) = content[search_from..].find(CCR_PREFIX) {
let abs_start = search_from + start;
let after_prefix = abs_start + CCR_PREFIX.len();
if let Some(end) = content[after_prefix..].find(CCR_SUFFIX) {
let abs_end = after_prefix + end + CCR_SUFFIX.len();
let full_marker = content[abs_start..abs_end].to_string();
if let Some(hash) = parse_marker_hash(&full_marker) {
markers.push((full_marker, hash));
}
search_from = abs_end;
} else {
search_from = after_prefix;
}
}
markers
}
pub fn resolve_one(state: &mut AphroditeState, hash_val: &str) -> Option<String> {
let hash_val = crate::marker::normalize_hash(hash_val);
if hash_val.starts_with("i:") {
return state.inline_store_get(hash_val);
}
state.inline_store_get(hash_val)
}
pub fn filter_lines(content: &str, query: &str) -> String {
if query.is_empty() {
return content.to_string();
}
let query_lower = query.to_lowercase();
let matching: Vec<&str> = content
.lines()
.filter(|line| line.to_lowercase().contains(&query_lower))
.collect();
if matching.is_empty() {
format!("[aphrodite: no lines matched {query:?} - returning full content]\n{content}")
} else {
matching.join("\n")
}
}
pub fn resolve_recursive(
state: &mut AphroditeState,
hash_val: &str,
depth: usize,
resolved: &mut HashMap<String, String>,
visited: &mut Vec<String>,
) -> Option<String> {
if visited.contains(&hash_val.to_string()) {
return resolved.get(hash_val).cloned();
}
visited.push(hash_val.to_string());
if depth >= RECURSIVE_DEPTH {
return resolve_one(state, hash_val);
}
if let Some(cached) = resolved.get(hash_val) {
return Some(cached.clone());
}
let content = resolve_one(state, hash_val)?;
resolved.insert(hash_val.to_string(), content.clone());
let nested_markers = find_markers(&content);
if nested_markers.is_empty() {
return Some(content);
}
let mut replacements: Vec<(String, Option<String>)> = Vec::new();
for (marker, nested_hash) in &nested_markers {
let nested_content = if let Some(cached) = resolved.get(nested_hash) {
Some(cached.clone())
} else {
resolve_recursive(state, nested_hash, depth + 1, resolved, visited)
};
replacements.push((marker.clone(), nested_content));
}
let mut result = content;
for (marker, replacement) in &replacements {
if let Some(repl) = replacement {
result = result.replace(marker.as_str(), repl.as_str());
}
}
Some(result)
}
pub fn expand(state: &mut AphroditeState, hash_val: &str) -> Option<String> {
let mut resolved = HashMap::new();
let mut visited = Vec::new();
resolve_recursive(state, hash_val, 0, &mut resolved, &mut visited)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_marker_hash() {
let hash = parse_marker_hash("<<<CCR:abc123def456|text|100>>>");
assert_eq!(hash, Some("abc123def456".into()));
let hash = parse_marker_hash("<<<CCR:abc|code_rust|5000>>>");
assert_eq!(hash, Some("abc".into()));
}
#[test]
fn test_parse_marker_invalid() {
assert_eq!(parse_marker_hash("not a marker"), None);
assert_eq!(parse_marker_hash("<<<CCR:"), None);
}
#[test]
fn test_find_markers_single() {
let content = "hello <<<CCR:abc|text|100>>> world";
let markers = find_markers(content);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].0, "<<<CCR:abc|text|100>>>");
assert_eq!(markers[0].1, "abc");
}
#[test]
fn test_find_markers_multiple() {
let content = "<<<CCR:a|t|1>>> middle <<<CCR:b|t|2>>>";
let markers = find_markers(content);
assert_eq!(markers.len(), 2);
}
#[test]
fn test_find_markers_none() {
assert!(find_markers("plain text").is_empty());
}
#[test]
fn test_filter_empty_query() {
assert_eq!(filter_lines("hello\nworld", ""), "hello\nworld");
}
#[test]
fn test_filter_matching() {
let content = "line one\nerror: broke\nline two\nERROR: fatal\n";
let result = filter_lines(content, "error");
assert!(result.contains("error: broke"));
assert!(result.contains("ERROR: fatal"));
assert!(!result.contains("line one"));
}
#[test]
fn test_filter_no_match() {
let content = "hello\nworld\n";
let result = filter_lines(content, "nonexistent");
assert!(result.contains("[aphrodite: no lines matched"));
}
#[test]
fn test_resolve_one_inline() {
let mut s = AphroditeState::default();
s.inline_store_put("abc123".into(), "hello world".into());
assert_eq!(resolve_one(&mut s, "abc123"), Some("hello world".into()));
}
#[test]
fn test_resolve_one_missing() {
let mut s = AphroditeState::default();
assert_eq!(resolve_one(&mut s, "nonexistent"), None);
}
#[test]
fn test_resolve_one_never_shadowed_by_stage2_key() {
let mut s = AphroditeState::default();
s.inline_store_put("h".into(), "ORIGINAL".into());
s.inline_store_put("h#stage2".into(), "REDUCED".into());
assert_eq!(resolve_one(&mut s, "h"), Some("ORIGINAL".to_string()));
}
#[test]
fn regression_resolve_one_tolerates_pipe_suffixed_hash() {
let mut s = AphroditeState::default();
s.inline_store_put("abc123".into(), "<the real content>".into());
assert_eq!(resolve_one(&mut s, "abc123"), Some("<the real content>".into()));
assert_eq!(
resolve_one(&mut s, "abc123|tool|1024"),
Some("<the real content>".into()),
"a pipe-suffixed hash (as an LLM might echo a full marker body) must still resolve"
);
assert_eq!(
resolve_one(&mut s, " abc123 "),
Some("<the real content>".into()),
"whitespace around the hash must be tolerated too"
);
}
#[test]
fn test_resolve_one_i_prefix() {
let mut s = AphroditeState::default();
s.inline_store_put("i:abc123def".into(), "inline content".into());
assert_eq!(resolve_one(&mut s, "i:abc123def"), Some("inline content".into()));
}
#[test]
fn test_expand_simple() {
let mut s = AphroditeState::default();
s.inline_store_put("simple".into(), "just content".into());
assert_eq!(expand(&mut s, "simple"), Some("just content".into()));
}
#[test]
fn test_expand_with_nesting() {
let mut s = AphroditeState::default();
let inner = "<<<CCR:inner123|text|10>>>";
s.inline_store_put("outer".into(), format!("before {} after", inner));
s.inline_store_put("inner123".into(), "RESOLVED".into());
let result = expand(&mut s, "outer");
assert_eq!(result, Some("before RESOLVED after".into()));
}
#[test]
fn test_expand_unresolved() {
let mut s = AphroditeState::default();
s.inline_store_put("outer".into(), "before <<<CCR:missing|text|10>>> after".into());
let result = expand(&mut s, "outer");
assert_eq!(result, Some("before <<<CCR:missing|text|10>>> after".to_string()));
}
#[test]
fn test_expand_missing_hash() {
let mut s = AphroditeState::default();
assert_eq!(expand(&mut s, "nope"), None);
}
#[test]
fn test_expand_deep_nesting_respects_limit() {
let mut s = AphroditeState::default();
s.inline_store_put("h0".into(), "<<<CCR:h1|t|1>>>".into());
s.inline_store_put("h1".into(), "<<<CCR:h2|t|1>>>".into());
s.inline_store_put("h2".into(), "<<<CCR:h3|t|1>>>".into());
s.inline_store_put("h3".into(), "<<<CCR:h4|t|1>>>".into());
s.inline_store_put("h4".into(), "<<<CCR:h5|t|1>>>".into());
s.inline_store_put("h5".into(), "DEEP".into());
let result = expand(&mut s, "h0");
assert_eq!(result, Some("DEEP".to_string()));
}
#[test]
fn test_expand_cycle_safe() {
let mut s = AphroditeState::default();
s.inline_store_put("hA".into(), "<<<CCR:hB|t|1>>>".into());
s.inline_store_put("hB".into(), "<<<CCR:hA|t|1>>>".into());
let result = expand(&mut s, "hA").unwrap();
assert!(
!result.contains("[CCR_UNRESOLVED"),
"cycle must not surface as unresolved: {result}"
);
assert_eq!(result, "<<<CCR:hB|t|1>>>");
}
#[test]
fn test_find_markers_nested_unclosed_is_skipped() {
let content = "<<<CCR:a<<<CCR:b|t|1>>>";
let markers = find_markers(content);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].1, "a<<<CCR:b");
}
#[test]
fn test_find_markers_unterminated_no_panic() {
assert!(find_markers("<<<CCR:").is_empty());
assert!(find_markers("text <<<CCR:abc|t|1").is_empty());
}
#[test]
fn test_parse_marker_hash_empty_hash() {
assert_eq!(parse_marker_hash("<<<CCR:>>>"), Some(String::new()));
}
use proptest::{prop_assert_eq, proptest};
proptest! {
#[test]
fn prop_find_markers_never_panics_and_roundtrips(s in ".*") {
let markers = find_markers(&s);
for (marker, hash) in markers {
let parsed = parse_marker_hash(&marker);
prop_assert_eq!(parsed, Some(hash));
}
}
}
#[test]
fn test_expand_never_writes_back_over_the_original_key() {
let mut s = AphroditeState::default();
let original = "before <<<CCR:inner|t|1>>> after";
s.inline_store_put("outer".into(), original.to_string());
s.inline_store_put("inner".into(), "X".to_string());
let _ = expand(&mut s, "outer");
assert_eq!(resolve_one(&mut s, "outer"), Some(original.to_string()));
}
#[test]
fn test_expand_is_idempotent() {
let mut s = AphroditeState::default();
s.inline_store_put("outer".into(), "<<<CCR:inner|t|1>>>".into());
s.inline_store_put("inner".into(), "X".into());
let first = expand(&mut s, "outer");
let second = expand(&mut s, "outer");
assert_eq!(first, second);
assert_eq!(first, Some("X".to_string()));
}
#[test]
fn test_expand_preserves_literal_marker_shaped_text_in_content() {
let mut s = AphroditeState::default();
let content = "see the format <<<CCR:deadbeef|text|4>>> for reference";
s.inline_store_put("doc".into(), content.to_string());
let result = expand(&mut s, "doc");
assert_eq!(result, Some(content.to_string()));
}
#[test]
fn test_expand_diamond_nesting_fully_expands() {
let mut s = AphroditeState::default();
s.inline_store_put("outer".into(), "<<<CCR:a|t|1>>> and <<<CCR:b|t|1>>>".into());
s.inline_store_put("a".into(), "A".into());
s.inline_store_put("b".into(), "wraps <<<CCR:a|t|1>>>".into());
let result = expand(&mut s, "outer").unwrap();
assert!(!result.contains("<<<CCR:"), "no raw marker should remain: {result}");
assert_eq!(result, "A and wraps A");
}
}