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.
This commit is contained in:
136
tools/NatsNet.PortTracker/Audit/AuditVerifier.cs
Normal file
136
tools/NatsNet.PortTracker/Audit/AuditVerifier.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user