feat(porttracker): add all remaining commands (feature, test, library, dependency, report, phase)

This commit is contained in:
Joseph Doherty
2026-02-26 06:17:43 -05:00
parent c31bf6050d
commit cecbb49653
8 changed files with 941 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class DependencyCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var depCommand = new Command("dependency", "Manage dependencies");
// show
var showType = new Argument<string>("type") { Description = "Item type (module, feature, unit_test)" };
var showId = new Argument<int>("id") { Description = "Item ID" };
var showCmd = new Command("show", "Show dependencies for an item");
showCmd.Add(showType);
showCmd.Add(showId);
showCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var type = parseResult.GetValue(showType)!;
var id = parseResult.GetValue(showId);
using var db = new Database(dbPath);
var deps = db.Query(
"SELECT target_type, target_id, dependency_kind FROM dependencies WHERE source_type = @type AND source_id = @id",
("@type", type), ("@id", id));
Console.WriteLine($"Dependencies of {type} #{id} ({deps.Count}):");
foreach (var d in deps)
Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} [{d["dependency_kind"]}]");
var rdeps = db.Query(
"SELECT source_type, source_id, dependency_kind FROM dependencies WHERE target_type = @type AND target_id = @id",
("@type", type), ("@id", id));
Console.WriteLine($"\nReverse dependencies (depends on {type} #{id}) ({rdeps.Count}):");
foreach (var d in rdeps)
Console.WriteLine($" <- {d["source_type"]} #{d["source_id"]} [{d["dependency_kind"]}]");
});
// blocked
var blockedCmd = new Command("blocked", "Show items blocked by unported dependencies");
blockedCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var sql = @"
SELECT 'module' as item_type, m.id, m.name, m.status, d.target_type, d.target_id, d.dependency_kind
FROM modules m
JOIN dependencies d ON d.source_type = 'module' AND d.source_id = m.id
WHERE m.status NOT IN ('complete', 'verified', 'n_a')
AND (
(d.target_type = 'module' AND d.target_id IN (SELECT id FROM modules WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'feature' AND d.target_id IN (SELECT id FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'unit_test' AND d.target_id IN (SELECT id FROM unit_tests WHERE status NOT IN ('complete', 'verified', 'n_a')))
)
UNION ALL
SELECT 'feature' as item_type, f.id, f.name, f.status, d.target_type, d.target_id, d.dependency_kind
FROM features f
JOIN dependencies d ON d.source_type = 'feature' AND d.source_id = f.id
WHERE f.status NOT IN ('complete', 'verified', 'n_a')
AND (
(d.target_type = 'module' AND d.target_id IN (SELECT id FROM modules WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'feature' AND d.target_id IN (SELECT id FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'unit_test' AND d.target_id IN (SELECT id FROM unit_tests WHERE status NOT IN ('complete', 'verified', 'n_a')))
)
ORDER BY 1, 2";
var rows = db.Query(sql);
if (rows.Count == 0)
{
Console.WriteLine("No blocked items found.");
return;
}
Console.WriteLine($"{"Type",-10} {"ID",-5} {"Name",-30} {"Status",-15} {"Blocked By",-15} {"Dep ID",-8} {"Kind",-10}");
Console.WriteLine(new string('-', 93));
foreach (var row in rows)
{
Console.WriteLine($"{row["item_type"],-10} {row["id"],-5} {Truncate(row["name"]?.ToString(), 29),-30} {row["status"],-15} {row["target_type"],-15} {row["target_id"],-8} {row["dependency_kind"],-10}");
}
Console.WriteLine($"\n{rows.Count} blocking relationships found.");
});
// ready
var readyCmd = new Command("ready", "Show items ready to port (no unported dependencies)");
readyCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var sql = @"
SELECT 'module' as item_type, m.id, m.name, m.status
FROM modules m
WHERE m.status IN ('not_started', 'stub')
AND NOT EXISTS (
SELECT 1 FROM dependencies d
WHERE d.source_type = 'module' AND d.source_id = m.id
AND (
(d.target_type = 'module' AND d.target_id IN (SELECT id FROM modules WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'feature' AND d.target_id IN (SELECT id FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'unit_test' AND d.target_id IN (SELECT id FROM unit_tests WHERE status NOT IN ('complete', 'verified', 'n_a')))
)
)
UNION ALL
SELECT 'feature' as item_type, f.id, f.name, f.status
FROM features f
WHERE f.status IN ('not_started', 'stub')
AND NOT EXISTS (
SELECT 1 FROM dependencies d
WHERE d.source_type = 'feature' AND d.source_id = f.id
AND (
(d.target_type = 'module' AND d.target_id IN (SELECT id FROM modules WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'feature' AND d.target_id IN (SELECT id FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')))
OR (d.target_type = 'unit_test' AND d.target_id IN (SELECT id FROM unit_tests WHERE status NOT IN ('complete', 'verified', 'n_a')))
)
)
ORDER BY 1, 2";
var rows = db.Query(sql);
if (rows.Count == 0)
{
Console.WriteLine("No items are ready to port (all items either have unported deps or are already done).");
return;
}
Console.WriteLine($"{"Type",-10} {"ID",-5} {"Name",-40} {"Status",-15}");
Console.WriteLine(new string('-', 70));
foreach (var row in rows)
{
Console.WriteLine($"{row["item_type"],-10} {row["id"],-5} {Truncate(row["name"]?.ToString(), 39),-40} {row["status"],-15}");
}
Console.WriteLine($"\n{rows.Count} items ready to port.");
});
depCommand.Add(showCmd);
depCommand.Add(blockedCmd);
depCommand.Add(readyCmd);
return depCommand;
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}

View File

@@ -0,0 +1,183 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class FeatureCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var featureCommand = new Command("feature", "Manage features");
// list
var listModule = new Option<int?>("--module") { Description = "Filter by module ID" };
var listStatus = new Option<string?>("--status") { Description = "Filter by status" };
var listCmd = new Command("list", "List features");
listCmd.Add(listModule);
listCmd.Add(listStatus);
listCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var moduleId = parseResult.GetValue(listModule);
var status = parseResult.GetValue(listStatus);
using var db = new Database(dbPath);
var sql = "SELECT f.id, f.name, f.status, f.module_id, m.name as module_name, f.go_method, f.dotnet_method FROM features f LEFT JOIN modules m ON f.module_id = m.id";
var parameters = new List<(string, object?)>();
var clauses = new List<string>();
if (moduleId is not null)
{
clauses.Add("f.module_id = @module");
parameters.Add(("@module", moduleId));
}
if (status is not null)
{
clauses.Add("f.status = @status");
parameters.Add(("@status", status));
}
if (clauses.Count > 0)
sql += " WHERE " + string.Join(" AND ", clauses);
sql += " ORDER BY m.name, f.name";
var rows = db.Query(sql, parameters.ToArray());
Console.WriteLine($"{"ID",-5} {"Name",-30} {"Status",-15} {"Module",-20} {"Go Method",-25} {"DotNet Method",-25}");
Console.WriteLine(new string('-', 120));
foreach (var row in rows)
{
Console.WriteLine($"{row["id"],-5} {Truncate(row["name"]?.ToString(), 29),-30} {row["status"],-15} {Truncate(row["module_name"]?.ToString(), 19),-20} {Truncate(row["go_method"]?.ToString(), 24),-25} {Truncate(row["dotnet_method"]?.ToString(), 24),-25}");
}
Console.WriteLine($"\nTotal: {rows.Count} features");
});
// show
var showId = new Argument<int>("id") { Description = "Feature ID" };
var showCmd = new Command("show", "Show feature details");
showCmd.Add(showId);
showCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(showId);
using var db = new Database(dbPath);
var features = db.Query(
"SELECT f.*, m.name as module_name FROM features f LEFT JOIN modules m ON f.module_id = m.id WHERE f.id = @id",
("@id", id));
if (features.Count == 0)
{
Console.WriteLine($"Feature {id} not found.");
return;
}
var f = features[0];
Console.WriteLine($"Feature #{f["id"]}: {f["name"]}");
Console.WriteLine($" Module: #{f["module_id"]} ({f["module_name"]})");
Console.WriteLine($" Status: {f["status"]}");
Console.WriteLine($" Go File: {f["go_file"]}");
Console.WriteLine($" Go Class: {f["go_class"]}");
Console.WriteLine($" Go Method: {f["go_method"]}");
Console.WriteLine($" Go Line: {f["go_line_number"]}");
Console.WriteLine($" Go LOC: {f["go_line_count"]}");
Console.WriteLine($" .NET: {f["dotnet_project"]} / {f["dotnet_class"]} / {f["dotnet_method"]}");
Console.WriteLine($" Notes: {f["notes"]}");
var deps = db.Query(
"SELECT d.target_type, d.target_id, d.dependency_kind FROM dependencies d WHERE d.source_type = 'feature' AND d.source_id = @id",
("@id", id));
Console.WriteLine($"\n Dependencies ({deps.Count}):");
foreach (var d in deps)
Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} [{d["dependency_kind"]}]");
var rdeps = db.Query(
"SELECT d.source_type, d.source_id, d.dependency_kind FROM dependencies d WHERE d.target_type = 'feature' AND d.target_id = @id",
("@id", id));
Console.WriteLine($"\n Reverse Dependencies ({rdeps.Count}):");
foreach (var d in rdeps)
Console.WriteLine($" <- {d["source_type"]} #{d["source_id"]} [{d["dependency_kind"]}]");
});
// update
var updateId = new Argument<int>("id") { Description = "Feature ID (use 0 with --all-in-module)" };
var updateStatus = new Option<string>("--status") { Description = "New status", Required = true };
var updateAllInModule = new Option<int?>("--all-in-module") { Description = "Update all features in this module ID" };
var updateCmd = new Command("update", "Update feature status");
updateCmd.Add(updateId);
updateCmd.Add(updateStatus);
updateCmd.Add(updateAllInModule);
updateCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(updateId);
var status = parseResult.GetValue(updateStatus)!;
var allInModule = parseResult.GetValue(updateAllInModule);
using var db = new Database(dbPath);
if (allInModule is not null)
{
var affected = db.Execute(
"UPDATE features SET status = @status WHERE module_id = @module",
("@status", status), ("@module", allInModule));
Console.WriteLine($"Updated {affected} features in module {allInModule} to '{status}'.");
}
else
{
var affected = db.Execute(
"UPDATE features SET status = @status WHERE id = @id",
("@status", status), ("@id", id));
Console.WriteLine(affected > 0 ? $"Feature {id} updated to '{status}'." : $"Feature {id} not found.");
}
});
// map
var mapId = new Argument<int>("id") { Description = "Feature ID" };
var mapProject = new Option<string?>("--project") { Description = "Target .NET project" };
var mapClass = new Option<string?>("--class") { Description = "Target .NET class" };
var mapMethod = new Option<string?>("--method") { Description = "Target .NET method" };
var mapCmd = new Command("map", "Map feature to .NET method");
mapCmd.Add(mapId);
mapCmd.Add(mapProject);
mapCmd.Add(mapClass);
mapCmd.Add(mapMethod);
mapCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(mapId);
var project = parseResult.GetValue(mapProject);
var cls = parseResult.GetValue(mapClass);
var method = parseResult.GetValue(mapMethod);
using var db = new Database(dbPath);
var affected = db.Execute(
"UPDATE features SET dotnet_project = COALESCE(@project, dotnet_project), dotnet_class = COALESCE(@cls, dotnet_class), dotnet_method = COALESCE(@method, dotnet_method) WHERE id = @id",
("@project", project), ("@cls", cls), ("@method", method), ("@id", id));
Console.WriteLine(affected > 0 ? $"Feature {id} mapped." : $"Feature {id} not found.");
});
// set-na
var naId = new Argument<int>("id") { Description = "Feature ID" };
var naReason = new Option<string>("--reason") { Description = "Reason for N/A", Required = true };
var naCmd = new Command("set-na", "Mark feature as N/A");
naCmd.Add(naId);
naCmd.Add(naReason);
naCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(naId);
var reason = parseResult.GetValue(naReason)!;
using var db = new Database(dbPath);
var affected = db.Execute(
"UPDATE features SET status = 'n_a', notes = @reason WHERE id = @id",
("@reason", reason), ("@id", id));
Console.WriteLine(affected > 0 ? $"Feature {id} set to N/A: {reason}" : $"Feature {id} not found.");
});
featureCommand.Add(listCmd);
featureCommand.Add(showCmd);
featureCommand.Add(updateCmd);
featureCommand.Add(mapCmd);
featureCommand.Add(naCmd);
return featureCommand;
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}

