difflore-cli 0.7.0

Your AI coding agent learned public code, not your team's private decisions. difflore turns past PR reviews into source-backed local rules.
//! `.opencodereview/rule.json` emitter: feeds mined team rules to Alibaba's
//! Open Code Review CLI. Whole-file JSON (no marker block); ownership is
//! tracked via a top-level `_difflore` key.

use difflore_core::export::ExportRule;

pub(crate) static OPEN_CODE_REVIEW: super::Emitter = super::Emitter {
    format: "open-code-review",
    file_name: ".opencodereview/rule.json",
    engine: None,
    kind: super::EmitterKind::OwnedJson,
};

pub(crate) fn render_ocr_rules(rules: &[ExportRule]) -> String {
    let mut entries = Vec::new();

    for rule in rules {
        let fallback_globs;
        let globs = if rule.file_patterns.is_empty() {
            fallback_globs = vec!["**/*".to_owned()];
            &fallback_globs
        } else {
            &rule.file_patterns
        };
        let text = format!("{}: {}", rule.name, rule.description);

        for glob in globs {
            entries.push(serde_json::json!({
                "path": glob,
                "rule": text,
                "merge_system_rule": true,
            }));
        }
    }

    let value = serde_json::json!({
        "_difflore": {
            "generator": "difflore",
            "version": env!("CARGO_PKG_VERSION"),
            "note": "generated by `difflore export --format open-code-review`; manual edits will be overwritten",
        },
        "rules": entries,
    });
    serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_owned())
}

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

    fn rule(patterns: &[&str], name: &str, description: &str) -> ExportRule {
        ExportRule {
            id: "rule-id".into(),
            name: name.into(),
            description: description.into(),
            r#type: "pattern".into(),
            confidence: 0.9,
            origin: "test".into(),
            source_kind: "review".into(),
            source: "local".into(),
            repo_scope: None,
            check_prompt: None,
            file_patterns: patterns.iter().map(ToString::to_string).collect(),
            examples: Vec::new(),
        }
    }

    #[test]
    fn renders_ocr_rule_json_with_ownership_marker() {
        let json = render_ocr_rules(&[rule(
            &["src/**/*.ts", "lib/**/*.ts"],
            "No double retries",
            "This package handles retries; don't add a second loop.",
        )]);
        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(v["_difflore"]["generator"], "difflore");
        let rules = v["rules"].as_array().unwrap();
        assert_eq!(rules.len(), 2);
        assert_eq!(rules[0]["path"], "src/**/*.ts");
        assert_eq!(rules[0]["merge_system_rule"], true);
        assert!(
            rules[0]["rule"]
                .as_str()
                .unwrap()
                .contains("No double retries")
        );
    }

    #[test]
    fn rule_without_patterns_matches_everything() {
        let json = render_ocr_rules(&[rule(&[], "t", "c")]);
        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(v["rules"][0]["path"], "**/*");
    }
}