Files
natsnet/tools/NatsNet.PortTracker/Commands/BatchCommands.cs
Joseph Doherty c5e0416793 Add batch commands to PortTracker CLI and migrate batch tables into porting.db
Implements batch list/show/ready/next/start/complete commands with dependency
validation, migrates 42 implementation batches (2377 features, 2087 tests) from
porting_batches.db into the live tracking database, and documents the batch
workflow in AGENTS.md.
2026-02-27 13:02:43 -05:00

455 lines
16 KiB
C#

using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class BatchCommands
{
public static Command Create(Option<string> dbOption)
{
var batchCommand = new Command("batch", "Manage implementation batches");
batchCommand.Add(CreateList(dbOption));
batchCommand.Add(CreateShow(dbOption));
batchCommand.Add(CreateReady(dbOption));
batchCommand.Add(CreateNext(dbOption));
batchCommand.Add(CreateStart(dbOption));
batchCommand.Add(CreateComplete(dbOption));
return batchCommand;
}
private static Command CreateList(Option<string> dbOption)
{
var statusOpt = new Option<string?>("--status")
{
Description = "Filter by status: pending, in_progress, complete"
};
var cmd = new Command("list", "List all implementation batches");
cmd.Add(statusOpt);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var status = parseResult.GetValue(statusOpt);
using var db = new Database(dbPath);
var sql = "SELECT id, name, feature_count, test_count, status, depends_on FROM implementation_batches";
var parameters = new List<(string, object?)>();
if (status is not null)
{
sql += " WHERE status = @status";
parameters.Add(("@status", status));
}
sql += " ORDER BY priority, id";
var rows = db.Query(sql, parameters.ToArray());
if (rows.Count == 0)
{
Console.WriteLine("No batches found.");
return;
}
Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Status",-12} {"Depends On"}");
Console.WriteLine(new string('-', 100));
foreach (var row in rows)
{
Console.WriteLine(
$"{row["id"],-4} " +
$"{Truncate(row["name"]?.ToString(), 44),-45} " +
$"{row["feature_count"],-5} " +
$"{row["test_count"],-5} " +
$"{row["status"],-12} " +
$"{row["depends_on"] ?? ""}");
}
Console.WriteLine($"\nTotal: {rows.Count} batches");
});
return cmd;
}
private static Command CreateShow(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to show" };
var cmd = new Command("show", "Show batch details with features and tests");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
Console.WriteLine($"Batch {batch["id"]}: {batch["name"]}");
Console.WriteLine($" Description: {batch["description"] ?? "(none)"}");
Console.WriteLine($" Priority: {batch["priority"]}");
Console.WriteLine($" Status: {batch["status"]}");
Console.WriteLine($" Depends On: {batch["depends_on"] ?? "(none)"}");
Console.WriteLine($" Go Files: {batch["go_files"] ?? "(none)"}");
Console.WriteLine($" Features: {batch["feature_count"]}");
Console.WriteLine($" Tests: {batch["test_count"]}");
Console.WriteLine();
// Show features
var features = db.Query(
"""
SELECT f.id, f.name, f.status, f.go_file
FROM batch_features bf
JOIN features f ON f.id = bf.feature_id
WHERE bf.batch_id = @id
ORDER BY f.id
""",
("@id", id));
if (features.Count > 0)
{
Console.WriteLine($"Features ({features.Count}):");
Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}");
Console.WriteLine(" " + new string('-', 90));
foreach (var f in features)
{
Console.WriteLine(
$" {f["id"],-6} " +
$"{f["status"],-10} " +
$"{Truncate(f["name"]?.ToString(), 49),-50} " +
$"{Truncate(f["go_file"]?.ToString(), 30)}");
}
}
Console.WriteLine();
// Show tests
var tests = db.Query(
"""
SELECT t.id, t.name, t.status, t.go_file
FROM batch_tests bt
JOIN unit_tests t ON t.id = bt.test_id
WHERE bt.batch_id = @id
ORDER BY t.id
""",
("@id", id));
if (tests.Count > 0)
{
Console.WriteLine($"Tests ({tests.Count}):");
Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}");
Console.WriteLine(" " + new string('-', 90));
foreach (var t in tests)
{
Console.WriteLine(
$" {t["id"],-6} " +
$"{t["status"],-10} " +
$"{Truncate(t["name"]?.ToString(), 49),-50} " +
$"{Truncate(t["go_file"]?.ToString(), 30)}");
}
}
});
return cmd;
}
private static Command CreateReady(Option<string> dbOption)
{
var cmd = new Command("ready", "List batches ready to start (all dependencies complete)");
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var readyBatches = GetReadyBatches(db);
if (readyBatches.Count == 0)
{
Console.WriteLine("No batches ready.");
return;
}
Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Priority"}");
Console.WriteLine(new string('-', 70));
foreach (var row in readyBatches)
{
Console.WriteLine(
$"{row["id"],-4} " +
$"{Truncate(row["name"]?.ToString(), 44),-45} " +
$"{row["feature_count"],-5} " +
$"{row["test_count"],-5} " +
$"{row["priority"]}");
}
Console.WriteLine($"\n{readyBatches.Count} batches ready");
});
return cmd;
}
private static Command CreateNext(Option<string> dbOption)
{
var cmd = new Command("next", "Show the next recommended batch (lowest priority ready)");
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var readyBatches = GetReadyBatches(db);
if (readyBatches.Count == 0)
{
Console.WriteLine("No batches ready.");
return;
}
var next = readyBatches[0]; // Already sorted by priority
Console.WriteLine($"Next batch: #{next["id"]} — {next["name"]}");
Console.WriteLine($" Priority: {next["priority"]}");
Console.WriteLine($" Features: {next["feature_count"]}");
Console.WriteLine($" Tests: {next["test_count"]}");
Console.WriteLine($" Depends On: {next["depends_on"] ?? "(none)"}");
Console.WriteLine($"\nRun: batch start {next["id"]}");
});
return cmd;
}
private static Command CreateStart(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to start" };
var cmd = new Command("start", "Mark batch as in-progress (validates dependencies)");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
var status = batch["status"]?.ToString();
if (status == "complete")
{
Console.WriteLine($"Batch {id} is already complete.");
return;
}
if (status == "in_progress")
{
Console.WriteLine($"Batch {id} is already in progress.");
return;
}
// Check dependencies
if (!AreDependenciesMet(db, batch))
{
var deps = batch["depends_on"]?.ToString() ?? "";
Console.WriteLine($"Cannot start batch {id}: dependencies not met.");
Console.WriteLine($" Depends on: {deps}");
PrintDependencyStatus(db, deps);
return;
}
db.Execute(
"UPDATE implementation_batches SET status = 'in_progress' WHERE id = @id",
("@id", id));
Console.WriteLine($"Batch {id} started: {batch["name"]}");
Console.WriteLine($" Features: {batch["feature_count"]}, Tests: {batch["test_count"]}");
Console.WriteLine($"\nRun: batch show {id} — to see all items");
});
return cmd;
}
private static Command CreateComplete(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to complete" };
var cmd = new Command("complete", "Mark batch as complete (validates all items done)");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
if (batch["status"]?.ToString() == "complete")
{
Console.WriteLine($"Batch {id} is already complete.");
return;
}
// Check all features are done
var incompleteFeatures = db.Query(
"""
SELECT f.id, f.name, f.status
FROM batch_features bf
JOIN features f ON f.id = bf.feature_id
WHERE bf.batch_id = @id AND f.status NOT IN ('verified', 'complete', 'n_a')
ORDER BY f.id
""",
("@id", id));
// Check all tests are done
var incompleteTests = db.Query(
"""
SELECT t.id, t.name, t.status
FROM batch_tests bt
JOIN unit_tests t ON t.id = bt.test_id
WHERE bt.batch_id = @id AND t.status NOT IN ('verified', 'complete', 'n_a')
ORDER BY t.id
""",
("@id", id));
if (incompleteFeatures.Count > 0 || incompleteTests.Count > 0)
{
Console.WriteLine($"Cannot complete batch {id}: items remain.");
if (incompleteFeatures.Count > 0)
{
Console.WriteLine($"\n Incomplete features ({incompleteFeatures.Count}):");
foreach (var f in incompleteFeatures.Take(20))
{
Console.WriteLine($" {f["id"],-6} {f["status"],-10} {Truncate(f["name"]?.ToString(), 50)}");
}
if (incompleteFeatures.Count > 20)
Console.WriteLine($" ... and {incompleteFeatures.Count - 20} more");
}
if (incompleteTests.Count > 0)
{
Console.WriteLine($"\n Incomplete tests ({incompleteTests.Count}):");
foreach (var t in incompleteTests.Take(20))
{
Console.WriteLine($" {t["id"],-6} {t["status"],-10} {Truncate(t["name"]?.ToString(), 50)}");
}
if (incompleteTests.Count > 20)
Console.WriteLine($" ... and {incompleteTests.Count - 20} more");
}
return;
}
db.Execute(
"UPDATE implementation_batches SET status = 'complete' WHERE id = @id",
("@id", id));
Console.WriteLine($"Batch {id} completed: {batch["name"]}");
});
return cmd;
}
private static List<Dictionary<string, object?>> GetReadyBatches(Database db)
{
// Get all pending batches
var pending = db.Query(
"SELECT * FROM implementation_batches WHERE status = 'pending' ORDER BY priority, id");
// Get statuses of all batches for dependency checking
var allStatuses = db.Query("SELECT id, status FROM implementation_batches");
var statusMap = new Dictionary<long, string>();
foreach (var row in allStatuses)
{
statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending";
}
var ready = new List<Dictionary<string, object?>>();
foreach (var batch in pending)
{
if (AreDependenciesMet(statusMap, batch["depends_on"]?.ToString()))
ready.Add(batch);
}
return ready;
}
private static bool AreDependenciesMet(Database db, Dictionary<string, object?> batch)
{
var depsStr = batch["depends_on"]?.ToString();
if (string.IsNullOrWhiteSpace(depsStr))
return true;
var allStatuses = db.Query("SELECT id, status FROM implementation_batches");
var statusMap = new Dictionary<long, string>();
foreach (var row in allStatuses)
{
statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending";
}
return AreDependenciesMet(statusMap, depsStr);
}
private static bool AreDependenciesMet(Dictionary<long, string> statusMap, string? depsStr)
{
if (string.IsNullOrWhiteSpace(depsStr))
return true;
var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var depIdStr in depIds)
{
if (long.TryParse(depIdStr, out var depId))
{
if (!statusMap.TryGetValue(depId, out var depStatus) || depStatus != "complete")
return false;
}
}
return true;
}
private static void PrintDependencyStatus(Database db, string depsStr)
{
if (string.IsNullOrWhiteSpace(depsStr)) return;
var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var depIdStr in depIds)
{
if (int.TryParse(depIdStr, out var depId))
{
var rows = db.Query(
"SELECT id, name, status FROM implementation_batches WHERE id = @id",
("@id", depId));
if (rows.Count > 0)
{
var r = rows[0];
Console.WriteLine($" Batch {r["id"]}: {r["status"]} — {r["name"]}");
}
}
}
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}