Files
natsnet/tools/NatsNet.PortTracker/Audit/AuditVerifier.cs
Joseph Doherty 7a338dd510 feat: add audit-verified status updates with override tracking
Status updates (feature/test update and batch-update) now verify the
requested status against Roslyn audit classification. Mismatches
require --override "reason" to force. Overrides are logged to a new
status_overrides table and reviewable via 'override list' command.
2026-02-27 05:50:15 -05:00

137 lines
5.4 KiB
C#

namespace NatsNet.PortTracker.Audit;
using NatsNet.PortTracker.Data;
/// <summary>
/// Verifies status updates against audit classification results.
/// Used by feature and test update commands to ensure status accuracy.
/// </summary>
public static class AuditVerifier
{
public record VerificationResult(
long ItemId,
string AuditStatus,
string AuditReason,
bool Matches);
private static readonly Dictionary<string, string> DefaultSourcePaths = new()
{
["features"] = Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server"),
["unit_tests"] = Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "tests", "ZB.MOM.NatsNet.Server.Tests")
};
/// <summary>
/// Build a SourceIndexer for the appropriate table type.
/// </summary>
public static SourceIndexer BuildIndexer(string tableName)
{
var sourcePath = DefaultSourcePaths[tableName];
if (!Directory.Exists(sourcePath))
throw new DirectoryNotFoundException($"Source directory not found: {sourcePath}");
Console.WriteLine($"Building audit index from {sourcePath}...");
var indexer = new SourceIndexer();
indexer.IndexDirectory(sourcePath);
Console.WriteLine($"Indexed {indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods/properties.");
return indexer;
}
/// <summary>
/// Verify items matching a WHERE clause against audit classification.
/// </summary>
public static List<VerificationResult> VerifyItems(
Database db, SourceIndexer indexer, string tableName,
string whereClause, List<(string, object?)> parameters, string requestedStatus)
{
var sql = $"SELECT id, dotnet_class, dotnet_method, go_file, go_method FROM {tableName}{whereClause} ORDER BY id";
var rows = db.Query(sql, parameters.ToArray());
var classifier = new FeatureClassifier(indexer);
var results = new List<VerificationResult>();
foreach (var row in rows)
{
var record = 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 classification = classifier.Classify(record);
var matches = classification.Status == requestedStatus;
results.Add(new VerificationResult(record.Id, classification.Status, classification.Reason, matches));
}
return results;
}
/// <summary>
/// Check verification results and print a report.
/// Returns true if the update should proceed.
/// </summary>
public static bool CheckAndReport(
List<VerificationResult> results, string requestedStatus, string? overrideComment)
{
var matches = results.Where(r => r.Matches).ToList();
var mismatches = results.Where(r => !r.Matches).ToList();
Console.WriteLine($"\nAudit verification: {matches.Count} match, {mismatches.Count} mismatch");
if (mismatches.Count == 0)
return true;
Console.WriteLine($"\nMismatches (requested '{requestedStatus}'):");
foreach (var m in mismatches.Take(20))
Console.WriteLine($" ID {m.ItemId}: audit says '{m.AuditStatus}' ({m.AuditReason})");
if (mismatches.Count > 20)
Console.WriteLine($" ... and {mismatches.Count - 20} more");
if (overrideComment is null)
{
Console.WriteLine($"\n{mismatches.Count} items have audit mismatches. Use --override \"reason\" to force.");
return false;
}
Console.WriteLine($"\nOverride applied: \"{overrideComment}\"");
return true;
}
/// <summary>
/// Log override records to the status_overrides table.
/// </summary>
public static void LogOverrides(
Database db, string tableName, IEnumerable<VerificationResult> mismatches,
string requestedStatus, string comment)
{
var mismatchList = mismatches.ToList();
if (mismatchList.Count == 0) return;
using var transaction = db.Connection.BeginTransaction();
try
{
foreach (var mismatch in mismatchList)
{
using var cmd = db.CreateCommand(
"INSERT INTO status_overrides (table_name, item_id, audit_status, audit_reason, requested_status, comment) " +
"VALUES (@table, @item, @auditStatus, @auditReason, @requestedStatus, @comment)");
cmd.Parameters.AddWithValue("@table", tableName);
cmd.Parameters.AddWithValue("@item", mismatch.ItemId);
cmd.Parameters.AddWithValue("@auditStatus", mismatch.AuditStatus);
cmd.Parameters.AddWithValue("@auditReason", mismatch.AuditReason);
cmd.Parameters.AddWithValue("@requestedStatus", requestedStatus);
cmd.Parameters.AddWithValue("@comment", comment);
cmd.Transaction = transaction;
cmd.ExecuteNonQuery();
}
transaction.Commit();
Console.WriteLine($"Logged {mismatchList.Count} override(s) to status_overrides table.");
}
catch
{
transaction.Rollback();
throw;
}
}
}