const std = @import("std"); /// RunManifest represents a run manifest - identical schema between local and server /// Schema compatibility is a hard requirement enforced here pub const RunManifest = struct { run_id: []const u8, experiment: []const u8, command: []const u8, args: [][]const u8, commit_id: ?[]const u8, started_at: []const u8, ended_at: ?[]const u8, status: []const u8, // RUNNING, FINISHED, FAILED, CANCELLED exit_code: ?i32, params: std.StringHashMap([]const u8), metrics_summary: ?std.StringHashMap(f64), artifact_path: []const u8, synced: bool, pub fn init(allocator: std.mem.Allocator) RunManifest { return .{ .run_id = "", .experiment = "", .command = "", .args = &[_][]const u8{}, .commit_id = null, .started_at = "", .ended_at = null, .status = "RUNNING", .exit_code = null, .params = std.StringHashMap([]const u8).init(allocator), .metrics_summary = null, .artifact_path = "", .synced = false, }; } pub fn deinit(self: *RunManifest, allocator: std.mem.Allocator) void { var params_iter = self.params.iterator(); while (params_iter.next()) |entry| { allocator.free(entry.key_ptr.*); allocator.free(entry.value_ptr.*); } self.params.deinit(); if (self.metrics_summary) |*summary| { var summary_iter = summary.iterator(); while (summary_iter.next()) |entry| { allocator.free(entry.key_ptr.*); } summary.deinit(); } for (self.args) |arg| { allocator.free(arg); } allocator.free(self.args); } }; /// Write manifest to JSON file pub fn writeManifest(manifest: RunManifest, path: []const u8, allocator: std.mem.Allocator) !void { var file = try std.fs.cwd().createFile(path, .{}); defer file.close(); // Write JSON manually to avoid std.json complexity with hash maps try file.writeAll("{\n"); const line1 = try std.fmt.allocPrint(allocator, " \"run_id\": \"{s}\",\n", .{manifest.run_id}); defer allocator.free(line1); try file.writeAll(line1); const line2 = try std.fmt.allocPrint(allocator, " \"experiment\": \"{s}\",\n", .{manifest.experiment}); defer allocator.free(line2); try file.writeAll(line2); const line3 = try std.fmt.allocPrint(allocator, " \"command\": \"{s}\",\n", .{manifest.command}); defer allocator.free(line3); try file.writeAll(line3); // Args array try file.writeAll(" \"args\": ["); for (manifest.args, 0..) |arg, i| { if (i > 0) try file.writeAll(", "); const arg_str = try std.fmt.allocPrint(allocator, "\"{s}\"", .{arg}); defer allocator.free(arg_str); try file.writeAll(arg_str); } try file.writeAll("],\n"); // Commit ID (optional) if (manifest.commit_id) |cid| { const cid_str = try std.fmt.allocPrint(allocator, " \"commit_id\": \"{s}\",\n", .{cid}); defer allocator.free(cid_str); try file.writeAll(cid_str); } else { try file.writeAll(" \"commit_id\": null,\n"); } const started_str = try std.fmt.allocPrint(allocator, " \"started_at\": \"{s}\",\n", .{manifest.started_at}); defer allocator.free(started_str); try file.writeAll(started_str); // Ended at (optional) if (manifest.ended_at) |ended| { const ended_str = try std.fmt.allocPrint(allocator, " \"ended_at\": \"{s}\",\n", .{ended}); defer allocator.free(ended_str); try file.writeAll(ended_str); } else { try file.writeAll(" \"ended_at\": null,\n"); } const status_str = try std.fmt.allocPrint(allocator, " \"status\": \"{s}\",\n", .{manifest.status}); defer allocator.free(status_str); try file.writeAll(status_str); // Exit code (optional) if (manifest.exit_code) |code| { const exit_str = try std.fmt.allocPrint(allocator, " \"exit_code\": {d},\n", .{code}); defer allocator.free(exit_str); try file.writeAll(exit_str); } else { try file.writeAll(" \"exit_code\": null,\n"); } // Params object try file.writeAll(" \"params\": {"); var params_first = true; var params_iter = manifest.params.iterator(); while (params_iter.next()) |entry| { if (!params_first) try file.writeAll(", "); params_first = false; const param_str = try std.fmt.allocPrint(allocator, "\"{s}\": \"{s}\"", .{ entry.key_ptr.*, entry.value_ptr.* }); defer allocator.free(param_str); try file.writeAll(param_str); } try file.writeAll("},\n"); // Metrics summary (optional) if (manifest.metrics_summary) |summary| { try file.writeAll(" \"metrics_summary\": {"); var summary_first = true; var summary_iter = summary.iterator(); while (summary_iter.next()) |entry| { if (!summary_first) try file.writeAll(", "); summary_first = false; const metric_str = try std.fmt.allocPrint(allocator, "\"{s}\": {d:.4}", .{ entry.key_ptr.*, entry.value_ptr.* }); defer allocator.free(metric_str); try file.writeAll(metric_str); } try file.writeAll("},\n"); } else { try file.writeAll(" \"metrics_summary\": null,\n"); } const artifact_str = try std.fmt.allocPrint(allocator, " \"artifact_path\": \"{s}\",\n", .{manifest.artifact_path}); defer allocator.free(artifact_str); try file.writeAll(artifact_str); const synced_str = try std.fmt.allocPrint(allocator, " \"synced\": {}", .{manifest.synced}); defer allocator.free(synced_str); try file.writeAll(synced_str); try file.writeAll("\n}\n"); } /// Read manifest from JSON file pub fn readManifest(path: []const u8, allocator: std.mem.Allocator) !RunManifest { var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{}); defer parsed.deinit(); if (parsed.value != .object) { return error.InvalidManifest; } const root = parsed.value.object; var manifest = RunManifest.init(allocator); // Required fields manifest.run_id = try getStringField(allocator, root, "run_id") orelse return error.MissingRunId; manifest.experiment = try getStringField(allocator, root, "experiment") orelse return error.MissingExperiment; manifest.command = try getStringField(allocator, root, "command") orelse return error.MissingCommand; manifest.status = try getStringField(allocator, root, "status") orelse "RUNNING"; manifest.started_at = try getStringField(allocator, root, "started_at") orelse ""; // Optional fields manifest.ended_at = try getStringField(allocator, root, "ended_at"); manifest.commit_id = try getStringField(allocator, root, "commit_id"); manifest.artifact_path = try getStringField(allocator, root, "artifact_path") orelse ""; // Synced boolean if (root.get("synced")) |synced_val| { if (synced_val == .bool) { manifest.synced = synced_val.bool; } } // Exit code if (root.get("exit_code")) |exit_val| { if (exit_val == .integer) { manifest.exit_code = @intCast(exit_val.integer); } } // Args array if (root.get("args")) |args_val| { if (args_val == .array) { const args = try allocator.alloc([]const u8, args_val.array.items.len); for (args_val.array.items, 0..) |arg, i| { if (arg == .string) { args[i] = try allocator.dupe(u8, arg.string); } } manifest.args = args; } } // Params object if (root.get("params")) |params_val| { if (params_val == .object) { var params_iter = params_val.object.iterator(); while (params_iter.next()) |entry| { if (entry.value_ptr.* == .string) { const key = try allocator.dupe(u8, entry.key_ptr.*); const value = try allocator.dupe(u8, entry.value_ptr.*.string); try manifest.params.put(key, value); } } } } // Metrics summary if (root.get("metrics_summary")) |metrics_val| { if (metrics_val == .object) { var summary = std.StringHashMap(f64).init(allocator); var metrics_iter = metrics_val.object.iterator(); while (metrics_iter.next()) |entry| { const val = entry.value_ptr.*; if (val == .float) { const key = try allocator.dupe(u8, entry.key_ptr.*); try summary.put(key, val.float); } else if (val == .integer) { const key = try allocator.dupe(u8, entry.key_ptr.*); try summary.put(key, @floatFromInt(val.integer)); } } manifest.metrics_summary = summary; } } return manifest; } /// Get string field from JSON object, duplicating the string fn getStringField(allocator: std.mem.Allocator, obj: std.json.ObjectMap, field: []const u8) !?[]const u8 { const val = obj.get(field) orelse return null; if (val != .string) return null; return try allocator.dupe(u8, val.string); } /// Update manifest status and ended_at on run completion pub fn updateManifestStatus(path: []const u8, status: []const u8, exit_code: ?i32, allocator: std.mem.Allocator) !void { var manifest = try readManifest(path, allocator); defer manifest.deinit(allocator); manifest.status = status; manifest.exit_code = exit_code; // Set ended_at to current timestamp const now = std.time.timestamp(); const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(now) }; const epoch_day = epoch_seconds.getEpochDay(); const year_day = epoch_day.calculateYearDay(); const month_day = year_day.calculateMonthDay(); const day_seconds = epoch_seconds.getDaySeconds(); var buf: [30]u8 = undefined; const timestamp = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{ year_day.year, month_day.month.numeric(), month_day.day_index + 1, day_seconds.getHoursIntoDay(), day_seconds.getMinutesIntoHour(), day_seconds.getSecondsIntoMinute(), }) catch unreachable; manifest.ended_at = try allocator.dupe(u8, timestamp); try writeManifest(manifest, path, allocator); } /// Mark manifest as synced pub fn markManifestSynced(path: []const u8, allocator: std.mem.Allocator) !void { var manifest = try readManifest(path, allocator); defer manifest.deinit(allocator); manifest.synced = true; try writeManifest(manifest, path, allocator); } /// Build manifest path from experiment and run_id pub fn buildManifestPath(artifact_path: []const u8, experiment: []const u8, run_id: []const u8, allocator: std.mem.Allocator) ![]const u8 { return std.fs.path.join(allocator, &[_][]const u8{ artifact_path, experiment, run_id, "run_manifest.json", }); } /// Resolve manifest path from input (path, run_id, or task_id) pub fn resolveManifestPath(input: []const u8, base_path: ?[]const u8, allocator: std.mem.Allocator) ![]const u8 { // If input is a valid file path, use it directly if (std.fs.path.isAbsolute(input)) { if (std.fs.cwd().access(input, .{})) { // It's a file or directory const stat = std.fs.cwd().statFile(input) catch { // It's a directory, append manifest name return std.fs.path.join(allocator, &[_][]const u8{ input, "run_manifest.json" }); }; _ = stat; // It's a file, use as-is return try allocator.dupe(u8, input); } else |_| {} } // Try relative path if (std.fs.cwd().access(input, .{})) { const stat = std.fs.cwd().statFile(input) catch { return std.fs.path.join(allocator, &[_][]const u8{ input, "run_manifest.json" }); }; _ = stat; return try allocator.dupe(u8, input); } else |_| {} // Search by run_id in base_path if (base_path) |bp| { return try findManifestById(bp, input, allocator); } return error.ManifestNotFound; } /// Find manifest by run_id in base path fn findManifestById(base_path: []const u8, id: []const u8, allocator: std.mem.Allocator) ![]const u8 { // Look in experiments/ subdirectories var experiments_dir = std.fs.cwd().openDir(base_path, .{ .iterate = true }) catch { return error.ManifestNotFound; }; defer experiments_dir.close(); var iter = experiments_dir.iterate(); while (try iter.next()) |entry| { if (entry.kind != .directory) continue; // Check if this experiment has a subdirectory matching the run_id const run_dir_path = try std.fs.path.join(allocator, &[_][]const u8{ base_path, entry.name, id, }); defer allocator.free(run_dir_path); const manifest_path = try std.fs.path.join(allocator, &[_][]const u8{ run_dir_path, "run_manifest.json", }); if (std.fs.cwd().access(manifest_path, .{})) { return manifest_path; } else |_| { allocator.free(manifest_path); } } return error.ManifestNotFound; }