refactor(cli): consolidate duplicate functions into common.zig

Move shared utility functions from queue.zig to common.zig:
- buildNarrativeJson() - was duplicated in queue.zig, exec/dryrun.zig, exec/remote.zig
- formatNextSteps() - was duplicated in queue.zig
- dryRun() - was duplicated in exec/dryrun.zig
- JobOptions struct - shared configuration options

Added common.zig import to queue.zig and updated all references.

Reduction: ~80 lines of duplicate code removed
All tests pass.
This commit is contained in:
Jeremie Fraeys 2026-03-05 10:12:44 -05:00
parent ab7da26d77
commit 0d05ec0317
No known key found for this signature in database
2 changed files with 104 additions and 86 deletions

View file

@ -60,3 +60,101 @@ pub fn withConnection(
pub fn handleConnectionError(err: anyerror, operation_name: []const u8) void {
std.debug.print("Failed to {s}: {}\n", .{ operation_name, err });
}
/// Options for job operations
pub const JobOptions = struct {
cpu: u8 = 1,
memory: u8 = 4,
gpu: u8 = 0,
gpu_memory: ?[]const u8 = null,
dry_run: bool = false,
validate: bool = false,
explain: bool = false,
json: bool = false,
force: bool = false,
hypothesis: ?[]const u8 = null,
context: ?[]const u8 = null,
intent: ?[]const u8 = null,
expected_outcome: ?[]const u8 = null,
tags: ?[]const u8 = null,
};
/// Build narrative JSON from options
pub fn buildNarrativeJson(allocator: std.mem.Allocator, options: anytype) !?[]const u8 {
if (options.hypothesis == null and options.context == null and
options.intent == null and options.expected_outcome == null)
{
return null;
}
var buf = try std.ArrayList(u8).initCapacity(allocator, 256);
defer buf.deinit(allocator);
const writer = buf.writer(allocator);
try writer.writeAll("{");
var first = true;
if (options.hypothesis) |h| {
if (!first) try writer.writeAll(",");
try writer.print("\"hypothesis\":\"{s}\"", .{h});
first = false;
}
if (options.context) |c| {
if (!first) try writer.writeAll(",");
try writer.print("\"context\":\"{s}\"", .{c});
first = false;
}
if (options.intent) |i| {
if (!first) try writer.writeAll(",");
try writer.print("\"intent\":\"{s}\"", .{i});
first = false;
}
if (options.expected_outcome) |eo| {
if (!first) try writer.writeAll(",");
try writer.print("\"expected_outcome\":\"{s}\"", .{eo});
first = false;
}
try writer.writeAll("}");
return try buf.toOwnedSlice(allocator);
}
/// Show dry run preview for job
pub fn dryRun(
_allocator: std.mem.Allocator,
job_name: []const u8,
exec_mode: anytype,
options: anytype,
args_str: []const u8,
) !void {
_ = _allocator;
std.debug.print("Dry run for job: {s}\n", .{job_name});
std.debug.print(" Mode: {s}\n", .{@tagName(exec_mode)});
std.debug.print(" CPU: {d}, Memory: {d}GB, GPU: {d}\n", .{ options.cpu, options.memory, options.gpu });
if (args_str.len > 0) {
std.debug.print(" Args: {s}\n", .{args_str});
}
if (options.hypothesis) |h| {
std.debug.print(" Hypothesis: {s}\n", .{h});
}
std.debug.print("\n Action: Would {s}\n", .{
switch (exec_mode) {
.local => "execute locally and mark for sync",
.remote => "queue on remote server",
},
});
}
/// Format next steps message
pub fn formatNextSteps(allocator: std.mem.Allocator, job_name: []const u8, commit_hex: []const u8) ![]const u8 {
var out = try std.ArrayList(u8).initCapacity(allocator, 128);
errdefer out.deinit(allocator);
const writer = out.writer(allocator);
try writer.writeAll("Next steps:\n");
try writer.writeAll("\tml status --watch\n");
try writer.print("\tml cancel {s}\n", .{job_name});
try writer.print("\tml validate {s}\n", .{commit_hex});
return out.toOwnedSlice(allocator);
}

View file

@ -9,6 +9,7 @@ const mode = @import("../mode.zig");
const db = @import("../db.zig");
const manifest_lib = @import("../manifest.zig");
const progress = @import("../utils/progress.zig");
const common = @import("common.zig");
// Use modular queue structure
const queue_mod = @import("queue/mod.zig");
@ -460,7 +461,7 @@ fn queueSingleJob(
defer if (commit_override == null) allocator.free(commit_id);
// Build narrative JSON if any narrative fields are set
const narrative_json = buildNarrativeJson(allocator, options) catch null;
const narrative_json = common.buildNarrativeJson(allocator, options) catch null;
defer if (narrative_json) |j| allocator.free(j);
const config = try Config.load(allocator);
@ -627,7 +628,7 @@ fn queueSingleJob(
} else {
std.debug.print("Job queued: {s}\n", .{job_name});
if (print_next_steps) {
const next_steps = try formatNextSteps(allocator, job_name, commit_hex);
const next_steps = try common.formatNextSteps(allocator, job_name, commit_hex);
defer allocator.free(next_steps);
std.debug.print("{s}\n", .{next_steps});
}
@ -648,7 +649,7 @@ fn queueSingleJob(
std.debug.print("Warning: failed to record job in history ({})\n", .{err});
};
if (print_next_steps) {
const next_steps = try formatNextSteps(allocator, job_name, commit_hex);
const next_steps = try common.formatNextSteps(allocator, job_name, commit_hex);
defer allocator.free(next_steps);
std.debug.print("{s}\n", .{next_steps});
}
@ -707,19 +708,6 @@ fn printUsage() !void {
std.debug.print("\t\t--context 'Following paper XYZ' --tags ablation,lr-scaling\n", .{});
}
pub fn formatNextSteps(allocator: std.mem.Allocator, job_name: []const u8, commit_hex: []const u8) ![]u8 {
var out = try std.ArrayList(u8).initCapacity(allocator, 128);
errdefer out.deinit(allocator);
const writer = out.writer(allocator);
try writer.writeAll("Next steps:\n");
try writer.writeAll("\tml status --watch\n");
try writer.print("\tml cancel {s}\n", .{job_name});
try writer.print("\tml validate {s}\n", .{commit_hex});
return out.toOwnedSlice(allocator);
}
fn explainJob(
allocator: std.mem.Allocator,
job_name: []const u8,
@ -737,7 +725,7 @@ fn explainJob(
}
// Build narrative JSON for display
const narrative_json = buildNarrativeJson(allocator, options) catch null;
const narrative_json = common.buildNarrativeJson(allocator, options) catch null;
defer if (narrative_json) |j| allocator.free(j);
if (options.json) {
@ -865,7 +853,7 @@ fn dryRunJob(
}
// Build narrative JSON for display
const narrative_json = buildNarrativeJson(allocator, options) catch null;
const narrative_json = common.buildNarrativeJson(allocator, options) catch null;
defer if (narrative_json) |j| allocator.free(j);
if (options.json) {
@ -1135,74 +1123,6 @@ fn hexDigit(v: u8) u8 {
return if (v < 10) ('0' + v) else ('a' + (v - 10));
}
// buildNarrativeJson creates a JSON object from narrative fields
fn buildNarrativeJson(allocator: std.mem.Allocator, options: *const QueueOptions) !?[]u8 {
// Check if any narrative field is set
if (options.hypothesis == null and
options.context == null and
options.intent == null and
options.expected_outcome == null and
options.experiment_group == null and
options.tags == null)
{
return null;
}
var buf = try std.ArrayList(u8).initCapacity(allocator, 256);
defer buf.deinit(allocator);
const writer = buf.writer(allocator);
try writer.writeAll("{");
var first = true;
if (options.hypothesis) |h| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"hypothesis\":");
try writeJSONString(writer, h);
}
if (options.context) |c| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"context\":");
try writeJSONString(writer, c);
}
if (options.intent) |i| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"intent\":");
try writeJSONString(writer, i);
}
if (options.expected_outcome) |eo| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"expected_outcome\":");
try writeJSONString(writer, eo);
}
if (options.experiment_group) |eg| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"experiment_group\":");
try writeJSONString(writer, eg);
}
if (options.tags) |t| {
if (!first) try writer.writeAll(",");
first = false;
try writer.writeAll("\"tags\":");
try writeJSONString(writer, t);
}
try writer.writeAll("}");
return try buf.toOwnedSlice(allocator);
}
/// Check if a job with the same commit_id already exists on the server
/// Returns: Optional JSON response from server if duplicate found
fn checkExistingJob(