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.
455 lines
16 KiB
C#
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)] + "..";
|
|
}
|
|
}
|