View File

@@ -0,0 +1,98 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class LibraryCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var libraryCommand = new Command("library", "Manage library mappings");
// list
var listStatus = new Option<string?>("--status") { Description = "Filter by status" };
var listCmd = new Command("list", "List library mappings");
listCmd.Add(listStatus);
listCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var status = parseResult.GetValue(listStatus);
using var db = new Database(dbPath);
var sql = "SELECT id, go_import_path, go_library_name, dotnet_package, dotnet_namespace, status FROM library_mappings";
var parameters = new List<(string, object?)>();
if (status is not null)
{
sql += " WHERE status = @status";
parameters.Add(("@status", status));
}
sql += " ORDER BY go_import_path";
var rows = db.Query(sql, parameters.ToArray());
Console.WriteLine($"{"ID",-5} {"Go Import Path",-40} {"Go Library",-20} {"DotNet Package",-25} {"DotNet Namespace",-25} {"Status",-12}");
Console.WriteLine(new string('-', 127));
foreach (var row in rows)
{
Console.WriteLine($"{row["id"],-5} {Truncate(row["go_import_path"]?.ToString(), 39),-40} {Truncate(row["go_library_name"]?.ToString(), 19),-20} {Truncate(row["dotnet_package"]?.ToString(), 24),-25} {Truncate(row["dotnet_namespace"]?.ToString(), 24),-25} {row["status"],-12}");
}
Console.WriteLine($"\nTotal: {rows.Count} library mappings");
});
// map
var mapId = new Argument<int>("id") { Description = "Library mapping ID" };
var mapPackage = new Option<string?>("--package") { Description = ".NET NuGet package" };
var mapNamespace = new Option<string?>("--namespace") { Description = ".NET namespace" };
var mapNotes = new Option<string?>("--notes") { Description = "Usage notes" };
var mapCmd = new Command("map", "Map Go library to .NET package");
mapCmd.Add(mapId);
mapCmd.Add(mapPackage);
mapCmd.Add(mapNamespace);
mapCmd.Add(mapNotes);
mapCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(mapId);
var package = parseResult.GetValue(mapPackage);
var ns = parseResult.GetValue(mapNamespace);
var notes = parseResult.GetValue(mapNotes);
using var db = new Database(dbPath);
var affected = db.Execute(
"UPDATE library_mappings SET dotnet_package = COALESCE(@package, dotnet_package), dotnet_namespace = COALESCE(@ns, dotnet_namespace), dotnet_usage_notes = COALESCE(@notes, dotnet_usage_notes), status = 'mapped' WHERE id = @id",
("@package", package), ("@ns", ns), ("@notes", notes), ("@id", id));
Console.WriteLine(affected > 0 ? $"Library {id} mapped." : $"Library {id} not found.");
});
// suggest
var suggestCmd = new Command("suggest", "Show unmapped libraries");
suggestCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var rows = db.Query(
"SELECT id, go_import_path, go_library_name, go_usage_description FROM library_mappings WHERE status = 'not_mapped' ORDER BY go_import_path");
if (rows.Count == 0)
{
Console.WriteLine("All libraries have been mapped!");
return;
}
Console.WriteLine($"{"ID",-5} {"Go Import Path",-45} {"Library",-20} {"Usage",-40}");
Console.WriteLine(new string('-', 110));
foreach (var row in rows)
{
Console.WriteLine($"{row["id"],-5} {Truncate(row["go_import_path"]?.ToString(), 44),-45} {Truncate(row["go_library_name"]?.ToString(), 19),-20} {Truncate(row["go_usage_description"]?.ToString(), 39),-40}");
}
Console.WriteLine($"\n{rows.Count} unmapped libraries need attention.");
});
libraryCommand.Add(listCmd);
libraryCommand.Add(mapCmd);
libraryCommand.Add(suggestCmd);
return libraryCommand;
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}

