using System.CommandLine; using NatsNet.PortTracker.Audit; 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 updateOverride = new Option("--override") { Description = "Override audit mismatch with this comment" }; var updateCmd = new Command("update", "Update feature status (audit-verified)"); updateCmd.Add(updateId); updateCmd.Add(updateStatus); updateCmd.Add(updateAllInModule); updateCmd.Add(updateOverride); updateCmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(updateId); var status = parseResult.GetValue(updateStatus)!; var allInModule = parseResult.GetValue(updateAllInModule); var overrideComment = parseResult.GetValue(updateOverride); using var db = new Database(dbPath); var indexer = AuditVerifier.BuildIndexer("features"); if (allInModule is not null) { var verifications = AuditVerifier.VerifyItems(db, indexer, "features", " WHERE module_id = @module", [("@module", (object?)allInModule)], status); if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment)) return; 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}'."); var mismatches = verifications.Where(r => !r.Matches).ToList(); if (mismatches.Count > 0 && overrideComment is not null) AuditVerifier.LogOverrides(db, "features", mismatches, status, overrideComment); } else { var verifications = AuditVerifier.VerifyItems(db, indexer, "features", " WHERE id = @id", [("@id", (object?)id)], status); if (verifications.Count == 0) { Console.WriteLine($"Feature {id} not found."); return; } if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment)) return; 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."); var mismatches = verifications.Where(r => !r.Matches).ToList(); if (mismatches.Count > 0 && overrideComment is not null) AuditVerifier.LogOverrides(db, "features", mismatches, status, overrideComment); } }); // 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); featureCommand.Add(CreateBatchUpdate(dbOption)); featureCommand.Add(CreateBatchMap(dbOption)); return featureCommand; } private static Command CreateBatchUpdate(Option dbOption) { var cmd = new Command("batch-update", "Bulk update feature status (audit-verified)"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; var overrideOpt = new Option("--override") { Description = "Override audit mismatches with this comment" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); cmd.Add(overrideOpt); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); var overrideComment = parseResult.GetValue(overrideOpt); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); // Audit verification var indexer = AuditVerifier.BuildIndexer("features"); var verifications = AuditVerifier.VerifyItems(db, indexer, "features", whereClause, filterParams, newStatus); if (!AuditVerifier.CheckAndReport(verifications, newStatus, overrideComment)) return; var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) { setClauses.Add("notes = @newNotes"); updateParams.Add(("@newNotes", notes)); } BatchFilters.PreviewOrExecute(db, "features", "id, name, status, module_id, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); // Log overrides after successful execute if (execute) { var mismatches = verifications.Where(r => !r.Matches).ToList(); if (mismatches.Count > 0 && overrideComment is not null) AuditVerifier.LogOverrides(db, "features", mismatches, newStatus, overrideComment); } }); return cmd; } private static Command CreateBatchMap(Option dbOption) { var cmd = new Command("batch-map", "Bulk map features to .NET methods"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setProject = new Option("--set-project") { Description = ".NET project" }; var setClass = new Option("--set-class") { Description = ".NET class" }; var setMethod = new Option("--set-method") { Description = ".NET method" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setProject); cmd.Add(setClass); cmd.Add(setMethod); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var project = parseResult.GetValue(setProject); var cls = parseResult.GetValue(setClass); var method = parseResult.GetValue(setMethod); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } if (project is null && cls is null && method is null) { Console.WriteLine("Error: at least one of --set-project, --set-class, --set-method is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); var setClauses = new List(); var updateParams = new List<(string, object?)>(); if (project is not null) { setClauses.Add("dotnet_project = @setProject"); updateParams.Add(("@setProject", project)); } if (cls is not null) { setClauses.Add("dotnet_class = @setClass"); updateParams.Add(("@setClass", cls)); } if (method is not null) { setClauses.Add("dotnet_method = @setMethod"); updateParams.Add(("@setMethod", method)); } BatchFilters.PreviewOrExecute(db, "features", "id, name, status, dotnet_project, dotnet_class, dotnet_method", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } private static string Truncate(string? s, int maxLen) { if (s is null) return ""; return s.Length <= maxLen ? s : s[..(maxLen - 2)] + ".."; } }