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 72f8487..1f330f3 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index e1e403e..e772497 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-27 10:46:13 UTC +Generated: 2026-02-27 10:50:16 UTC ## Modules (12 total) @@ -12,10 +12,10 @@ Generated: 2026-02-27 10:46:13 UTC | Status | Count | |--------|-------| -| deferred | 2500 | +| deferred | 2501 | | n_a | 18 | | stub | 168 | -| verified | 987 | +| verified | 986 | ## Unit Tests (3257 total) @@ -35,4 +35,4 @@ Generated: 2026-02-27 10:46:13 UTC ## Overall Progress -**1594/6942 items complete (23.0%)** +**1593/6942 items complete (22.9%)** diff --git a/reports/report_3297334.md b/reports/report_3297334.md new file mode 100644 index 0000000..e772497 --- /dev/null +++ b/reports/report_3297334.md @@ -0,0 +1,38 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-27 10:50:16 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2501 | +| n_a | 18 | +| stub | 168 | +| verified | 986 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2662 | +| n_a | 187 | +| stub | 18 | +| verified | 390 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**1593/6942 items complete (22.9%)** diff --git a/tools/NatsNet.PortTracker/Audit/AuditVerifier.cs b/tools/NatsNet.PortTracker/Audit/AuditVerifier.cs new file mode 100644 index 0000000..ffe4fcb --- /dev/null +++ b/tools/NatsNet.PortTracker/Audit/AuditVerifier.cs @@ -0,0 +1,136 @@ +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; + } + } +} 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();