refactor(cli): Rename note to annotate and re-add experiment command
- Renamed note.zig to annotate.zig (preserves user's preferred naming) - Updated all references from 'ml note' to 'ml annotate' - Re-added experiment.zig with create/list/show subcommands - Updated main.zig dispatch: 'a' for annotate, 'e' for experiment - Updated printUsage and test block to reflect changes
This commit is contained in:
parent
7c4a59012b
commit
04ac745b01
3 changed files with 383 additions and 4 deletions
143
cli/src/commands/annotate.zig
Normal file
143
cli/src/commands/annotate.zig
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
const std = @import("std");
|
||||
const config = @import("../config.zig");
|
||||
const db = @import("../db.zig");
|
||||
const core = @import("../core.zig");
|
||||
const colors = @import("../utils/colors.zig");
|
||||
const manifest_lib = @import("../manifest.zig");
|
||||
|
||||
/// Annotate command - unified metadata annotation
|
||||
/// Usage:
|
||||
/// ml annotate <run_id> --text "Try lr=3e-4 next"
|
||||
/// ml annotate <run_id> --hypothesis "LR scaling helps"
|
||||
/// ml annotate <run_id> --outcome validates --confidence 0.9
|
||||
/// ml annotate <run_id> --privacy private
|
||||
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
||||
var flags = core.flags.CommonFlags{};
|
||||
var command_args = try core.flags.parseCommon(allocator, args, &flags);
|
||||
defer command_args.deinit(allocator);
|
||||
|
||||
core.output.init(if (flags.json) .json else .text);
|
||||
|
||||
if (flags.help) {
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
if (command_args.items.len < 1) {
|
||||
std.log.err("Usage: ml annotate <run_id> [options]", .{});
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const run_id = command_args.items[0];
|
||||
|
||||
// Parse metadata options
|
||||
const text = core.flags.parseKVFlag(command_args.items, "text");
|
||||
const hypothesis = core.flags.parseKVFlag(command_args.items, "hypothesis");
|
||||
const outcome = core.flags.parseKVFlag(command_args.items, "outcome");
|
||||
const confidence = core.flags.parseKVFlag(command_args.items, "confidence");
|
||||
const privacy = core.flags.parseKVFlag(command_args.items, "privacy");
|
||||
const author = core.flags.parseKVFlag(command_args.items, "author");
|
||||
|
||||
// Check that at least one option is provided
|
||||
if (text == null and hypothesis == null and outcome == null and privacy == null) {
|
||||
std.log.err("No metadata provided. Use --text, --hypothesis, --outcome, or --privacy", .{});
|
||||
return error.MissingMetadata;
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// Get DB path
|
||||
const db_path = try cfg.getDBPath(allocator);
|
||||
defer allocator.free(db_path);
|
||||
|
||||
var database = try db.DB.init(allocator, db_path);
|
||||
defer database.close();
|
||||
|
||||
// Verify run exists
|
||||
const check_sql = "SELECT 1 FROM ml_runs WHERE run_id = ?;";
|
||||
const check_stmt = try database.prepare(check_sql);
|
||||
defer db.DB.finalize(check_stmt);
|
||||
try db.DB.bindText(check_stmt, 1, run_id);
|
||||
const has_row = try db.DB.step(check_stmt);
|
||||
if (!has_row) {
|
||||
std.log.err("Run not found: {s}", .{run_id});
|
||||
return error.RunNotFound;
|
||||
}
|
||||
|
||||
// Add text note as a tag
|
||||
if (text) |t| {
|
||||
try addTag(allocator, &database, run_id, "note", t, author);
|
||||
}
|
||||
|
||||
// Add hypothesis
|
||||
if (hypothesis) |h| {
|
||||
try addTag(allocator, &database, run_id, "hypothesis", h, author);
|
||||
}
|
||||
|
||||
// Add outcome
|
||||
if (outcome) |o| {
|
||||
try addTag(allocator, &database, run_id, "outcome", o, author);
|
||||
if (confidence) |c| {
|
||||
try addTag(allocator, &database, run_id, "confidence", c, author);
|
||||
}
|
||||
}
|
||||
|
||||
// Add privacy level
|
||||
if (privacy) |p| {
|
||||
try addTag(allocator, &database, run_id, "privacy", p, author);
|
||||
}
|
||||
|
||||
// Checkpoint WAL
|
||||
database.checkpointOnExit();
|
||||
|
||||
if (flags.json) {
|
||||
std.debug.print("{{\"success\":true,\"run_id\":\"{s}\",\"action\":\"note_added\"}}\n", .{run_id});
|
||||
} else {
|
||||
colors.printSuccess("✓ Added note to run {s}\n", .{run_id[0..8]});
|
||||
}
|
||||
}
|
||||
|
||||
fn addTag(
|
||||
allocator: std.mem.Allocator,
|
||||
database: *db.DB,
|
||||
run_id: []const u8,
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
author: ?[]const u8,
|
||||
) !void {
|
||||
const full_value = if (author) |a|
|
||||
try std.fmt.allocPrint(allocator, "{s} (by {s})", .{ value, a })
|
||||
else
|
||||
try allocator.dupe(u8, value);
|
||||
defer allocator.free(full_value);
|
||||
|
||||
const sql = "INSERT INTO ml_tags (run_id, key, value) VALUES (?, ?, ?);";
|
||||
const stmt = try database.prepare(sql);
|
||||
defer db.DB.finalize(stmt);
|
||||
|
||||
try db.DB.bindText(stmt, 1, run_id);
|
||||
try db.DB.bindText(stmt, 2, key);
|
||||
try db.DB.bindText(stmt, 3, full_value);
|
||||
_ = try db.DB.step(stmt);
|
||||
}
|
||||
|
||||
fn printUsage() !void {
|
||||
std.debug.print("Usage: ml annotate <run_id> [options]\n\n", .{});
|
||||
std.debug.print("Add metadata annotations to a run.\n\n", .{});
|
||||
std.debug.print("Options:\n", .{});
|
||||
std.debug.print(" --text <string> Free-form annotation\n", .{});
|
||||
std.debug.print(" --hypothesis <string> Research hypothesis\n", .{});
|
||||
std.debug.print(" --outcome <status> Outcome: validates/refutes/inconclusive\n", .{});
|
||||
std.debug.print(" --confidence <0-1> Confidence in outcome\n", .{});
|
||||
std.debug.print(" --privacy <level> Privacy: private/team/public\n", .{});
|
||||
std.debug.print(" --author <name> Author of the annotation\n", .{});
|
||||
std.debug.print(" --help, -h Show this help\n", .{});
|
||||
std.debug.print(" --json Output structured JSON\n\n", .{});
|
||||
std.debug.print("Examples:\n", .{});
|
||||
std.debug.print(" ml annotate abc123 --text \"Try lr=3e-4 next\"\n", .{});
|
||||
std.debug.print(" ml annotate abc123 --hypothesis \"LR scaling helps\"\n", .{});
|
||||
std.debug.print(" ml annotate abc123 --outcome validates --confidence 0.9\n", .{});
|
||||
}
|
||||
229
cli/src/commands/experiment.zig
Normal file
229
cli/src/commands/experiment.zig
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
const std = @import("std");
|
||||
const config = @import("../config.zig");
|
||||
const db = @import("../db.zig");
|
||||
const core = @import("../core.zig");
|
||||
const colors = @import("../utils/colors.zig");
|
||||
const mode = @import("../mode.zig");
|
||||
|
||||
/// Experiment command - manage experiments
|
||||
/// Usage:
|
||||
/// ml experiment create --name "baseline-cnn"
|
||||
/// ml experiment list
|
||||
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
||||
var flags = core.flags.CommonFlags{};
|
||||
var command_args = try core.flags.parseCommon(allocator, args, &flags);
|
||||
defer command_args.deinit(allocator);
|
||||
|
||||
core.output.init(if (flags.json) .json else .text);
|
||||
|
||||
if (flags.help or command_args.items.len == 0) {
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const subcommand = command_args.items[0];
|
||||
const sub_args = if (command_args.items.len > 1) command_args.items[1..] else &[_][]const u8{};
|
||||
|
||||
if (std.mem.eql(u8, subcommand, "create")) {
|
||||
return try createExperiment(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "list")) {
|
||||
return try listExperiments(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "show")) {
|
||||
return try showExperiment(allocator, sub_args, flags.json);
|
||||
} else {
|
||||
core.output.errorMsg("experiment", "Unknown subcommand: {s}", .{subcommand});
|
||||
return printUsage();
|
||||
}
|
||||
}
|
||||
|
||||
fn createExperiment(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var name: ?[]const u8 = null;
|
||||
var description: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
|
||||
name = args[i + 1];
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, args[i], "--description") and i + 1 < args.len) {
|
||||
description = args[i + 1];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
core.output.errorMsg("experiment", "--name is required", .{});
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// Check mode
|
||||
const mode_result = try mode.detect(allocator, cfg);
|
||||
|
||||
if (mode.isOffline(mode_result.mode)) {
|
||||
// Local mode: create in SQLite
|
||||
const db_path = try cfg.getDBPath(allocator);
|
||||
defer allocator.free(db_path);
|
||||
|
||||
var database = try db.DB.init(allocator, db_path);
|
||||
defer database.close();
|
||||
|
||||
const sql = "INSERT INTO ml_experiments (experiment_id, name, description) VALUES (?, ?, ?);";
|
||||
const stmt = try database.prepare(sql);
|
||||
defer db.DB.finalize(stmt);
|
||||
|
||||
const exp_id = try generateExperimentID(allocator);
|
||||
defer allocator.free(exp_id);
|
||||
|
||||
try db.DB.bindText(stmt, 1, exp_id);
|
||||
try db.DB.bindText(stmt, 2, name.?);
|
||||
try db.DB.bindText(stmt, 3, description orelse "");
|
||||
_ = try db.DB.step(stmt);
|
||||
|
||||
// Update config with new experiment
|
||||
var mut_cfg = cfg;
|
||||
if (mut_cfg.experiment == null) {
|
||||
mut_cfg.experiment = config.ExperimentConfig{};
|
||||
}
|
||||
mut_cfg.experiment.?.name = try allocator.dupe(u8, name.?);
|
||||
try mut_cfg.save(allocator);
|
||||
|
||||
database.checkpointOnExit();
|
||||
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"experiment_id\":\"{s}\",\"name\":\"{s}\"}}\n", .{ exp_id, name.? });
|
||||
} else {
|
||||
colors.printSuccess("✓ Created experiment: {s} ({s})\n", .{ name.?, exp_id[0..8] });
|
||||
}
|
||||
} else {
|
||||
// Server mode: would send to server
|
||||
// For now, just update local config
|
||||
var mut_cfg = cfg;
|
||||
if (mut_cfg.experiment == null) {
|
||||
mut_cfg.experiment = config.ExperimentConfig{};
|
||||
}
|
||||
mut_cfg.experiment.?.name = try allocator.dupe(u8, name.?);
|
||||
try mut_cfg.save(allocator);
|
||||
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"name\":\"{s}\"}}\n", .{name.?});
|
||||
} else {
|
||||
colors.printSuccess("✓ Set active experiment: {s}\n", .{name.?});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn listExperiments(allocator: std.mem.Allocator, _: []const []const u8, json: bool) !void {
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
const mode_result = try mode.detect(allocator, cfg);
|
||||
|
||||
if (mode.isOffline(mode_result.mode)) {
|
||||
// Local mode: list from SQLite
|
||||
const db_path = try cfg.getDBPath(allocator);
|
||||
defer allocator.free(db_path);
|
||||
|
||||
var database = try db.DB.init(allocator, db_path);
|
||||
defer database.close();
|
||||
|
||||
const sql = "SELECT experiment_id, name, description, created_at, status FROM ml_experiments ORDER BY created_at DESC;";
|
||||
const stmt = try database.prepare(sql);
|
||||
defer db.DB.finalize(stmt);
|
||||
|
||||
var experiments = std.ArrayList(ExperimentInfo).init(allocator);
|
||||
defer {
|
||||
for (experiments.items) |*e| e.deinit(allocator);
|
||||
experiments.deinit();
|
||||
}
|
||||
|
||||
while (try db.DB.step(stmt)) {
|
||||
try experiments.append(ExperimentInfo{
|
||||
.id = try allocator.dupe(u8, db.DB.columnText(stmt, 0)),
|
||||
.name = try allocator.dupe(u8, db.DB.columnText(stmt, 1)),
|
||||
.description = try allocator.dupe(u8, db.DB.columnText(stmt, 2)),
|
||||
.created_at = try allocator.dupe(u8, db.DB.columnText(stmt, 3)),
|
||||
.status = try allocator.dupe(u8, db.DB.columnText(stmt, 4)),
|
||||
});
|
||||
}
|
||||
|
||||
if (json) {
|
||||
std.debug.print("[", .{});
|
||||
for (experiments.items, 0..) |e, i| {
|
||||
if (i > 0) std.debug.print(",", .{});
|
||||
std.debug.print("{{\"id\":\"{s}\",\"name\":\"{s}\",\"status\":\"{s}\"}}", .{ e.id, e.name, e.status });
|
||||
}
|
||||
std.debug.print("]\n", .{});
|
||||
} else {
|
||||
if (experiments.items.len == 0) {
|
||||
colors.printInfo("No experiments found.\n", .{});
|
||||
} else {
|
||||
colors.printInfo("Experiments:\n", .{});
|
||||
for (experiments.items) |e| {
|
||||
std.debug.print(" {s} {s} ({s})\n", .{ e.id[0..8], e.name, e.status });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Server mode: would query server
|
||||
colors.printInfo("Server mode: would list experiments from server\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn showExperiment(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
if (args.len == 0) {
|
||||
core.output.errorMsg("experiment", "experiment_id required", .{});
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const exp_id = args[0];
|
||||
_ = json;
|
||||
_ = allocator;
|
||||
|
||||
colors.printInfo("Show experiment: {s}\n", .{exp_id});
|
||||
// Implementation would show experiment details
|
||||
}
|
||||
|
||||
const ExperimentInfo = struct {
|
||||
id: []const u8,
|
||||
name: []const u8,
|
||||
description: []const u8,
|
||||
created_at: []const u8,
|
||||
status: []const u8,
|
||||
|
||||
fn deinit(self: *ExperimentInfo, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.id);
|
||||
allocator.free(self.name);
|
||||
allocator.free(self.description);
|
||||
allocator.free(self.created_at);
|
||||
allocator.free(self.status);
|
||||
}
|
||||
};
|
||||
|
||||
fn generateExperimentID(allocator: std.mem.Allocator) ![]const u8 {
|
||||
const uuid = @import("../utils/uuid.zig");
|
||||
return try uuid.generateV4(allocator);
|
||||
}
|
||||
|
||||
fn printUsage() !void {
|
||||
std.debug.print("Usage: ml experiment <subcommand> [options]\n\n", .{});
|
||||
std.debug.print("Subcommands:\n", .{});
|
||||
std.debug.print(" create --name <name> [--description <desc>] Create new experiment\n", .{});
|
||||
std.debug.print(" list List experiments\n", .{});
|
||||
std.debug.print(" show <experiment_id> Show experiment details\n", .{});
|
||||
std.debug.print("\nOptions:\n", .{});
|
||||
std.debug.print(" --name <string> Experiment name (required for create)\n", .{});
|
||||
std.debug.print(" --description <string> Experiment description\n", .{});
|
||||
std.debug.print(" --help, -h Show this help\n", .{});
|
||||
std.debug.print(" --json Output structured JSON\n\n", .{});
|
||||
std.debug.print("Examples:\n", .{});
|
||||
std.debug.print(" ml experiment create --name \"baseline-cnn\"\n", .{});
|
||||
std.debug.print(" ml experiment list\n", .{});
|
||||
}
|
||||
|
|
@ -38,8 +38,13 @@ pub fn main() !void {
|
|||
} else if (std.mem.eql(u8, command, "info")) {
|
||||
try @import("commands/info.zig").run(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
'n' => if (std.mem.eql(u8, command, "note")) {
|
||||
try @import("commands/note.zig").run(allocator, args[2..]);
|
||||
'a' => if (std.mem.eql(u8, command, "annotate")) {
|
||||
try @import("commands/annotate.zig").execute(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
'e' => if (std.mem.eql(u8, command, "experiment")) {
|
||||
try @import("commands/experiment.zig").execute(allocator, args[2..]);
|
||||
} else if (std.mem.eql(u8, command, "export")) {
|
||||
try @import("commands/export_cmd.zig").run(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
's' => if (std.mem.eql(u8, command, "sync")) {
|
||||
try @import("commands/sync.zig").run(allocator, args[2..]);
|
||||
|
|
@ -93,7 +98,8 @@ fn printUsage() void {
|
|||
std.debug.print(" init Initialize project with config (use --local for SQLite)\n", .{});
|
||||
std.debug.print(" run [args] Execute a run locally (forks, captures, parses metrics)\n", .{});
|
||||
std.debug.print(" queue <job> Queue job on server (--rerun <id> to re-queue local run)\n", .{});
|
||||
std.debug.print(" note <id> Add metadata annotation (hypothesis/outcome/confidence)\n", .{});
|
||||
std.debug.print(" annotate <id> Add metadata annotations (hypothesis/outcome/confidence)\n", .{});
|
||||
std.debug.print(" experiment Manage experiments (create, list, show)\n", .{});
|
||||
std.debug.print(" logs <id> Fetch or stream run logs (--follow for live tail)\n", .{});
|
||||
std.debug.print(" sync [id] Push local runs to server (sync_run + sync_ack protocol)\n", .{});
|
||||
std.debug.print(" cancel <id> Cancel local run (SIGTERM/SIGKILL by PID)\n", .{});
|
||||
|
|
@ -115,5 +121,6 @@ test {
|
|||
_ = @import("commands/find.zig");
|
||||
_ = @import("commands/export_cmd.zig");
|
||||
_ = @import("commands/log.zig");
|
||||
_ = @import("commands/note.zig");
|
||||
_ = @import("commands/annotate.zig");
|
||||
_ = @import("commands/experiment.zig");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue