difflore-core 0.4.0

Core library for the difflore CLI — rule store, retrieval, MCP server, hooks, cloud sync. Not intended for direct use; depend on `difflore-cli` instead.
const GO_RANGE_LOOP_VARIABLE_SIGNAL: &str =
    "go range loop variable capture goroutine closure iteration pointer address";
const KOREAN_OPTIONAL_BODY_TRANSLATION_SIGNAL: &str = "korean translation optional required body fields attribute default none 선택적 필수 필수가 아닙니다 기본값 어트리뷰트";
const ASYNC_CANCELLATION_AWAIT_SIGNAL: &str =
    "async task cancellation await point asyncio anyio cooperative yield custom response cancel";
const SOLID_FORM_PREVENT_DEFAULT_SIGNAL: &str =
    "solid form submit onSubmit handler preventDefault page reload tanstack router action";
const TEST_PLACEMENT_SIGNAL: &str = "test placement unrelated utility module collocate tests with module they exercise cloneRawRequest raw request";

pub fn build_recall_query_with_signals(file: &str, intent: &str) -> String {
    let base = format!("{file} {intent}");
    let signals = recall_query_signal_text(file, intent);
    if signals.is_empty() {
        base
    } else {
        format!("{base}\n{}", signals.join("\n"))
    }
}

fn recall_query_signal_text(file: &str, intent: &str) -> Vec<&'static str> {
    let mut signals = Vec::new();
    if has_go_range_loop_variable_hazard(intent) && looks_like_go(file, intent) {
        signals.push(GO_RANGE_LOOP_VARIABLE_SIGNAL);
    }
    if has_korean_optional_body_translation_signal(file, intent) {
        signals.push(KOREAN_OPTIONAL_BODY_TRANSLATION_SIGNAL);
    }
    if has_async_cancellation_await_signal(file, intent) {
        signals.push(ASYNC_CANCELLATION_AWAIT_SIGNAL);
    }
    if has_solid_form_submit_signal(file, intent) {
        signals.push(SOLID_FORM_PREVENT_DEFAULT_SIGNAL);
    }
    if has_test_placement_signal(file, intent) {
        signals.push(TEST_PLACEMENT_SIGNAL);
    }
    signals
}

fn looks_like_go(file: &str, intent: &str) -> bool {
    file.trim().to_ascii_lowercase().ends_with(".go")
        || intent
            .lines()
            .any(|line| strip_diff_marker(line).trim_start().starts_with("package "))
}

fn strip_diff_marker(line: &str) -> &str {
    let trimmed = line.trim_start();
    match trimmed.as_bytes().first().copied() {
        Some(b'+' | b'-' | b'>') => &trimmed[1..],
        _ => line,
    }
}

fn strip_diff_line_markers(text: &str) -> String {
    text.lines()
        .map(strip_diff_marker)
        .collect::<Vec<_>>()
        .join("\n")
}

fn normalized_path(path: &str) -> String {
    path.trim_start_matches('/').replace('\\', "/")
}

fn normalized_intent(intent: &str) -> String {
    strip_diff_line_markers(intent).to_lowercase()
}

fn contains_any(text: &str, needles: &[&str]) -> bool {
    needles.iter().any(|needle| text.contains(needle))
}

fn path_has_segment(path: &str, segment: &str) -> bool {
    path == segment
        || path.starts_with(&format!("{segment}/"))
        || path.ends_with(&format!("/{segment}"))
        || path.contains(&format!("/{segment}/"))
}

fn is_markdown_path(path: &str) -> bool {
    path.ends_with(".md") || path.ends_with(".mdx")
}

fn is_test_file_path(path: &str) -> bool {
    [
        ".test.ts",
        ".test.tsx",
        ".test.js",
        ".test.jsx",
        ".test.mts",
        ".test.cts",
        ".spec.ts",
        ".spec.tsx",
        ".spec.js",
        ".spec.jsx",
        ".spec.mts",
        ".spec.cts",
    ]
    .iter()
    .any(|suffix| path.ends_with(suffix))
}

