From 7a338dd510cb0c82cb9963fd67c73557e4f9ac1a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 27 Feb 2026 05:50:15 -0500 Subject: [PATCH] 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. --- porting-schema.sql | 11 ++ porting.db | Bin 3403776 -> 3403776 bytes reports/current.md | 8 +- reports/report_3297334.md | 38 +++++ .../Audit/AuditVerifier.cs | 136 ++++++++++++++++++ .../Commands/FeatureCommands.cs | 50 ++++++- .../Commands/OverrideCommands.cs | 66 +++++++++ .../Commands/TestCommands.cs | 41 +++++- tools/NatsNet.PortTracker/Program.cs | 1 + 9 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 reports/report_3297334.md create mode 100644 tools/NatsNet.PortTracker/Audit/AuditVerifier.cs create mode 100644 tools/NatsNet.PortTracker/Commands/OverrideCommands.cs diff --git a/porting-schema.sql b/porting-schema.sql index 41b6ed1..f016122 100644 --- a/porting-schema.sql +++ b/porting-schema.sql @@ -88,6 +88,17 @@ CREATE TABLE IF NOT EXISTS library_mappings ( updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS status_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL CHECK (table_name IN ('features', 'unit_tests')), + item_id INTEGER NOT NULL, + audit_status TEXT NOT NULL, + audit_reason TEXT NOT NULL, + requested_status TEXT NOT NULL, + comment TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + -- Indexes CREATE INDEX IF NOT EXISTS idx_features_module ON features(module_id); CREATE INDEX IF NOT EXISTS idx_features_status ON features(status); diff --git a/porting.db b/porting.db index 72f8487a3864b77b7cbff1f0abaaf6a72e562bb7..1f330f322e6a03ec7bdbfc85f2fed400d235aa42 100644 GIT binary patch delta 699 zcmZ{hO>YuW6o%*C0fynDGqgplK)FyWAWT8Qq`J|HB-WC)shGHf5iXImfWqL)#70kd zCRT}#J0%ykOa~lx+9$yhh?^3eQ|uLYzyI(2`q`FD%erRqs5O znoCb)(=lmwgAvXWEuI~u`oI7-S2t+X~x&TB+&7IpG#uB*m z*+Q*Sp;e_fR?uxfLb5_m=?`wj?87Lm?Iuv*vFQ`;W4M><>_ePYPl!0FEqBl)(%!vC zAB;m{k(Ts-phbMf&RSV7|7X_TeniqZa;Rp4cQ8w|4#9z2>^9R;jcfSo-Orpyd$5Kh z$Yk3Tc!5`6t>d$r|0#aLu@W<==6TVhKDsDg(sIvn+j%cX+9puUh?z9qP_EOw`T`)@V!~J(f%Y3kEQP2{gQPddtoxL=>P|a$bye?VOordiZDT|!td0lZYH{^N^ zx$2Zeu63$`QAV3x?mryS$cwd3f0fy0R#+>Q{BTO1dOzEq*EL%WcNVH?n|$Y3q~evX zVH3U4QLHGutV6v|RolYHPpM~hHPNfS$0-P3I;+MMtI?i6g&ko^PPik5rPXUElvAF* zmDQ>T!*yHP7}e(loPgKf$R>nSx3Mh|UQ|+$`}s)4UsFB3+Iy=qsy1BS%o^e>QyZha z$`&cSmXGn~b~)Kk`7Rshjgv;Z(P}gq8;w;)y|K`!F)EEo#yF$I$TUKRZ7BLh{fvH0 zZwt%{Oba|7cpxx3P#DN?E;?s?kLfM?HhsOmQeUFa*I(49>W}DS^*i)D=T0a8jrfQP zmn~w;*lM6^N;dsE5QYAY1Q z(h^wjYxl;@m|blifw#vY4g*RM-vNpchX9#~Zvi1hyJoFWY}7&TLOL7d`D25&cP$29 zjrnhod)?ZQe~ zvp|^InhC(rRu%b2e7MeJal8hA#qk#avbZWhC005efW`6W09YKa1eDacy_?0Sfv`GW z0l+Qc&jPSGJ{5q)@hP-8zfq**z(GUbdkEf>Q%eDI>Vx=a-TC=#sBRQd6jBsWaNdXvzN?=~6_>baWifa^CDgL4OyBBWnS#^aD z{Y7z^;u6J0ia#kXP;>GolT! z2H^I5R2={<$D#dzR}fzT<{-WVR3Y{O$`N}3;}LrR_aa&W#faU25s1$LgI&~TAlW7@ z0NEy=@-jts$tM8WCA$E!OFjn3E@=kHF4+l?UGfn?c1f7KW&Lf^1Vge-b^v6Xd8e%J-0{q=a0D-B!j;Vkii=OGWZ&R489s5gTD!o!B+ufaCkE6 z5B@qJ6Mo3o^4zYto(GXqyV)5lFJkWi;6-c`fU9Q*0JwVAegG~6uXE>W%3~fyTU})<}0EV`n0AOf78GxaAIRHcRM*&zbe;9!E z!iGZVzAopZCW2(a69Bk#d_37XK8zx>se^4u_U^u6CwbM^S3~#L<|+_ dB12?~VPd$*5+g*m7%6f@uE-PlqF`NN*Z +/// 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; + } + } +} diff --git a/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs b/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs index b6bc620..a928a1b 100644 --- a/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs +++ b/tools/NatsNet.PortTracker/Commands/FeatureCommands.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using NatsNet.PortTracker.Audit; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; @@ -96,31 +97,59 @@ public static class FeatureCommands var updateId = new Argument("id") { Description = "Feature ID (use 0 with --all-in-module)" }; var updateStatus = new Option("--status") { Description = "New status", Required = true }; var updateAllInModule = new Option("--all-in-module") { Description = "Update all features in this module ID" }; - var updateCmd = new Command("update", "Update feature status"); + var updateOverride = new Option("--override") { Description = "Override audit mismatch with this comment" }; + var updateCmd = new Command("update", "Update feature status (audit-verified)"); updateCmd.Add(updateId); updateCmd.Add(updateStatus); updateCmd.Add(updateAllInModule); + updateCmd.Add(updateOverride); updateCmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(updateId); var status = parseResult.GetValue(updateStatus)!; var allInModule = parseResult.GetValue(updateAllInModule); + var overrideComment = parseResult.GetValue(updateOverride); using var db = new Database(dbPath); + var indexer = AuditVerifier.BuildIndexer("features"); + if (allInModule is not null) { + var verifications = AuditVerifier.VerifyItems(db, indexer, "features", + " WHERE module_id = @module", [("@module", (object?)allInModule)], status); + if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment)) + return; + var affected = db.Execute( "UPDATE features SET status = @status WHERE module_id = @module", ("@status", status), ("@module", allInModule)); Console.WriteLine($"Updated {affected} features in module {allInModule} to '{status}'."); + + var mismatches = verifications.Where(r => !r.Matches).ToList(); + if (mismatches.Count > 0 && overrideComment is not null) + AuditVerifier.LogOverrides(db, "features", mismatches, status, overrideComment); } else { + var verifications = AuditVerifier.VerifyItems(db, indexer, "features", + " WHERE id = @id", [("@id", (object?)id)], status); + if (verifications.Count == 0) + { + Console.WriteLine($"Feature {id} not found."); + return; + } + if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment)) + return; + var affected = db.Execute( "UPDATE features SET status = @status WHERE id = @id", ("@status", status), ("@id", id)); Console.WriteLine(affected > 0 ? $"Feature {id} updated to '{status}'." : $"Feature {id} not found."); + + var mismatches = verifications.Where(r => !r.Matches).ToList(); + if (mismatches.Count > 0 && overrideComment is not null) + AuditVerifier.LogOverrides(db, "features", mismatches, status, overrideComment); } }); @@ -179,13 +208,14 @@ public static class FeatureCommands private static Command CreateBatchUpdate(Option dbOption) { - var cmd = new Command("batch-update", "Bulk update feature status"); + var cmd = new Command("batch-update", "Bulk update feature status (audit-verified)"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; + var overrideOpt = new Option("--override") { Description = "Override audit mismatches with this comment" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); @@ -193,6 +223,7 @@ public static class FeatureCommands cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); + cmd.Add(overrideOpt); cmd.SetAction(parseResult => { @@ -203,6 +234,7 @@ public static class FeatureCommands var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); + var overrideComment = parseResult.GetValue(overrideOpt); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { @@ -213,6 +245,12 @@ public static class FeatureCommands using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); + // Audit verification + var indexer = AuditVerifier.BuildIndexer("features"); + var verifications = AuditVerifier.VerifyItems(db, indexer, "features", whereClause, filterParams, newStatus); + if (!AuditVerifier.CheckAndReport(verifications, newStatus, overrideComment)) + return; + var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) @@ -225,6 +263,14 @@ public static class FeatureCommands "id, name, status, module_id, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); + + // Log overrides after successful execute + if (execute) + { + var mismatches = verifications.Where(r => !r.Matches).ToList(); + if (mismatches.Count > 0 && overrideComment is not null) + AuditVerifier.LogOverrides(db, "features", mismatches, newStatus, overrideComment); + } }); return cmd; diff --git a/tools/NatsNet.PortTracker/Commands/OverrideCommands.cs b/tools/NatsNet.PortTracker/Commands/OverrideCommands.cs new file mode 100644 index 0000000..288a4e5 --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/OverrideCommands.cs @@ -0,0 +1,66 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class OverrideCommands +{ + public static Command Create(Option dbOption) + { + var overrideCommand = new Command("override", "Review status override records"); + + var typeOpt = new Option("--type") + { + Description = "Filter by table: features or tests" + }; + + var listCmd = new Command("list", "List all status overrides"); + listCmd.Add(typeOpt); + listCmd.SetAction(parseResult => + { + var dbPath = parseResult.GetValue(dbOption)!; + var type = parseResult.GetValue(typeOpt); + using var db = new Database(dbPath); + + var sql = "SELECT id, table_name, item_id, audit_status, requested_status, comment, created_at FROM status_overrides"; + var parameters = new List<(string, object?)>(); + + if (type is not null) + { + var tableName = type switch + { + "features" => "features", + "tests" => "unit_tests", + _ => type + }; + sql += " WHERE table_name = @table"; + parameters.Add(("@table", tableName)); + } + sql += " ORDER BY created_at DESC"; + + var rows = db.Query(sql, parameters.ToArray()); + if (rows.Count == 0) + { + Console.WriteLine("No overrides found."); + return; + } + + Console.WriteLine($"{"ID",-5} {"Table",-12} {"Item",-6} {"Audit",-10} {"Requested",-10} {"Comment",-35} {"Date",-20}"); + Console.WriteLine(new string('-', 98)); + foreach (var row in rows) + { + Console.WriteLine($"{row["id"],-5} {row["table_name"],-12} {row["item_id"],-6} {row["audit_status"],-10} {row["requested_status"],-10} {Truncate(row["comment"]?.ToString(), 34),-35} {row["created_at"],-20}"); + } + Console.WriteLine($"\nTotal: {rows.Count} overrides"); + }); + + overrideCommand.Add(listCmd); + return overrideCommand; + } + + private static string Truncate(string? s, int maxLen) + { + if (s is null) return ""; + return s.Length <= maxLen ? s : s[..(maxLen - 2)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Commands/TestCommands.cs b/tools/NatsNet.PortTracker/Commands/TestCommands.cs index f2ce4f6..5bbcb9d 100644 --- a/tools/NatsNet.PortTracker/Commands/TestCommands.cs +++ b/tools/NatsNet.PortTracker/Commands/TestCommands.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using NatsNet.PortTracker.Audit; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; @@ -89,18 +90,37 @@ public static class TestCommands // update var updateId = new Argument("id") { Description = "Test ID" }; var updateStatus = new Option("--status") { Description = "New status", Required = true }; - var updateCmd = new Command("update", "Update test status"); + var updateOverride = new Option("--override") { Description = "Override audit mismatch with this comment" }; + var updateCmd = new Command("update", "Update test status (audit-verified)"); updateCmd.Add(updateId); updateCmd.Add(updateStatus); + updateCmd.Add(updateOverride); updateCmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(updateId); var status = parseResult.GetValue(updateStatus)!; + var overrideComment = parseResult.GetValue(updateOverride); using var db = new Database(dbPath); + + var indexer = AuditVerifier.BuildIndexer("unit_tests"); + var verifications = AuditVerifier.VerifyItems(db, indexer, "unit_tests", + " WHERE id = @id", [("@id", (object?)id)], status); + if (verifications.Count == 0) + { + Console.WriteLine($"Test {id} not found."); + return; + } + if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment)) + return; + var affected = db.Execute("UPDATE unit_tests SET status = @status WHERE id = @id", ("@status", status), ("@id", id)); Console.WriteLine(affected > 0 ? $"Test {id} updated to '{status}'." : $"Test {id} not found."); + + var mismatches = verifications.Where(r => !r.Matches).ToList(); + if (mismatches.Count > 0 && overrideComment is not null) + AuditVerifier.LogOverrides(db, "unit_tests", mismatches, status, overrideComment); }); // map @@ -139,13 +159,14 @@ public static class TestCommands private static Command CreateBatchUpdate(Option dbOption) { - var cmd = new Command("batch-update", "Bulk update test status"); + var cmd = new Command("batch-update", "Bulk update test status (audit-verified)"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; + var overrideOpt = new Option("--override") { Description = "Override audit mismatches with this comment" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); @@ -153,6 +174,7 @@ public static class TestCommands cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); + cmd.Add(overrideOpt); cmd.SetAction(parseResult => { @@ -163,6 +185,7 @@ public static class TestCommands var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); + var overrideComment = parseResult.GetValue(overrideOpt); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { @@ -173,6 +196,12 @@ public static class TestCommands using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); + // Audit verification + var indexer = AuditVerifier.BuildIndexer("unit_tests"); + var verifications = AuditVerifier.VerifyItems(db, indexer, "unit_tests", whereClause, filterParams, newStatus); + if (!AuditVerifier.CheckAndReport(verifications, newStatus, overrideComment)) + return; + var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) @@ -185,6 +214,14 @@ public static class TestCommands "id, name, status, module_id, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); + + // Log overrides after successful execute + if (execute) + { + var mismatches = verifications.Where(r => !r.Matches).ToList(); + if (mismatches.Count > 0 && overrideComment is not null) + AuditVerifier.LogOverrides(db, "unit_tests", mismatches, newStatus, overrideComment); + } }); return cmd; diff --git a/tools/NatsNet.PortTracker/Program.cs b/tools/NatsNet.PortTracker/Program.cs index 1c943f8..49f1a8d 100644 --- a/tools/NatsNet.PortTracker/Program.cs +++ b/tools/NatsNet.PortTracker/Program.cs @@ -40,6 +40,7 @@ rootCommand.Add(DependencyCommands.Create(dbOption, schemaOption)); rootCommand.Add(ReportCommands.Create(dbOption, schemaOption)); rootCommand.Add(PhaseCommands.Create(dbOption, schemaOption)); rootCommand.Add(AuditCommand.Create(dbOption)); +rootCommand.Add(OverrideCommands.Create(dbOption)); var parseResult = rootCommand.Parse(args); return await parseResult.InvokeAsync();