diff --git a/README.md b/README.md index e7cdff4..bd9aaf6 100644 --- a/README.md +++ b/README.md @@ -367,3 +367,6 @@ This keeps the service headless while letting `nullhub` own install/setup UI. - Add dataset, prompt version, and experiment entities. - Add regression diff endpoints for comparing prompt/model/strategy versions. - Add alert rules and anomaly summaries that `nullhub` can render. + +## Community SDKs +- **[nullwatch-python-sdk](https://github.com/nullclaw/nullwatch-python-sdk/)** — Python SDK with zero required dependencies. Ships built-in eval scorers for RAG hallucination detection ([LettuceDetect](https://github.com/KRLabsOrg/LettuceDetect)) and tool-call schema validation. diff --git a/src/compat/fs.zig b/src/compat/fs.zig index 2be980d..f2025b1 100644 --- a/src/compat/fs.zig +++ b/src/compat/fs.zig @@ -222,10 +222,7 @@ pub const Dir = struct { pub fn makePath(self: Dir, sub_path: []const u8) !void { if (sub_path.len == 0) return; if (path.isAbsolute(sub_path)) { - makeDirAbsolute(sub_path) catch |err| switch (err) { - error.PathAlreadyExists => return, - else => |e| return e, - }; + try makePathAbsolute(sub_path); return; } @@ -281,6 +278,10 @@ pub fn makeDirAbsolute(absolute_path: []const u8) Io.Dir.CreateDirError!void { try Io.Dir.createDirAbsolute(shared.io(), absolute_path, .default_dir); } +pub fn makePathAbsolute(absolute_path: []const u8) Io.Dir.CreateDirPathError!void { + try Io.Dir.createDirPath(Io.Dir.cwd(), shared.io(), absolute_path); +} + pub fn deleteFileAbsolute(absolute_path: []const u8) Io.Dir.DeleteFileError!void { try Io.Dir.deleteFileAbsolute(shared.io(), absolute_path); } @@ -305,3 +306,20 @@ pub fn realpathAlloc(allocator: Allocator, file_path: []const u8) ![]u8 { } return try cwd().realpathAlloc(allocator, file_path); } + +test "makePathAbsolute creates nested absolute directories" { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_dir = Dir.wrap(tmp.dir); + const root = try tmp_dir.realpathAlloc(std.testing.allocator, "."); + defer std.testing.allocator.free(root); + + const nested = try path.join(std.testing.allocator, &.{ root, "nested", "path" }); + defer std.testing.allocator.free(nested); + + try makePathAbsolute(nested); + try accessAbsolute(nested, .{}); + + try makePathAbsolute(nested); +} diff --git a/src/from_json.zig b/src/from_json.zig index f37b6d8..a7e12c9 100644 --- a/src/from_json.zig +++ b/src/from_json.zig @@ -62,10 +62,7 @@ fn getU16(obj: std.json.ObjectMap, key: []const u8) ?u16 { fn ensureHome(home: []const u8) !void { if (std.fs.path.isAbsolute(home)) { - std_compat.fs.makeDirAbsolute(home) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; + try std_compat.fs.makePathAbsolute(home); return; } diff --git a/src/store.zig b/src/store.zig index a9ada2d..eddffc0 100644 --- a/src/store.zig +++ b/src/store.zig @@ -420,10 +420,7 @@ fn finalizeVerdict(summary: *domain.RunSummary) void { fn ensureDirExists(path: []const u8) !void { if (std.fs.path.isAbsolute(path)) { - std_compat.fs.makeDirAbsolute(path) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; + try std_compat.fs.makePathAbsolute(path); return; }