fn has_korean_optional_body_translation_signal(file: &str, intent: &str) -> bool {
    let path = normalized_path(file).to_lowercase();
    if !path.contains("docs/ko/docs") {
        return false;
    }

    let text = normalized_intent(intent);
    let has_optional_required_term = contains_any(&text, &["선택", "필수", "optional", "required"]);
    let has_body_field_term = contains_any(
        &text,
        &[
            "body",
            "본문",
            "바디",
            "field",
            "attribute",
            "어트리뷰트",
            "default",
            "none",
            "기본값",
        ],
    );
    has_optional_required_term && has_body_field_term
}

fn has_async_cancellation_await_signal(file: &str, intent: &str) -> bool {
    let path = normalized_path(file).to_lowercase();
    let text = normalized_intent(intent);
    let mentions_cancellation = text.contains("task can only be cancelled")
        || (text.contains("cancel") && contains_any(&text, &["asyncio", "anyio"]));
    mentions_cancellation
        && text.contains("await")
        && (path.contains("custom-response")
            || path.contains("/docs/")
            || text.contains("async generator"))
}

fn has_solid_form_submit_signal(file: &str, intent: &str) -> bool {
    let path = normalized_path(file).to_lowercase();
    let text = normalized_intent(intent);
    let solid_context = path_has_segment(&path, "solid")
        || text.contains("solid")
        || text.contains("@tanstack/solid-router");
    let submit_context = contains_any(&text, &["<form", "onsubmit", "submit", "formdata"]);
    solid_context && submit_context && is_markdown_path(&path)
}

fn has_test_placement_signal(file: &str, intent: &str) -> bool {
    let path = normalized_path(file).to_lowercase();
    if !is_test_file_path(&path) {
        return false;
    }

    let text = normalized_intent(intent);
    let has_test_shape = contains_any(&text, &["describe(", "it(", "test("]);
    let has_utility_subject = contains_any(
        &text,
        &[
            "clonerawrequest",
            "raw request",
            "rawrequest",
            "util",
            "utility",
        ],
    );
    has_test_shape && has_utility_subject
}

fn has_go_range_loop_variable_hazard(intent: &str) -> bool {
    let text = strip_diff_line_markers(intent);
    let mut offset = 0usize;
    for line in text.lines() {
        if let Some(variable) = range_loop_variable(line) {
            let end = (offset + 1200).min(text.len());
            let window = &text[offset..end];
            if returns_loop_variable_address(window, &variable)
                || closure_captures_loop_variable(window, &variable)
            {
                return true;
            }
        }
        offset += line.len() + 1;
    }
    false
}

fn range_loop_variable(line: &str) -> Option<String> {
    let lower = line.to_ascii_lowercase();
    let for_index = lower.find("for ")?;
    let after_for = &line[for_index + 4..];
    let lower_after_for = &lower[for_index + 4..];
    let range_index = lower_after_for.find(" range")?;
    let before_range = after_for[..range_index].trim();
    let before_assign = before_range.split(":=").next()?.trim();
    let variable = before_assign.split(',').next_back()?.trim();
    if is_go_ident(variable) && variable != "_" {
        Some(variable.to_owned())
    } else {
        None
    }
}

fn is_go_ident(value: &str) -> bool {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    if !(first == '_' || first.is_ascii_alphabetic()) {
        return false;
    }
    chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}

fn returns_loop_variable_address(window: &str, variable: &str) -> bool {
    window.lines().any(|line| {
        let Some(rest) = line.trim_start().strip_prefix("return") else {
            return false;
        };
        let Some(rest) = rest.trim_start().strip_prefix('&') else {
            return false;
        };
        let rest = rest.trim_start();
        rest == variable
            || rest
                .strip_prefix(variable)
                .is_some_and(|suffix| suffix.starts_with(char::is_whitespace) || suffix == ",")
    })
}

fn closure_captures_loop_variable(window: &str, variable: &str) -> bool {
    let lower = window.to_ascii_lowercase();
    let Some(func_index) = lower.find("func") else {
        return false;
    };
    let end = (func_index + 700).min(window.len());
    contains_word(&window[func_index..end], variable)
}