View File

@@ -0,0 +1,212 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class PhaseCommands
{
private static readonly (int Number, string Name, string Description)[] Phases =
[
(1, "Analysis & Schema", "Run Go AST analyzer, populate DB schema, map libraries"),
(2, "Core Infrastructure", "Port foundational modules (logging, errors, options)"),
(3, "Message Layer", "Port message parsing, headers, protocol handling"),
(4, "Connection Layer", "Port connection management, reconnection logic"),
(5, "Client API", "Port publish, subscribe, request-reply"),
(6, "Advanced Features", "Port JetStream, KV, Object Store, Services"),
(7, "Testing & Verification", "Port and verify all unit tests, integration tests"),
];
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var phaseCommand = new Command("phase", "Manage porting phases");
// list
var listCmd = new Command("list", "List all phases with status");
listCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
Console.WriteLine($"{"Phase",-7} {"Name",-25} {"Description",-55} {"Status",-12}");
Console.WriteLine(new string('-', 99));
foreach (var (number, name, description) in Phases)
{
var status = CalculatePhaseStatus(db, number);
Console.WriteLine($"{number,-7} {name,-25} {description,-55} {status,-12}");
}
});
// check
var checkPhase = new Argument<int>("phase") { Description = "Phase number (1-7)" };
var checkCmd = new Command("check", "Check phase completion status");
checkCmd.Add(checkPhase);
checkCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var phase = parseResult.GetValue(checkPhase);
using var db = new Database(dbPath);
if (phase < 1 || phase > 7)
{
Console.WriteLine("Phase must be between 1 and 7.");
return;
}
var (_, name, description) = Phases[phase - 1];
Console.WriteLine($"Phase {phase}: {name}");
Console.WriteLine($" {description}\n");
RunPhaseCheck(db, phase);
});
phaseCommand.Add(listCmd);
phaseCommand.Add(checkCmd);
return phaseCommand;
}
private static string CalculatePhaseStatus(Database db, int phase)
{
return phase switch
{
1 => CalculatePhase1Status(db),
2 => CalculateModulePhaseStatus(db, "Core Infrastructure"),
3 => CalculateModulePhaseStatus(db, "Message Layer"),
4 => CalculateModulePhaseStatus(db, "Connection Layer"),
5 => CalculateModulePhaseStatus(db, "Client API"),
6 => CalculateModulePhaseStatus(db, "Advanced Features"),
7 => CalculatePhase7Status(db),
_ => "unknown"
};
}
private static string CalculatePhase1Status(Database db)
{
var totalModules = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules");
var totalLibraries = db.ExecuteScalar<long>("SELECT COUNT(*) FROM library_mappings");
if (totalModules == 0 && totalLibraries == 0) return "not_started";
var mappedLibraries = db.ExecuteScalar<long>("SELECT COUNT(*) FROM library_mappings WHERE status != 'not_mapped'");
if (totalModules > 0 && (totalLibraries == 0 || mappedLibraries == totalLibraries)) return "complete";
return "in_progress";
}
private static string CalculateModulePhaseStatus(Database db, string phaseDescription)
{
// Generic phase status based on overall module completion
var total = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules");
if (total == 0) return "not_started";
var done = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')");
if (done == total) return "complete";
var started = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status != 'not_started'");
return started > 0 ? "in_progress" : "not_started";
}
private static string CalculatePhase7Status(Database db)
{
var totalTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests");
if (totalTests == 0) return "not_started";
var doneTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')");
if (doneTests == totalTests) return "complete";
var startedTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status != 'not_started'");
return startedTests > 0 ? "in_progress" : "not_started";
}
private static void RunPhaseCheck(Database db, int phase)
{
switch (phase)
{
case 1:
CheckPhase1(db);
break;
case 2:
case 3:
case 4:
case 5:
case 6:
CheckModulePhase(db, phase);
break;
case 7:
CheckPhase7(db);
break;
}
}
private static void CheckPhase1(Database db)
{
var totalModules = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules");
var totalFeatures = db.ExecuteScalar<long>("SELECT COUNT(*) FROM features");
var totalTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests");
var totalLibs = db.ExecuteScalar<long>("SELECT COUNT(*) FROM library_mappings");
var mappedLibs = db.ExecuteScalar<long>("SELECT COUNT(*) FROM library_mappings WHERE status != 'not_mapped'");
var totalDeps = db.ExecuteScalar<long>("SELECT COUNT(*) FROM dependencies");
Console.WriteLine("Phase 1 Checklist:");
Console.WriteLine($" [{ (totalModules > 0 ? "x" : " ") }] Modules populated: {totalModules}");
Console.WriteLine($" [{ (totalFeatures > 0 ? "x" : " ") }] Features populated: {totalFeatures}");
Console.WriteLine($" [{ (totalTests > 0 ? "x" : " ") }] Unit tests populated: {totalTests}");
Console.WriteLine($" [{ (totalDeps > 0 ? "x" : " ") }] Dependencies mapped: {totalDeps}");
Console.WriteLine($" [{ (totalLibs > 0 ? "x" : " ") }] Libraries identified: {totalLibs}");
Console.WriteLine($" [{ (totalLibs > 0 && mappedLibs == totalLibs ? "x" : " ") }] All libraries mapped: {mappedLibs}/{totalLibs}");
}
private static void CheckModulePhase(Database db, int phase)
{
var totalModules = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules");
var doneModules = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')");
var stubModules = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status = 'stub'");
var totalFeatures = db.ExecuteScalar<long>("SELECT COUNT(*) FROM features");
var doneFeatures = db.ExecuteScalar<long>("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')");
var stubFeatures = db.ExecuteScalar<long>("SELECT COUNT(*) FROM features WHERE status = 'stub'");
var modPct = totalModules > 0 ? (double)doneModules / totalModules * 100 : 0;
var featPct = totalFeatures > 0 ? (double)doneFeatures / totalFeatures * 100 : 0;
Console.WriteLine($"Phase {phase} Progress:");
Console.WriteLine($" Modules: {doneModules}/{totalModules} complete ({modPct:F1}%), {stubModules} stubs");
Console.WriteLine($" Features: {doneFeatures}/{totalFeatures} complete ({featPct:F1}%), {stubFeatures} stubs");
// Show incomplete modules
var incomplete = db.Query(
"SELECT id, name, status FROM modules WHERE status NOT IN ('complete', 'verified', 'n_a') ORDER BY name");
if (incomplete.Count > 0)
{
Console.WriteLine($"\n Incomplete modules ({incomplete.Count}):");
foreach (var m in incomplete)
Console.WriteLine($" #{m["id"],-5} {m["name"],-30} {m["status"]}");
}
}
private static void CheckPhase7(Database db)
{
var totalTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests");
var doneTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')");
var stubTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status = 'stub'");
var verifiedTests = db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status = 'verified'");
var pct = totalTests > 0 ? (double)doneTests / totalTests * 100 : 0;
Console.WriteLine("Phase 7 Progress:");
Console.WriteLine($" Total tests: {totalTests}");
Console.WriteLine($" Complete: {doneTests - verifiedTests}");
Console.WriteLine($" Verified: {verifiedTests}");
Console.WriteLine($" Stubs: {stubTests}");
Console.WriteLine($" Not started: {totalTests - doneTests - stubTests}");
Console.WriteLine($" Progress: {pct:F1}%");
// Modules with incomplete tests
var modulesWithIncomplete = db.Query(@"
SELECT m.id, m.name, COUNT(*) as total,
SUM(CASE WHEN t.status IN ('complete', 'verified', 'n_a') THEN 1 ELSE 0 END) as done
FROM unit_tests t
JOIN modules m ON t.module_id = m.id
GROUP BY m.id, m.name
HAVING done < total
ORDER BY m.name");
if (modulesWithIncomplete.Count > 0)
{
Console.WriteLine($"\n Modules with incomplete tests ({modulesWithIncomplete.Count}):");
foreach (var m in modulesWithIncomplete)
Console.WriteLine($" #{m["id"],-5} {m["name"],-30} {m["done"]}/{m["total"]}");
}
}
}

