Skip to documentation
SLOP

tiny.smg.diff

Reference tiny.smg diff

Defined in tiny.smg.

API (9)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.smgdiff
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsprivate; no linktools.smg.src.command.analyze.commanddeltaFiltercommand.diffruntest; no linktools.smg.src.difftest: diff detects added removed chan...test; no linktools.smg.src.difftest: diff detects hash and fuzzy ren...test; no linktools.smg.src.difftest: diff index matches linear edge ...+4 moreprivate; no linktools.smg.src.diffdetectRenamesprivate; no linktools.smg.src.diffhasIndexedEdgeprivate; no linktools.smg.src.diffnodeChangesprivate; no linktools.smg.src.diffsortChangedNodesviewsortEdgesviewsortNodesdiffgraphs
Static calls · unresolved targets: 4 · external targets: 7.
Called byCallsprivate; no linktools.smg.src.command.analyze.commanddeltaFiltercommand.diffruntest; no linktools.smg.src.difftest: loadGraphFromGit scans committe...gitrefExistsgitrunscanscanPathsWithOptionsdiffloadGraphFromGit
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallscommand.diffrunprivate; no linktools.smg.src.diffwriteEdgesprivate; no linktools.smg.src.diffwriteNodeNamesdifftoJson
Static calls · unresolved targets: 1 · external targets: 9.
Called byCallsNo direct callscommand.diffruntest; no linktools.smg.src.difftest: diff preserves edge identity an...difftoText
Static calls · unresolved targets: 3 · external targets: 4.

Source: tools/smg/src/diff.zig

