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.
This commit is contained in:
454
tools/NatsNet.PortTracker/Commands/BatchCommands.cs
Normal file
454
tools/NatsNet.PortTracker/Commands/BatchCommands.cs
Normal file
@@ -0,0 +1,454 @@
|
||||
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)] + "..";
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ rootCommand.Add(ReportCommands.Create(dbOption, schemaOption));
|
||||
rootCommand.Add(PhaseCommands.Create(dbOption, schemaOption));
|
||||
rootCommand.Add(AuditCommand.Create(dbOption));
|
||||
rootCommand.Add(OverrideCommands.Create(dbOption));
|
||||
rootCommand.Add(BatchCommands.Create(dbOption));
|
||||
|
||||
var parseResult = rootCommand.Parse(args);
|
||||
return await parseResult.InvokeAsync();
|
||||
|
||||
Reference in New Issue
Block a user