View File

@@ -0,0 +1,58 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
using NatsNet.PortTracker.Reporting;
namespace NatsNet.PortTracker.Commands;
public static class ReportCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var reportCommand = new Command("report", "Generate reports");
// summary
var summaryCmd = new Command("summary", "Show status summary");
summaryCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
ReportGenerator.PrintSummary(db);
});
// export
var exportFormat = new Option<string>("--format") { Description = "Export format (md)", DefaultValueFactory = _ => "md" };
var exportOutput = new Option<string?>("--output") { Description = "Output file path (stdout if not specified)" };
var exportCmd = new Command("export", "Export status report");
exportCmd.Add(exportFormat);
exportCmd.Add(exportOutput);
exportCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var format = parseResult.GetValue(exportFormat)!;
var output = parseResult.GetValue(exportOutput);
using var db = new Database(dbPath);
if (format != "md")
{
Console.WriteLine($"Unsupported format: {format}. Supported: md");
return;
}
var markdown = ReportGenerator.ExportMarkdown(db);
if (output is not null)
{
File.WriteAllText(output, markdown);
Console.WriteLine($"Report exported to {output}");
}
else
{
Console.Write(markdown);
}
});
reportCommand.Add(summaryCmd);
reportCommand.Add(exportCmd);
return reportCommand;
}
}

