Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/graph/ppr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Andersen-Chung-Lang push approximation for PPR.
// Given a query node, computes relevance scores for all reachable nodes.
//
// Push rule: if r[u] > ε × deg(u), push u:
// Push rule: if r[u] > ε × W_out(u), push u (W_out = total outgoing edge weight):
// p[u] += α × r[u]
// r[v] += (1-α) × r[u] × W(u,v) / W_out(u) for each out-neighbour v
// r[u] = 0
Expand All @@ -27,7 +27,14 @@ pub const ScoredNode = struct {
///
/// Returns a map of node_id → PPR score for all nodes with non-zero score.
/// `alpha` is the teleport probability (typically 0.15).
/// `epsilon` is the convergence threshold per unit of degree.
/// `epsilon` scales the push threshold with total out-weight W_out(u) (not raw degree).
fn totalOutWeight(g: *const CodeGraph, u: u64) f32 {
const edges = g.outEdges(u);
var s: f32 = 0;
for (edges) |e| s += e.weight;
return s;
}

pub fn pprPush(
g: *const CodeGraph,
query_node: u64,
Expand Down Expand Up @@ -69,11 +76,8 @@ pub fn pprPush(
while (it.next()) |entry| {
const u = entry.key_ptr.*;
const r_u = entry.value_ptr.*;
const deg = g.outDegree(u);
const threshold = if (deg > 0)
epsilon * @as(f32, @floatFromInt(deg))
else
epsilon;
const w_out = totalOutWeight(g, u);
const threshold = if (w_out > 0) epsilon * w_out else epsilon;
if (r_u > threshold) {
try to_push.append(alloc, u);
}
Expand Down
35 changes: 26 additions & 9 deletions src/graph/storage.zig
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,16 @@ pub fn deserialize(reader: anytype, alloc: std.mem.Allocator) !CodeGraph {

// ── File I/O convenience ────────────────────────────────────────────────────

/// Save a CodeGraph to a file path.
pub fn saveToFile(g: *const CodeGraph, path: []const u8) !void {
/// Save a CodeGraph to a file path. `alloc` is used for the serialization buffer.
pub fn saveToFile(g: *const CodeGraph, path: []const u8, alloc: std.mem.Allocator) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
// Serialize to in-memory buffer, then write all at once
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc_for_save);
try serialize(g, buf.writer(alloc_for_save));
defer buf.deinit(alloc);
try serialize(g, buf.writer(alloc));
try file.writeAll(buf.items);
}

/// Temporary allocator for saveToFile — uses page_allocator since we
/// don't have access to a caller-provided allocator in the current API.
const alloc_for_save = std.heap.page_allocator;

/// Load a CodeGraph from a file path.
pub fn loadFromFile(path: []const u8, alloc: std.mem.Allocator) !CodeGraph {
const file = try std.fs.cwd().openFile(path, .{});
Expand Down Expand Up @@ -879,3 +874,25 @@ test "round-trip with empty strings everywhere" {
try std.testing.expectEqualStrings("", g2.getCommit(1).?.author);
try std.testing.expectEqualStrings("", g2.getCommit(1).?.message);
}

test "saveToFile and loadFromFile round-trip (#405)" {
const alloc = std.testing.allocator;
var g = CodeGraph.init(alloc);
defer g.deinit();

try g.addSymbol(.{ .id = 1, .name = "foo", .kind = .function, .file_id = 1, .line = 5, .col = 0, .scope = "mod" });
try g.addFile(.{ .id = 1, .path = "a.zig", .language = .zig, .last_modified = 100, .hash = [_]u8{0xAB} ** 32 });
try g.addEdge(.{ .src = 1, .dst = 1, .kind = .calls, .weight = 3.0 });

const tmp = "test_save_load_405.bin";
try saveToFile(&g, tmp, alloc);
defer std.fs.cwd().deleteFile(tmp) catch {};

var g2 = try loadFromFile(tmp, alloc);
defer g2.deinit();

try std.testing.expectEqual(g.symbolCount(), g2.symbolCount());
try std.testing.expectEqual(g.files.count(), g2.files.count());
try std.testing.expectEqual(g.edgeCount(), g2.edgeCount());
try std.testing.expectEqualStrings("foo", g2.getSymbol(1).?.name);
}
10 changes: 6 additions & 4 deletions src/tools.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2166,7 +2166,9 @@ fn runAgentWithRole(
timeout_seconds: ?u32,
out: *std.ArrayList(u8),
) void {
runChainStep(alloc, role, mode, writable_flag, null, prompt, timeout_seconds, out);
const sec: u32 = timeout_seconds orelse 300;
const ms: u64 = @as(u64, sec) * std.time.ms_per_s;
runChainStep(alloc, role, mode, writable_flag, null, prompt, ms, sec, out);
}

fn parseTimeoutSeconds(args: *const std.json.ObjectMap, default_seconds: u32) u32 {
Expand Down Expand Up @@ -2217,7 +2219,7 @@ fn runChainStepWithBudget(
appendTimedOutJson(alloc, step_out, total_timeout_seconds);
return;
};
runChainStep(alloc, role, mode, writable_override, permission_mode, prompt, remaining, step_out);
runChainStep(alloc, role, mode, writable_override, permission_mode, prompt, @as(u64, remaining) * std.time.ms_per_s, remaining, step_out);
}

fn handleRunReviewer(
Expand Down Expand Up @@ -2381,7 +2383,7 @@ fn handleReviewFixLoop(
iter_json.appendSlice(alloc, ",\"review\":\"") catch return;
var review_out: std.ArrayList(u8) = .empty;
defer review_out.deinit(alloc);
runChainStep(alloc, "reviewer", null, false, null, review_prompt, 300, &review_out);
runChainStep(alloc, "reviewer", null, false, null, review_prompt, 300 * std.time.ms_per_s, 300, &review_out);

mj.writeEscaped(alloc, &iter_json, review_out.items);
iter_json.appendSlice(alloc, "\"") catch return;
Expand Down Expand Up @@ -2424,7 +2426,7 @@ fn handleReviewFixLoop(

var fix_out: std.ArrayList(u8) = .empty;
defer fix_out.deinit(alloc);
runChainStep(alloc, "fixer", null, true, null, fix_prompt, 300, &fix_out);
runChainStep(alloc, "fixer", null, true, null, fix_prompt, 300 * std.time.ms_per_s, 300, &fix_out);

iter_json.appendSlice(alloc, ",\"fix\":\"") catch return;
mj.writeEscaped(alloc, &iter_json, fix_out.items);
Expand Down
Loading