namespace NatsNet.PortTracker.Audit; using NatsNet.PortTracker.Data; /// /// Verifies status updates against audit classification results. /// Used by feature and test update commands to ensure status accuracy. /// public static class AuditVerifier { public record VerificationResult( long ItemId, string AuditStatus, string AuditReason, bool Matches); private static readonly Dictionary 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") }; /// /// Build a SourceIndexer for the appropriate table type. /// 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; } /// /// Verify items matching a WHERE clause against audit classification. /// public static List 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(); 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; } /// /// Check verification results and print a report. /// Returns true if the update should proceed. /// public static bool CheckAndReport( List 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; } /// /// Log override records to the status_overrides table. /// public static void LogOverrides( Database db, string tableName, IEnumerable 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; } } }