sem-core 0.7.0

Entity-level semantic diff engine. Extracts functions, classes, and methods from 20 languages via tree-sitter and diffs at the entity level.
Documentation
use crate::parser::differ::DiffResult;
use serde_json::{json, Value};

pub fn diff_json_value(result: &DiffResult) -> Value {
    let changes: Vec<Value> = result
        .changes
        .iter()
        .map(|c| {
            json!({
                "entityId": c.entity_id,
                "changeType": c.change_type,
                "entityType": c.entity_type,
                "entityName": c.entity_name,
                "startLine": c.start_line,
                "endLine": c.end_line,
                "oldStartLine": c.old_start_line,
                "oldEndLine": c.old_end_line,
                "oldEntityName": c.old_entity_name,
                "filePath": c.file_path,
                "oldFilePath": c.old_file_path,
                "oldParentId": c.old_parent_id,
                "beforeContent": c.before_content,
                "afterContent": c.after_content,
                "commitSha": c.commit_sha,
                "author": c.author,
                "structuralChange": c.structural_change,
            })
        })
        .collect();

    json!({
        "summary": {
            "fileCount": result.file_count,
            "added": result.added_count,
            "modified": result.modified_count,
            "deleted": result.deleted_count,
            "moved": result.moved_count,
            "renamed": result.renamed_count,
            "reordered": result.reordered_count,
            "orphan": result.orphan_count,
            "total": result.changes.len(),
        },
        "changes": changes,
    })
}

pub fn format_diff_json(result: &DiffResult) -> String {
    serde_json::to_string(&diff_json_value(result)).unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::change::{ChangeType, SemanticChange};

    #[test]
    fn diff_json_value_matches_cli_envelope() {
        let result = DiffResult {
            changes: vec![SemanticChange {
                id: "internal-change-id".to_string(),
                entity_id: "src/lib.rs::function::foo".to_string(),
                change_type: ChangeType::Modified,
                entity_type: "function".to_string(),
                entity_name: "foo".to_string(),
                entity_line: 12,
                start_line: 12,
                end_line: 12,
                old_start_line: None,
                old_end_line: None,
                parent_name: Some("module".to_string()),
                file_path: "src/lib.rs".to_string(),
                old_entity_name: Some("bar".to_string()),
                old_file_path: Some("src/old.rs".to_string()),
                old_parent_id: Some("old-parent".to_string()),
                before_content: Some("fn bar() {}".to_string()),
                after_content: Some("fn foo() {}".to_string()),
                commit_sha: Some("abc123".to_string()),
                author: Some("Ada".to_string()),
                timestamp: Some("2026-05-26".to_string()),
                structural_change: Some(true),
            }],
            file_count: 1,
            added_count: 0,
            modified_count: 1,
            deleted_count: 0,
            moved_count: 0,
            renamed_count: 0,
            reordered_count: 0,
            orphan_count: 0,
            total_entities_before: 1,
            total_entities_after: 1,
        };

        let value = diff_json_value(&result);

        assert_eq!(
            value,
            json!({
                "summary": {
                    "fileCount": 1,
                    "added": 0,
                    "modified": 1,
                    "deleted": 0,
                    "moved": 0,
                    "renamed": 0,
                    "reordered": 0,
                    "orphan": 0,
                    "total": 1,
                },
                "changes": [{
                    "entityId": "src/lib.rs::function::foo",
                    "changeType": "modified",
                    "entityType": "function",
                    "entityName": "foo",
                    "startLine": 12,
                    "endLine": 12,
                    "oldStartLine": null,
                    "oldEndLine": null,
                    "oldEntityName": "bar",
                    "filePath": "src/lib.rs",
                    "oldFilePath": "src/old.rs",
                    "oldParentId": "old-parent",
                    "beforeContent": "fn bar() {}",
                    "afterContent": "fn foo() {}",
                    "commitSha": "abc123",
                    "author": "Ada",
                    "structuralChange": true,
                }],
            })
        );
    }
}