zig
const std = @import("std");const pretty = @import("pretty");const sys = @import("sys");const smg = @import("root.zig");const git = @import("git.zig");const graph_mod = @import("graph.zig");const scan = @import("scan/root.zig");const view = @import("view.zig");const model = @import("model.zig");const text = @import("text/root.zig");const pretty_json = pretty.json;pub const NodeChange = struct {    name: []const u8,    field: []const u8,    old: ?[]const u8,    new: ?[]const u8,};pub const ChangedNode = struct {    node: model.Node,    changes: []const NodeChange,};pub const RenamedNode = struct {    old_name: []const u8,    new_name: []const u8,    old_node: model.Node,    new_node: model.Node,    match_type: []const u8,};pub const Result = struct {    added_nodes: []const model.Node = &.{},    removed_nodes: []const model.Node = &.{},    changed_nodes: []const ChangedNode = &.{},    renamed_nodes: []const RenamedNode = &.{},    added_edges: []const model.Edge = &.{},    removed_edges: []const model.Edge = &.{},    pub fn empty(self: Result) bool {        return self.added_nodes.len == 0 and self.removed_nodes.len == 0 and self.changed_nodes.len == 0 and self.renamed_nodes.len == 0 and self.added_edges.len == 0 and self.removed_edges.len == 0;    }};pub fn graphs(allocator: std.mem.Allocator, old: graph_mod.Graph, new: graph_mod.Graph, detect_renames: bool) !Result {    var result = Result{};    var added_nodes: std.ArrayList(model.Node) = .empty;    var removed_nodes: std.ArrayList(model.Node) = .empty;    var changed_nodes: std.ArrayList(ChangedNode) = .empty;    var added_edges: std.ArrayList(model.Edge) = .empty;    var removed_edges: std.ArrayList(model.Edge) = .empty;    for (new.nodes.items) |node| {        if (graph_mod.getNode(&old, node.name) == null) try added_nodes.append(allocator, node);    }    for (old.nodes.items) |node| {        if (graph_mod.getNode(&new, node.name) == null) try removed_nodes.append(allocator, node);    }    for (new.nodes.items) |node| {        const old_node = graph_mod.getNode(&old, node.name) orelse continue;        const changes = try nodeChanges(allocator, old_node, node);        if (changes.len != 0) try changed_nodes.append(allocator, .{ .node = node, .changes = changes });    }    var new_edge_iterator = new.edge_index.iterator();    while (new_edge_iterator.next()) |entry| {        const edge = new.edges.items[entry.value_ptr.*];        if (!hasIndexedEdge(old, entry.key_ptr.*, edge)) try added_edges.append(allocator, edge);    }    var old_edge_iterator = old.edge_index.iterator();    while (old_edge_iterator.next()) |entry| {        const edge = old.edges.items[entry.value_ptr.*];        if (!hasIndexedEdge(new, entry.key_ptr.*, edge)) try removed_edges.append(allocator, edge);    }    view.sortNodes(added_nodes.items);    view.sortNodes(removed_nodes.items);    sortChangedNodes(changed_nodes.items);    view.sortEdges(added_edges.items);    view.sortEdges(removed_edges.items);    result.added_nodes = try added_nodes.toOwnedSlice(allocator);    result.removed_nodes = try removed_nodes.toOwnedSlice(allocator);    result.changed_nodes = try changed_nodes.toOwnedSlice(allocator);    result.added_edges = try added_edges.toOwnedSlice(allocator);    result.removed_edges = try removed_edges.toOwnedSlice(allocator);    result.renamed_nodes = &.{};    if (detect_renames and result.added_nodes.len != 0 and result.removed_nodes.len != 0) result = try detectRenames(allocator, result);    return result;}fn sortChangedNodes(nodes: []ChangedNode) void {    std.mem.sort(ChangedNode, nodes, {}, changedNodeLessThan);}fn changedNodeLessThan(_: void, left: ChangedNode, right: ChangedNode) bool {    return std.mem.lessThan(u8, left.node.name, right.node.name);}fn hasIndexedEdge(graph: graph_mod.Graph, key: []const u8, edge: model.Edge) bool {    const index = graph.edge_index.get(key) orelse return false;    const candidate = graph.edges.items[index];    return std.mem.eql(u8, candidate.source, edge.source) and        std.mem.eql(u8, candidate.rel, edge.rel) and        std.mem.eql(u8, candidate.target, edge.target);}fn nodeChanges(allocator: std.mem.Allocator, old: model.Node, new: model.Node) ![]const NodeChange {    var changes: std.ArrayList(NodeChange) = .empty;    if (!std.mem.eql(u8, old.type, new.type)) try changes.append(allocator, .{ .name = new.name, .field = "type", .old = old.type, .new = new.type });    if (!optionalEqual(old.file, new.file)) try changes.append(allocator, .{ .name = new.name, .field = "file", .old = old.file, .new = new.file });    if (old.line != new.line) try changes.append(allocator, .{ .name = new.name, .field = "line", .old = try lineText(allocator, old.line), .new = try lineText(allocator, new.line) });    if (!optionalEqual(old.docstring, new.docstring)) try changes.append(allocator, .{ .name = new.name, .field = "docstring", .old = old.docstring, .new = new.docstring });    const old_content = model.pairValue(old.metadata, "content_hash");    const new_content = model.pairValue(new.metadata, "content_hash");    if (!optionalEqual(old_content, new_content)) try changes.append(allocator, .{ .name = new.name, .field = "content_hash", .old = old_content, .new = new_content });    const old_structure = model.pairValue(old.metadata, "structure_hash");    const new_structure = model.pairValue(new.metadata, "structure_hash");    if (!optionalEqual(old_structure, new_structure)) try changes.append(allocator, .{ .name = new.name, .field = "structure_hash", .old = old_structure, .new = new_structure });    return try changes.toOwnedSlice(allocator);}fn optionalEqual(a: ?[]const u8, b: ?[]const u8) bool {    if (a == null and b == null) return true;    if (a == null or b == null) return false;    return std.mem.eql(u8, a.?, b.?);}fn lineText(allocator: std.mem.Allocator, value: ?i64) ![]const u8 {    if (value) |line| return try std.fmt.allocPrint(allocator, "{d}", .{line});    return "None";}fn detectRenames(allocator: std.mem.Allocator, result: Result) !Result {    var matched_added: std.ArrayList([]const u8) = .empty;    var matched_removed: std.ArrayList([]const u8) = .empty;    var renamed: std.ArrayList(RenamedNode) = .empty;    for (result.added_nodes) |added| {        const structure_hash = model.pairValue(added.metadata, "structure_hash") orelse continue;        var candidates: std.ArrayList(model.Node) = .empty;        for (result.removed_nodes) |removed| {            if (containsString(matched_removed.items, removed.name)) continue;            const removed_structure = model.pairValue(removed.metadata, "structure_hash") orelse continue;            if (std.mem.eql(u8, removed_structure, structure_hash)) try candidates.append(allocator, removed);        }        if (candidates.items.len == 0) continue;        const content_hash = model.pairValue(added.metadata, "content_hash");        var exact: std.ArrayList(model.Node) = .empty;        if (content_hash) |hash| {            for (candidates.items) |candidate| {                const candidate_hash = model.pairValue(candidate.metadata, "content_hash") orelse continue;                if (std.mem.eql(u8, candidate_hash, hash)) try exact.append(allocator, candidate);            }        }        var match: ?model.Node = null;        var match_type: []const u8 = "structure";        if (exact.items.len == 1) {            match = exact.items[0];            match_type = "content";        } else if (candidates.items.len == 1) {            match = candidates.items[0];        }        if (match) |node| {            try renamed.append(allocator, .{ .old_name = node.name, .new_name = added.name, .old_node = node, .new_node = added, .match_type = match_type });            try matched_added.append(allocator, added.name);            try matched_removed.append(allocator, node.name);        }    }    try fuzzyMatch(allocator, result.added_nodes, result.removed_nodes, &renamed, &matched_added, &matched_removed);    var added_nodes: std.ArrayList(model.Node) = .empty;    var removed_nodes: std.ArrayList(model.Node) = .empty;    for (result.added_nodes) |node| {        if (!containsString(matched_added.items, node.name)) try added_nodes.append(allocator, node);    }    for (result.removed_nodes) |node| {        if (!containsString(matched_removed.items, node.name)) try removed_nodes.append(allocator, node);    }    return .{        .added_nodes = try added_nodes.toOwnedSlice(allocator),        .removed_nodes = try removed_nodes.toOwnedSlice(allocator),        .changed_nodes = result.changed_nodes,        .renamed_nodes = try renamed.toOwnedSlice(allocator),        .added_edges = result.added_edges,        .removed_edges = result.removed_edges,    };}fn fuzzyMatch(allocator: std.mem.Allocator, added_nodes: []const model.Node, removed_nodes: []const model.Node, renamed: *std.ArrayList(RenamedNode), matched_added: *std.ArrayList([]const u8), matched_removed: *std.ArrayList([]const u8)) !void {    for (added_nodes) |added| {        if (containsString(matched_added.items, added.name)) continue;        const added_tokens = try tokenize(allocator, added.name);        if (added_tokens.len == 0) continue;        var best_score: f64 = 0.0;        var best_match: ?model.Node = null;        for (removed_nodes) |removed| {            if (containsString(matched_removed.items, removed.name)) continue;            if (!std.mem.eql(u8, removed.type, added.type)) continue;            const removed_tokens = try tokenize(allocator, removed.name);            if (removed_tokens.len == 0) continue;            const smaller = @min(added_tokens.len, removed_tokens.len);            const larger = @max(added_tokens.len, removed_tokens.len);            if (@as(f64, @floatFromInt(smaller)) / @as(f64, @floatFromInt(larger)) < 0.5) continue;            const score = jaccard(added_tokens, removed_tokens);            if (score > best_score) {                best_score = score;                best_match = removed;            }        }        if (best_match) |node| {            if (best_score >= 0.8) {                try renamed.append(allocator, .{ .old_name = node.name, .new_name = added.name, .old_node = node, .new_node = added, .match_type = "fuzzy" });                try matched_added.append(allocator, added.name);                try matched_removed.append(allocator, node.name);            }        }    }}fn tokenize(allocator: std.mem.Allocator, name: []const u8) ![]const []const u8 {    var out: std.ArrayList([]const u8) = .empty;    var start: usize = 0;    var index: usize = 0;    while (index <= name.len) : (index += 1) {        if (index == name.len or name[index] == '.' or name[index] == '_') {            if (index > start) try appendUniqueString(allocator, &out, name[start..index]);            start = index + 1;        }    }    return try out.toOwnedSlice(allocator);}fn jaccard(a: []const []const u8, b: []const []const u8) f64 {    var intersection: usize = 0;    for (a) |item| {        if (containsString(b, item)) intersection += 1;    }    var union_count: usize = a.len;    for (b) |item| {        if (!containsString(a, item)) union_count += 1;    }    if (union_count == 0) return 0.0;    return @as(f64, @floatFromInt(intersection)) / @as(f64, @floatFromInt(union_count));}pub fn loadGraphFromGit(allocator: std.mem.Allocator, root: []const u8, ref: []const u8, invalid: *bool, limits: smg.Limits) !?graph_mod.Graph {    invalid.* = false;    if (!std.mem.eql(u8, ref, "HEAD")) {        if (!try git.refExists(allocator, root, ref)) {            invalid.* = true;            return null;        }    }    const temp_root = try std.fmt.allocPrint(allocator, "/tmp/smg-diff-ref-{x}-{x}", .{ @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))), @intFromPtr(&allocator) });    defer allocator.free(temp_root);    defer sys.fs.deleteTree(temp_root) catch {};    const add = try git.run(allocator, root, &.{ "git", "worktree", "add", "--detach", "--quiet", temp_root, ref }, 1024 * 1024);    if (!add.ok) return null;    defer {        _ = git.run(allocator, root, &.{ "git", "worktree", "remove", "--force", temp_root }, 1024 * 1024) catch {};    }    var graph = graph_mod.init(allocator);    _ = try scan.scanPathsWithOptions(allocator, &graph, temp_root, &.{temp_root}, false, .{ .limits = limits });    return graph;}test "loadGraphFromGit scans committed source ref" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-diff-git-test-{x}-{x}", .{ @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))), @intFromPtr(&arena) });    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    try std.testing.expect((try git.run(allocator, root, &.{ "git", "init" }, 1024 * 1024)).ok);    const source_path = try std.fs.path.join(allocator, &.{ root, "app.py" });    try text.writeFile(source_path, "def alpha():\n    return 1\n");    try std.testing.expect((try git.run(allocator, root, &.{ "git", "add", "app.py" }, 1024 * 1024)).ok);    try std.testing.expect((try git.run(allocator, root, &.{ "git", "-c", "user.name=smg", "-c", "user.email=smg@example.invalid", "commit", "-m", "base" }, 1024 * 1024)).ok);    try text.writeFile(source_path, "def alpha():\n    return 1\n\ndef beta():\n    return 2\n");    var invalid = false;    const loaded = (try loadGraphFromGit(allocator, root, "HEAD", &invalid, smg.default_limits)).?;    try std.testing.expect(!invalid);    try std.testing.expect(graph_mod.getNode(&loaded, "app.alpha") != null);    try std.testing.expect(graph_mod.getNode(&loaded, "app.beta") == null);}pub fn toJson(allocator: std.mem.Allocator, result: Result, ref: []const u8) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    var writer = pretty_json.Writer.init(&out.writer, .minified);    try writer.beginObject();    try writer.objectField("ref");    try writer.write(ref);    try writer.objectField("added_nodes");    try writeNodeNames(&writer, result.added_nodes);    try writer.objectField("removed_nodes");    try writeNodeNames(&writer, result.removed_nodes);    try writer.objectField("changed_nodes");    try writer.beginArray();    for (result.changed_nodes) |changed| {        try writer.beginObject();        try writer.objectField("name");        try writer.write(changed.node.name);        try writer.objectField("changes");        try writer.beginArray();        for (changed.changes) |change| {            try writer.beginObject();            try writer.objectField("field");            try writer.write(change.field);            try writer.objectField("old");            try writer.write(change.old);            try writer.objectField("new");            try writer.write(change.new);            try writer.endObject();        }        try writer.endArray();        try writer.endObject();    }    try writer.endArray();    try writer.objectField("renamed_nodes");    try writer.beginArray();    for (result.renamed_nodes) |renamed| {        try writer.beginObject();        try writer.objectField("old_name");        try writer.write(renamed.old_name);        try writer.objectField("new_name");        try writer.write(renamed.new_name);        try writer.objectField("match_type");        try writer.write(renamed.match_type);        try writer.endObject();    }    try writer.endArray();    try writer.objectField("added_edges");    try writeEdges(&writer, result.added_edges);    try writer.objectField("removed_edges");    try writeEdges(&writer, result.removed_edges);    try writer.objectField("summary");    try writer.beginObject();    try writer.objectField("nodes_added");    try writer.write(result.added_nodes.len);    try writer.objectField("nodes_removed");    try writer.write(result.removed_nodes.len);    try writer.objectField("nodes_changed");    try writer.write(result.changed_nodes.len);    try writer.objectField("nodes_renamed");    try writer.write(result.renamed_nodes.len);    try writer.objectField("edges_added");    try writer.write(result.added_edges.len);    try writer.objectField("edges_removed");    try writer.write(result.removed_edges.len);    try writer.endObject();    try writer.endObject();    try out.writer.writeByte('\n');    return try out.toOwnedSlice();}pub fn toText(allocator: std.mem.Allocator, result: Result, ref: []const u8, old_missing: bool) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    if (old_missing) try out.writer.print("No graph found at ref {s}. Showing full graph as new.\n", .{ref});    if (result.empty()) {        try out.writer.print("No structural changes vs {s}.\n", .{ref});        return try out.toOwnedSlice();    }    try out.writer.print("Diff vs {s}\n\n", .{ref});    if (result.added_nodes.len != 0) {        try out.writer.print("+ {d} node(s) added\n", .{result.added_nodes.len});        for (result.added_nodes) |node| try out.writer.print("  + [{s}] {s}\n", .{ node.type, node.name });    }    if (result.removed_nodes.len != 0) {        try out.writer.print("- {d} node(s) removed\n", .{result.removed_nodes.len});        for (result.removed_nodes) |node| try out.writer.print("  - [{s}] {s}\n", .{ node.type, node.name });    }    if (result.changed_nodes.len != 0) {        try out.writer.print("~ {d} node(s) changed\n", .{result.changed_nodes.len});        for (result.changed_nodes) |changed| {            try out.writer.print("  ~ {s}\n", .{changed.node.name});            for (changed.changes) |change| try out.writer.print("      {s}: {s} → {s}\n", .{ change.field, change.old orelse "None", change.new orelse "None" });        }    }    if (result.renamed_nodes.len != 0) {        try out.writer.print("~ {d} node(s) renamed/moved\n", .{result.renamed_nodes.len});        for (result.renamed_nodes) |renamed| {            const tag: []const u8 = if (std.mem.eql(u8, renamed.match_type, "content")) "exact" else "structural";            try out.writer.print("  ~ {s} → {s} ({s} match)\n", .{ renamed.old_name, renamed.new_name, tag });        }    }    if (result.added_edges.len != 0) {        try out.writer.print("+ {d} edge(s) added\n", .{result.added_edges.len});        for (result.added_edges[0..@min(result.added_edges.len, 20)]) |edge| try out.writer.print("  + {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target });        if (result.added_edges.len > 20) try out.writer.print("  ... and {d} more\n", .{result.added_edges.len - 20});    }    if (result.removed_edges.len != 0) {        try out.writer.print("- {d} edge(s) removed\n", .{result.removed_edges.len});        for (result.removed_edges[0..@min(result.removed_edges.len, 20)]) |edge| try out.writer.print("  - {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target });        if (result.removed_edges.len > 20) try out.writer.print("  ... and {d} more\n", .{result.removed_edges.len - 20});    }    try out.writer.writeAll("\nNodes: ");    var wrote = false;    if (result.added_nodes.len != 0) {        try out.writer.print("+{d}", .{result.added_nodes.len});        wrote = true;    }    if (result.removed_nodes.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("-{d}", .{result.removed_nodes.len});        wrote = true;    }    if (result.changed_nodes.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("~{d}", .{result.changed_nodes.len});        wrote = true;    }    if (result.renamed_nodes.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("↷{d}", .{result.renamed_nodes.len});    }    try out.writer.print(" | Edges: +{d} -{d}\n", .{ result.added_edges.len, result.removed_edges.len });    return try out.toOwnedSlice();}fn writeNodeNames(writer: *pretty_json.Writer, nodes: []const model.Node) !void {    try writer.beginArray();    for (nodes) |node| try writer.write(node.name);    try writer.endArray();}fn writeEdges(writer: *pretty_json.Writer, edges: []const model.Edge) !void {    try writer.beginArray();    for (edges) |edge| {        try writer.beginObject();        try writer.objectField("source");        try writer.write(edge.source);        try writer.objectField("rel");        try writer.write(edge.rel);        try writer.objectField("target");        try writer.write(edge.target);        try writer.endObject();    }    try writer.endArray();}fn containsString(values: []const []const u8, value: []const u8) bool {    for (values) |item| {        if (std.mem.eql(u8, item, value)) return true;    }    return false;}fn appendUniqueString(allocator: std.mem.Allocator, list: *std.ArrayList([]const u8), value: []const u8) !void {    if (!containsString(list.items, value)) try list.append(allocator, value);}fn linearHasEdge(graph: graph_mod.Graph, edge: model.Edge) bool {    for (graph.edges.items) |candidate| {        if (std.mem.eql(u8, candidate.source, edge.source) and            std.mem.eql(u8, candidate.rel, edge.rel) and            std.mem.eql(u8, candidate.target, edge.target)) return true;    }    return false;}fn linearEdgeDiff(    allocator: std.mem.Allocator,    subject: graph_mod.Graph,    other: graph_mod.Graph,) ![]const model.Edge {    var edges: std.ArrayList(model.Edge) = .empty;    for (subject.edges.items) |edge| {        if (!linearHasEdge(other, edge)) try edges.append(allocator, edge);    }    view.sortEdges(edges.items);    return try edges.toOwnedSlice(allocator);}fn expectEdgeIdentityEqual(expected: []const model.Edge, actual: []const model.Edge) !void {    try std.testing.expectEqual(expected.len, actual.len);    for (expected, actual) |left, right| {        try std.testing.expectEqualStrings(left.source, right.source);        try std.testing.expectEqualStrings(left.rel, right.rel);        try std.testing.expectEqualStrings(left.target, right.target);    }}const DifferentialFixture = struct {    old: graph_mod.Graph,    new: graph_mod.Graph,};fn differentialFixture(allocator: std.mem.Allocator, seed: usize) !DifferentialFixture {    const node_count = 32;    var names: [node_count][]const u8 = undefined;    for (&names, 0..) |*name, index| {        name.* = try std.fmt.allocPrint(allocator, "node_{d:0>2}", .{index});    }    var old = graph_mod.init(allocator);    var new = graph_mod.init(allocator);    for (0..node_count) |step| {        const old_index = (step * 5 + seed) % node_count;        const new_index = (step * 7 + seed) % node_count;        try graph_mod.addNode(&old, .{ .name = names[old_index], .type = model.NodeType.function });        try graph_mod.addNode(&new, .{ .name = names[new_index], .type = model.NodeType.function });    }    for (0..node_count) |step| {        const old_index = (step * 5 + seed) % node_count;        const new_index = (step * 7 + seed) % node_count;        try graph_mod.addEdge(&old, .{            .source = names[old_index],            .rel = "common",            .target = names[(old_index + 1) % node_count],        });        try graph_mod.addEdge(&new, .{            .source = names[new_index],            .rel = "common",            .target = names[(new_index + 1) % node_count],        });    }    for (0..8) |index| {        const target = names[(index + 9) % node_count];        try graph_mod.addEdge(&old, .{ .source = names[index], .rel = "old", .target = target });        try graph_mod.addEdge(&new, .{            .source = names[7 - index],            .rel = "new",            .target = names[(7 - index + 9) % node_count],        });    }    return .{ .old = old, .new = new };}test "diff detects added removed changed and edges" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var old = graph_mod.init(allocator);    try graph_mod.addNode(&old, .{ .name = "app", .type = model.NodeType.module });    try graph_mod.addNode(&old, .{ .name = "app.main", .type = model.NodeType.function, .file = "app.py", .line = 1, .docstring = "Entry" });    try graph_mod.addNode(&old, .{ .name = "app.Server", .type = model.NodeType.class, .file = "app.py", .line = 10 });    try graph_mod.addEdge(&old, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" });    try graph_mod.addEdge(&old, .{ .source = "app", .rel = model.RelType.contains, .target = "app.Server" });    var new = graph_mod.init(allocator);    try graph_mod.addNode(&new, .{ .name = "app", .type = model.NodeType.module });    try graph_mod.addNode(&new, .{ .name = "app.main", .type = model.NodeType.function, .file = "app.py", .line = 5, .docstring = "Updated" });    try graph_mod.addNode(&new, .{ .name = "app.helper", .type = model.NodeType.function });    try graph_mod.addEdge(&new, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" });    try graph_mod.addEdge(&new, .{ .source = "app", .rel = model.RelType.contains, .target = "app.helper" });    try graph_mod.addEdge(&new, .{ .source = "app.main", .rel = model.RelType.calls, .target = "app.helper" });    const result = try graphs(allocator, old, new, true);    try std.testing.expectEqual(@as(usize, 1), result.added_nodes.len);    try std.testing.expectEqual(@as(usize, 1), result.removed_nodes.len);    try std.testing.expectEqual(@as(usize, 1), result.changed_nodes.len);    try std.testing.expectEqual(@as(usize, 2), result.added_edges.len);    try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len);    try std.testing.expectEqualStrings("app.helper", result.added_nodes[0].name);}test "diff preserves edge identity and text output" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var old = graph_mod.init(allocator);    var new = graph_mod.init(allocator);    const names = [_][]const u8{ "root", "shared", "old_only", "new_only" };    for (names) |name| {        const node: model.Node = .{ .name = name, .type = model.NodeType.function };        try graph_mod.addNode(&old, node);        try graph_mod.addNode(&new, node);    }    const old_metadata = [_]model.Pair{.{ .key = "weight", .value = "1" }};    const new_metadata = [_]model.Pair{.{ .key = "weight", .value = "2" }};    try graph_mod.addEdge(&old, .{        .source = "root",        .rel = model.RelType.contains,        .target = "shared",        .metadata = &old_metadata,    });    try graph_mod.addEdge(&new, .{        .source = "root",        .rel = model.RelType.contains,        .target = "shared",        .metadata = &new_metadata,    });    try graph_mod.addEdge(&old, .{        .source = "root",        .rel = model.RelType.calls,        .target = "old_only",    });    try graph_mod.addEdge(&new, .{        .source = "root",        .rel = model.RelType.calls,        .target = "new_only",    });    const result = try graphs(allocator, old, new, false);    try std.testing.expectEqual(@as(usize, 1), result.added_edges.len);    try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len);    try std.testing.expectEqualStrings("new_only", result.added_edges[0].target);    try std.testing.expectEqualStrings("old_only", result.removed_edges[0].target);    try std.testing.expectEqualStrings(        "Diff vs base\n\n" ++            "+ 1 edge(s) added\n" ++            "  + root --calls--> new_only\n" ++            "- 1 edge(s) removed\n" ++            "  - root --calls--> old_only\n" ++            "\nNodes:  | Edges: +1 -1\n",        try toText(allocator, result, "base", false),    );}test "diff verifies indexed edge identity without graph allocation" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var old = graph_mod.init(allocator);    var new = graph_mod.init(allocator);    const long_source = try allocator.alloc(u8, 4096);    @memset(long_source, 'x');    const names = [_][]const u8{        "a\x1fb",        "d",        "a",        "c\x1fd",        long_source,        "long-target",    };    for (names) |name| {        const node: model.Node = .{ .name = name, .type = model.NodeType.function };        try graph_mod.addNode(&old, node);        try graph_mod.addNode(&new, node);    }    try graph_mod.addEdge(&old, .{ .source = "a\x1fb", .rel = "c", .target = "d" });    try graph_mod.addEdge(&new, .{ .source = "a", .rel = "b", .target = "c\x1fd" });    const long_edge: model.Edge = .{        .source = long_source,        .rel = model.RelType.calls,        .target = "long-target",    };    try graph_mod.addEdge(&old, long_edge);    try graph_mod.addEdge(&new, long_edge);    var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });    old.allocator = failing.allocator();    new.allocator = failing.allocator();    const result = try graphs(allocator, old, new, false);    try std.testing.expectEqual(@as(usize, 1), result.added_edges.len);    try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len);    try std.testing.expectEqualStrings("c\x1fd", result.added_edges[0].target);    try std.testing.expectEqualStrings("a\x1fb", result.removed_edges[0].source);}test "diff index matches linear edge oracle across insertion orders" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    for (0..16) |seed| {        const fixture = try differentialFixture(allocator, seed);        const result = try graphs(allocator, fixture.old, fixture.new, false);        const expected_added = try linearEdgeDiff(allocator, fixture.new, fixture.old);        const expected_removed = try linearEdgeDiff(allocator, fixture.old, fixture.new);        try expectEdgeIdentityEqual(expected_added, result.added_edges);        try expectEdgeIdentityEqual(expected_removed, result.removed_edges);    }}test "diff detects hash and fuzzy renames" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var old = graph_mod.init(allocator);    var new = graph_mod.init(allocator);    const old_meta = [_]model.Pair{        .{ .key = "content_hash", .value = "same" },        .{ .key = "structure_hash", .value = "shape" },    };    const new_meta = [_]model.Pair{        .{ .key = "content_hash", .value = "same" },        .{ .key = "structure_hash", .value = "shape" },    };    try graph_mod.addNode(&old, .{ .name = "app.foo", .type = model.NodeType.function, .metadata = &old_meta });    try graph_mod.addNode(&new, .{ .name = "app.bar", .type = model.NodeType.function, .metadata = &new_meta });    const result = try graphs(allocator, old, new, true);    try std.testing.expectEqual(@as(usize, 1), result.renamed_nodes.len);    try std.testing.expectEqualStrings("content", result.renamed_nodes[0].match_type);}

Source: tools/smg/src/root.zig:20

zig
pub const diff = @import("diff.zig");

Complete caller list for diff.graphs

9 direct callers.

Audit

Definitions10
Public names10
Members17
Version26.7.0
Revisiondaab053ee433