View File

@@ -0,0 +1,143 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class TestCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var testCommand = new Command("test", "Manage unit tests");
// list
var listModule = new Option<int?>("--module") { Description = "Filter by module ID" };
var listStatus = new Option<string?>("--status") { Description = "Filter by status" };
var listCmd = new Command("list", "List unit tests");
listCmd.Add(listModule);
listCmd.Add(listStatus);
listCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var moduleId = parseResult.GetValue(listModule);
var status = parseResult.GetValue(listStatus);
using var db = new Database(dbPath);
var sql = "SELECT t.id, t.name, t.status, t.module_id, m.name as module_name, t.go_method, t.dotnet_method FROM unit_tests t LEFT JOIN modules m ON t.module_id = m.id";
var parameters = new List<(string, object?)>();
var clauses = new List<string>();
if (moduleId is not null)
{
clauses.Add("t.module_id = @module");
parameters.Add(("@module", moduleId));
}
if (status is not null)
{
clauses.Add("t.status = @status");
parameters.Add(("@status", status));
}
if (clauses.Count > 0)
sql += " WHERE " + string.Join(" AND ", clauses);
sql += " ORDER BY m.name, t.name";
var rows = db.Query(sql, parameters.ToArray());
Console.WriteLine($"{"ID",-5} {"Name",-30} {"Status",-15} {"Module",-20} {"Go Method",-25} {"DotNet Method",-25}");
Console.WriteLine(new string('-', 120));
foreach (var row in rows)
{
Console.WriteLine($"{row["id"],-5} {Truncate(row["name"]?.ToString(), 29),-30} {row["status"],-15} {Truncate(row["module_name"]?.ToString(), 19),-20} {Truncate(row["go_method"]?.ToString(), 24),-25} {Truncate(row["dotnet_method"]?.ToString(), 24),-25}");
}
Console.WriteLine($"\nTotal: {rows.Count} tests");
});
// show
var showId = new Argument<int>("id") { Description = "Test ID" };
var showCmd = new Command("show", "Show test details");
showCmd.Add(showId);
showCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(showId);
using var db = new Database(dbPath);
var tests = db.Query(
"SELECT t.*, m.name as module_name FROM unit_tests t LEFT JOIN modules m ON t.module_id = m.id WHERE t.id = @id",
("@id", id));
if (tests.Count == 0)
{
Console.WriteLine($"Test {id} not found.");
return;
}
var t = tests[0];
Console.WriteLine($"Test #{t["id"]}: {t["name"]}");
Console.WriteLine($" Module: #{t["module_id"]} ({t["module_name"]})");
Console.WriteLine($" Feature: {(t["feature_id"] is not null ? $"#{t["feature_id"]}" : "(none)")}");
Console.WriteLine($" Status: {t["status"]}");
Console.WriteLine($" Go File: {t["go_file"]}");
Console.WriteLine($" Go Class: {t["go_class"]}");
Console.WriteLine($" Go Method: {t["go_method"]}");
Console.WriteLine($" Go Line: {t["go_line_number"]}");
Console.WriteLine($" Go LOC: {t["go_line_count"]}");
Console.WriteLine($" .NET: {t["dotnet_project"]} / {t["dotnet_class"]} / {t["dotnet_method"]}");
Console.WriteLine($" Notes: {t["notes"]}");
var deps = db.Query(
"SELECT d.target_type, d.target_id, d.dependency_kind FROM dependencies d WHERE d.source_type = 'unit_test' AND d.source_id = @id",
("@id", id));
Console.WriteLine($"\n Dependencies ({deps.Count}):");
foreach (var d in deps)
Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} [{d["dependency_kind"]}]");
});
// update
var updateId = new Argument<int>("id") { Description = "Test ID" };
var updateStatus = new Option<string>("--status") { Description = "New status", Required = true };
var updateCmd = new Command("update", "Update test status");
updateCmd.Add(updateId);
updateCmd.Add(updateStatus);
updateCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(updateId);
var status = parseResult.GetValue(updateStatus)!;
using var db = new Database(dbPath);
var affected = db.Execute("UPDATE unit_tests SET status = @status WHERE id = @id",
("@status", status), ("@id", id));
Console.WriteLine(affected > 0 ? $"Test {id} updated to '{status}'." : $"Test {id} not found.");
});
// map
var mapId = new Argument<int>("id") { Description = "Test ID" };
var mapProject = new Option<string?>("--project") { Description = "Target .NET project" };
var mapClass = new Option<string?>("--class") { Description = "Target .NET test class" };
var mapMethod = new Option<string?>("--method") { Description = "Target .NET test method" };
var mapCmd = new Command("map", "Map test to .NET test method");
mapCmd.Add(mapId);
mapCmd.Add(mapProject);
mapCmd.Add(mapClass);
mapCmd.Add(mapMethod);
mapCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(mapId);
var project = parseResult.GetValue(mapProject);
var cls = parseResult.GetValue(mapClass);
var method = parseResult.GetValue(mapMethod);
using var db = new Database(dbPath);
var affected = db.Execute(
"UPDATE unit_tests SET dotnet_project = COALESCE(@project, dotnet_project), dotnet_class = COALESCE(@cls, dotnet_class), dotnet_method = COALESCE(@method, dotnet_method) WHERE id = @id",
("@project", project), ("@cls", cls), ("@method", method), ("@id", id));
Console.WriteLine(affected > 0 ? $"Test {id} mapped." : $"Test {id} not found.");
});
testCommand.Add(listCmd);
testCommand.Add(showCmd);
testCommand.Add(updateCmd);
testCommand.Add(mapCmd);
return testCommand;
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}

