using System.CommandLine; using System.Text; using NatsNet.PortTracker.Audit; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class AuditCommand { public static Command Create(Option dbOption) { var sourceOpt = new Option("--source") { Description = "Path to the .NET source directory", DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server") }; var outputOpt = new Option("--output") { Description = "CSV report output path", DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results.csv") }; var moduleOpt = new Option("--module") { Description = "Restrict to a specific module ID" }; var executeOpt = new Option("--execute") { Description = "Apply DB updates (default: dry-run preview)", DefaultValueFactory = _ => false }; var cmd = new Command("audit", "Classify unknown features by inspecting .NET source code"); cmd.Add(sourceOpt); cmd.Add(outputOpt); cmd.Add(moduleOpt); cmd.Add(executeOpt); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var sourcePath = parseResult.GetValue(sourceOpt)!; var outputPath = parseResult.GetValue(outputOpt)!; var moduleId = parseResult.GetValue(moduleOpt); var execute = parseResult.GetValue(executeOpt); RunAudit(dbPath, sourcePath, outputPath, moduleId, execute); }); return cmd; } private static void RunAudit(string dbPath, string sourcePath, string outputPath, int? moduleId, bool execute) { // Validate source directory if (!Directory.Exists(sourcePath)) { Console.WriteLine($"Error: source directory not found: {sourcePath}"); return; } // 1. Build source index Console.WriteLine($"Parsing .NET source files in {sourcePath}..."); var indexer = new SourceIndexer(); indexer.IndexDirectory(sourcePath); Console.WriteLine($"Indexed {indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods/properties."); // 2. Query unknown features using var db = new Database(dbPath); var sql = "SELECT id, dotnet_class, dotnet_method, go_file, go_method FROM features WHERE status = 'unknown'"; var parameters = new List<(string, object?)>(); if (moduleId is not null) { sql += " AND module_id = @module"; parameters.Add(("@module", moduleId)); } sql += " ORDER BY id"; var rows = db.Query(sql, parameters.ToArray()); if (rows.Count == 0) { Console.WriteLine("No unknown features found."); return; } Console.WriteLine($"Found {rows.Count} unknown features to classify.\n"); // 3. Classify each feature var classifier = new FeatureClassifier(indexer); var results = new List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)>(); foreach (var row in rows) { var feature = new FeatureClassifier.FeatureRecord( Id: Convert.ToInt64(row["id"]), DotnetClass: row["dotnet_class"]?.ToString() ?? "", DotnetMethod: row["dotnet_method"]?.ToString() ?? "", GoFile: row["go_file"]?.ToString() ?? "", GoMethod: row["go_method"]?.ToString() ?? ""); var result = classifier.Classify(feature); results.Add((feature, result)); } // 4. Write CSV report WriteCsvReport(outputPath, results); // 5. Print console summary var grouped = results.GroupBy(r => r.Result.Status) .ToDictionary(g => g.Key, g => g.Count()); Console.WriteLine("Feature Status Audit Results"); Console.WriteLine("============================="); Console.WriteLine($"Source: {sourcePath} ({indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods indexed)"); Console.WriteLine($"Features audited: {results.Count}"); Console.WriteLine(); Console.WriteLine($" verified: {grouped.GetValueOrDefault("verified", 0)}"); Console.WriteLine($" stub: {grouped.GetValueOrDefault("stub", 0)}"); Console.WriteLine($" n_a: {grouped.GetValueOrDefault("n_a", 0)}"); Console.WriteLine($" deferred: {grouped.GetValueOrDefault("deferred", 0)}"); Console.WriteLine(); if (!execute) { Console.WriteLine("Dry-run mode. Add --execute to apply changes."); Console.WriteLine($"Report: {outputPath}"); return; } // 6. Apply DB updates ApplyUpdates(db, results); Console.WriteLine($"Report: {outputPath}"); } private static void WriteCsvReport( string outputPath, List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results) { // Ensure directory exists var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); var sb = new StringBuilder(); sb.AppendLine("id,dotnet_class,dotnet_method,go_file,go_method,old_status,new_status,reason"); foreach (var (feature, result) in results) { sb.AppendLine($"{feature.Id},{CsvEscape(feature.DotnetClass)},{CsvEscape(feature.DotnetMethod)},{CsvEscape(feature.GoFile)},{CsvEscape(feature.GoMethod)},unknown,{result.Status},{CsvEscape(result.Reason)}"); } File.WriteAllText(outputPath, sb.ToString()); } private static void ApplyUpdates( Database db, List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results) { // Group by (status, notes) for efficient batch updates var groups = results .GroupBy(r => (r.Result.Status, Notes: r.Result.Status == "n_a" ? r.Result.Reason : (string?)null)) .ToList(); var totalUpdated = 0; using var transaction = db.Connection.BeginTransaction(); try { foreach (var group in groups) { var ids = group.Select(r => r.Feature.Id).ToList(); var status = group.Key.Status; var notes = group.Key.Notes; // Build parameterized IN clause var placeholders = new List(); using var cmd = db.CreateCommand(""); for (var i = 0; i < ids.Count; i++) { placeholders.Add($"@id{i}"); cmd.Parameters.AddWithValue($"@id{i}", ids[i]); } cmd.Parameters.AddWithValue("@status", status); if (notes is not null) { cmd.CommandText = $"UPDATE features SET status = @status, notes = @notes WHERE id IN ({string.Join(", ", placeholders)})"; cmd.Parameters.AddWithValue("@notes", notes); } else { cmd.CommandText = $"UPDATE features SET status = @status WHERE id IN ({string.Join(", ", placeholders)})"; } cmd.Transaction = transaction; var affected = cmd.ExecuteNonQuery(); totalUpdated += affected; } transaction.Commit(); Console.WriteLine($"Updated {totalUpdated} features."); } catch { transaction.Rollback(); Console.WriteLine("Error: transaction rolled back."); throw; } } private static string CsvEscape(string value) { if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return $"\"{value.Replace("\"", "\"\"")}\""; return value; } }