217 lines
8.1 KiB
C#
217 lines
8.1 KiB
C#
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<string> dbOption)
|
|
{
|
|
var sourceOpt = new Option<string>("--source")
|
|
{
|
|
Description = "Path to the .NET source directory",
|
|
DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server")
|
|
};
|
|
|
|
var outputOpt = new Option<string>("--output")
|
|
{
|
|
Description = "CSV report output path",
|
|
DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results.csv")
|
|
};
|
|
|
|
var moduleOpt = new Option<int?>("--module")
|
|
{
|
|
Description = "Restrict to a specific module ID"
|
|
};
|
|
|
|
var executeOpt = new Option<bool>("--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<string>();
|
|
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;
|
|
}
|
|
}
|