fn contains_word(text: &str, needle: &str) -> bool {
    let mut start = 0usize;
    while let Some(relative) = text[start..].find(needle) {
        let index = start + relative;
        let before = text[..index].chars().next_back();
        let after = text[index + needle.len()..].chars().next();
        let before_ok = before.is_none_or(|c| !(c == '_' || c.is_ascii_alphanumeric()));
        let after_ok = after.is_none_or(|c| !(c == '_' || c.is_ascii_alphanumeric()));
        if before_ok && after_ok {
            return true;
        }
        start = index + needle.len();
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn recall_query_signal_adds_go_loop_variable_terms_for_pointer_return() {
        let intent = r"@@ -0,0 +1,12 @@
+package extensions
+
+func FindOfficialExtension(commandName string) *OfficialExtension {
+  for _, ext := range officialExtensions {
+    if ext.Name == commandName {
+      return &ext
+    }
+  }
+  return nil
+}";

        let query = build_recall_query_with_signals("pkg/extensions/official.go", intent);

        assert!(query.contains("loop variable capture"));
        assert!(query.contains("goroutine closure"));
    }

    #[test]
    fn recall_query_signal_adds_go_loop_variable_terms_for_closure_capture() {
        let intent = r"
package main

func run(items []Item) {
  for _, item := range items {
    go func() {
      handle(item)
    }()
  }
}
";

        let query = build_recall_query_with_signals("worker.go", intent);

        assert!(query.contains("loop variable capture"));
    }

    #[test]
    fn recall_query_signal_does_not_expand_ordinary_range_loop() {
        let intent = r"
package main

func names(items []Item) []string {
  names := make([]string, 0, len(items))
  for _, item := range items {
    names = append(names, item.Name)
  }
  return names
}
";

        assert_eq!(
            build_recall_query_with_signals("items.go", intent),
            format!("{} {}", "items.go", intent)
        );
    }

    #[test]
    fn recall_query_signal_does_not_expand_non_go_file() {
        let intent = r"
for _, item := range items {
  return &item
}
";

        assert_eq!(
            build_recall_query_with_signals("items.ts", intent),
            format!("{} {}", "items.ts", intent)
        );
    }

    #[test]
    fn recall_query_signal_adds_korean_optional_body_translation_terms() {
        let intent = r"
+ 필드에 기본값이 있는 경우에는 필수가 아닙니다.
+ None을 사용하면 선택적으로 보낼 수 있습니다.
";

        let query = build_recall_query_with_signals("docs/ko/docs/tutorial/body.md", intent);

        assert!(query.contains("korean translation optional"));
        assert!(query.contains("선택적"));
    }

    #[test]
    fn recall_query_signal_adds_async_cancellation_await_terms() {
        let intent = r"
+ In asyncio, a task can only be cancelled when it reaches an await.
+ Streaming responses should document the await point precisely.
";

        let query =
            build_recall_query_with_signals("docs/en/docs/advanced/custom-response.md", intent);

        assert!(query.contains("async task cancellation await"));
        assert!(query.contains("anyio"));
    }

    #[test]
    fn recall_query_signal_adds_solid_form_prevent_default_terms() {
        let intent = r"
+ <form onSubmit={async (event) => {
+   const formData = new FormData(event.currentTarget)
+ }}>
";

        let query = build_recall_query_with_signals(
            "docs/start/framework/solid/tutorial/reading-writing-file.md",
            intent,
        );

        assert!(query.contains("preventDefault"));
        assert!(query.contains("solid form submit"));
    }

    #[test]
    fn recall_query_signal_adds_test_placement_terms_for_utility_tests() {
        let intent = r"
+ describe('cloneRawRequest', () => {
+   it('keeps the raw request body', () => {})
+ })
";

        let query = build_recall_query_with_signals("src/validator/validator.test.ts", intent);

        assert!(query.contains("test placement unrelated utility"));
        assert!(query.contains("cloneRawRequest"));
    }
}