diff --git a/tools/NatsNet.PortTracker/Commands/DependencyCommands.cs b/tools/NatsNet.PortTracker/Commands/DependencyCommands.cs new file mode 100644 index 0000000..d8faf7d --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/DependencyCommands.cs @@ -0,0 +1,146 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class DependencyCommands +{ + public static Command Create(Option dbOption, Option schemaOption) + { + var depCommand = new Command("dependency", "Manage dependencies"); + + // show + var showType = new Argument("type") { Description = "Item type (module, feature, unit_test)" }; + var showId = new Argument("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)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs b/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs new file mode 100644 index 0000000..e543e4b --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs @@ -0,0 +1,183 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class FeatureCommands +{ + public static Command Create(Option dbOption, Option schemaOption) + { + var featureCommand = new Command("feature", "Manage features"); + + // list + var listModule = new Option("--module") { Description = "Filter by module ID" }; + var listStatus = new Option("--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(); + 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("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("id") { Description = "Feature ID (use 0 with --all-in-module)" }; + var updateStatus = new Option("--status") { Description = "New status", Required = true }; + var updateAllInModule = new Option("--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("id") { Description = "Feature ID" }; + var mapProject = new Option("--project") { Description = "Target .NET project" }; + var mapClass = new Option("--class") { Description = "Target .NET class" }; + var mapMethod = new Option("--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("id") { Description = "Feature ID" }; + var naReason = new Option("--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)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Commands/LibraryCommands.cs b/tools/NatsNet.PortTracker/Commands/LibraryCommands.cs new file mode 100644 index 0000000..7cfb90f --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/LibraryCommands.cs @@ -0,0 +1,98 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class LibraryCommands +{ + public static Command Create(Option dbOption, Option schemaOption) + { + var libraryCommand = new Command("library", "Manage library mappings"); + + // list + var listStatus = new Option("--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("id") { Description = "Library mapping ID" }; + var mapPackage = new Option("--package") { Description = ".NET NuGet package" }; + var mapNamespace = new Option("--namespace") { Description = ".NET namespace" }; + var mapNotes = new Option("--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)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Commands/PhaseCommands.cs b/tools/NatsNet.PortTracker/Commands/PhaseCommands.cs new file mode 100644 index 0000000..0d45378 --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/PhaseCommands.cs @@ -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 dbOption, Option 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("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("SELECT COUNT(*) FROM modules"); + var totalLibraries = db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings"); + if (totalModules == 0 && totalLibraries == 0) return "not_started"; + var mappedLibraries = db.ExecuteScalar("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("SELECT COUNT(*) FROM modules"); + if (total == 0) return "not_started"; + var done = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')"); + if (done == total) return "complete"; + var started = db.ExecuteScalar("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("SELECT COUNT(*) FROM unit_tests"); + if (totalTests == 0) return "not_started"; + var doneTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')"); + if (doneTests == totalTests) return "complete"; + var startedTests = db.ExecuteScalar("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("SELECT COUNT(*) FROM modules"); + var totalFeatures = db.ExecuteScalar("SELECT COUNT(*) FROM features"); + var totalTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests"); + var totalLibs = db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings"); + var mappedLibs = db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings WHERE status != 'not_mapped'"); + var totalDeps = db.ExecuteScalar("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("SELECT COUNT(*) FROM modules"); + var doneModules = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')"); + var stubModules = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE status = 'stub'"); + var totalFeatures = db.ExecuteScalar("SELECT COUNT(*) FROM features"); + var doneFeatures = db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')"); + var stubFeatures = db.ExecuteScalar("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("SELECT COUNT(*) FROM unit_tests"); + var doneTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE status IN ('complete', 'verified', 'n_a')"); + var stubTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE status = 'stub'"); + var verifiedTests = db.ExecuteScalar("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"]}"); + } + } +} diff --git a/tools/NatsNet.PortTracker/Commands/ReportCommands.cs b/tools/NatsNet.PortTracker/Commands/ReportCommands.cs new file mode 100644 index 0000000..a68a3f7 --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/ReportCommands.cs @@ -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 dbOption, Option 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("--format") { Description = "Export format (md)", DefaultValueFactory = _ => "md" }; + var exportOutput = new Option("--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; + } +} diff --git a/tools/NatsNet.PortTracker/Commands/TestCommands.cs b/tools/NatsNet.PortTracker/Commands/TestCommands.cs new file mode 100644 index 0000000..e18b3b2 --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/TestCommands.cs @@ -0,0 +1,143 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class TestCommands +{ + public static Command Create(Option dbOption, Option schemaOption) + { + var testCommand = new Command("test", "Manage unit tests"); + + // list + var listModule = new Option("--module") { Description = "Filter by module ID" }; + var listStatus = new Option("--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(); + 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("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("id") { Description = "Test ID" }; + var updateStatus = new Option("--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("id") { Description = "Test ID" }; + var mapProject = new Option("--project") { Description = "Target .NET project" }; + var mapClass = new Option("--class") { Description = "Target .NET test class" }; + var mapMethod = new Option("--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)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Program.cs b/tools/NatsNet.PortTracker/Program.cs index dbc1ab0..24d4624 100644 --- a/tools/NatsNet.PortTracker/Program.cs +++ b/tools/NatsNet.PortTracker/Program.cs @@ -33,6 +33,12 @@ initCommand.SetAction(parseResult => rootCommand.Add(initCommand); 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); return await parseResult.InvokeAsync(); diff --git a/tools/NatsNet.PortTracker/Reporting/ReportGenerator.cs b/tools/NatsNet.PortTracker/Reporting/ReportGenerator.cs new file mode 100644 index 0000000..56f9288 --- /dev/null +++ b/tools/NatsNet.PortTracker/Reporting/ReportGenerator.cs @@ -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("SELECT COUNT(*) FROM modules") + + db.ExecuteScalar("SELECT COUNT(*) FROM features") + + db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests"); + var doneItems = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')") + + db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')") + + db.ExecuteScalar("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("SELECT COUNT(*) FROM modules") + + db.ExecuteScalar("SELECT COUNT(*) FROM features") + + db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests"); + var doneItems = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE status IN ('complete', 'verified', 'n_a')") + + db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status IN ('complete', 'verified', 'n_a')") + + db.ExecuteScalar("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(); + } +}