View File

@@ -33,6 +33,12 @@ initCommand.SetAction(parseResult =>
rootCommand.Add(initCommand); rootCommand.Add(initCommand);
rootCommand.Add(ModuleCommands.Create(dbOption, schemaOption)); rootCommand.Add(ModuleCommands.Create(dbOption, schemaOption));
rootCommand.Add(FeatureCommands.Create(dbOption, schemaOption));
rootCommand.Add(TestCommands.Create(dbOption, schemaOption));
rootCommand.Add(LibraryCommands.Create(dbOption, schemaOption));
rootCommand.Add(DependencyCommands.Create(dbOption, schemaOption));
rootCommand.Add(ReportCommands.Create(dbOption, schemaOption));
rootCommand.Add(PhaseCommands.Create(dbOption, schemaOption));
var parseResult = rootCommand.Parse(args); var parseResult = rootCommand.Parse(args);
return await parseResult.InvokeAsync(); return await parseResult.InvokeAsync();

View File

@@ -0,0 +1,95 @@
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Reporting;
public static class ReportGenerator
{
public static void PrintSummary(Database db)
{
Console.WriteLine("=== Porting Status Summary ===\n");
PrintTableSummary(db, "modules", "Modules");
PrintTableSummary(db, "features", "Features");
PrintTableSummary(db, "unit_tests", "Unit Tests");
PrintLibrarySummary(db);
// Overall progress
var totalItems = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM features") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests");
var doneItems = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')");
var pct = totalItems > 0 ? (double)doneItems / totalItems * 100 : 0;
Console.WriteLine($"\nOverall Progress: {doneItems}/{totalItems} ({pct:F1}%)");
}
public static string ExportMarkdown(Database db)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("# NATS .NET Porting Status Report");
sb.AppendLine($"\nGenerated: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC\n");
AppendTableMarkdown(sb, db, "modules", "Modules");
AppendTableMarkdown(sb, db, "features", "Features");
AppendTableMarkdown(sb, db, "unit_tests", "Unit Tests");
AppendLibraryMarkdown(sb, db);
// Overall
var totalItems = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM features") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests");
var doneItems = db.ExecuteScalar<long>("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')") +
db.ExecuteScalar<long>("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')");
var pct = totalItems > 0 ? (double)doneItems / totalItems * 100 : 0;
sb.AppendLine($"\n## Overall Progress\n");
sb.AppendLine($"**{doneItems}/{totalItems} items complete ({pct:F1}%)**");
return sb.ToString();
}
private static void PrintTableSummary(Database db, string table, string label)
{
var rows = db.Query($"SELECT status, COUNT(*) as cnt FROM {table} GROUP BY status ORDER BY status");
var total = rows.Sum(r => Convert.ToInt64(r["cnt"]));
Console.WriteLine($"{label} ({total} total):");
foreach (var row in rows)
Console.WriteLine($" {row["status"],-15} {row["cnt"],5}");
Console.WriteLine();
}
private static void PrintLibrarySummary(Database db)
{
var rows = db.Query("SELECT status, COUNT(*) as cnt FROM library_mappings GROUP BY status ORDER BY status");
var total = rows.Sum(r => Convert.ToInt64(r["cnt"]));
Console.WriteLine($"Library Mappings ({total} total):");
foreach (var row in rows)
Console.WriteLine($" {row["status"],-15} {row["cnt"],5}");
Console.WriteLine();
}
private static void AppendTableMarkdown(System.Text.StringBuilder sb, Database db, string table, string label)
{
var rows = db.Query($"SELECT status, COUNT(*) as cnt FROM {table} GROUP BY status ORDER BY status");
var total = rows.Sum(r => Convert.ToInt64(r["cnt"]));
sb.AppendLine($"## {label} ({total} total)\n");
sb.AppendLine("| Status | Count |");
sb.AppendLine("|--------|-------|");
foreach (var row in rows)
sb.AppendLine($"| {row["status"]} | {row["cnt"]} |");
sb.AppendLine();
}
private static void AppendLibraryMarkdown(System.Text.StringBuilder sb, Database db)
{
var rows = db.Query("SELECT status, COUNT(*) as cnt FROM library_mappings GROUP BY status ORDER BY status");
var total = rows.Sum(r => Convert.ToInt64(r["cnt"]));
sb.AppendLine($"## Library Mappings ({total} total)\n");
sb.AppendLine("| Status | Count |");
sb.AppendLine("|--------|-------|");
foreach (var row in rows)
sb.AppendLine($"| {row["status"]} | {row["cnt"]} |");
sb.AppendLine();
}
}