# Feature Audit Script Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Add a `feature audit` command to the PortTracker CLI that uses Roslyn syntax tree analysis to automatically classify 3394 unknown features into verified/stub/n_a/deferred. **Architecture:** Three new files — `SourceIndexer` parses all .cs files and builds a method lookup index, `FeatureClassifier` applies classification heuristics, `AuditCommand` wires the CLI and orchestrates the audit. Direct DB updates via the existing `Database` class. **Tech Stack:** `Microsoft.CodeAnalysis.CSharp` (Roslyn) for C# parsing, `Microsoft.Data.Sqlite` (existing), `System.CommandLine` (existing) **Design doc:** `docs/plans/2026-02-27-feature-audit-script-design.md` --- ## Important Rules (Read Before Every Task) 1. All new files go under `tools/NatsNet.PortTracker/` 2. Follow the existing code style — see `FeatureCommands.cs` and `BatchFilters.cs` for patterns 3. Use `System.CommandLine` v3 (preview) APIs — `SetAction`, `parseResult.GetValue()`, etc. 4. The `Database` class methods: `Query()`, `Execute()`, `ExecuteScalar()`, `ExecuteInTransaction()` 5. Run `dotnet build --project tools/NatsNet.PortTracker` after each file creation to verify compilation --- ### Task 0: Add Roslyn NuGet package **Files:** - Modify: `tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` **Step 1: Add the package reference** Add `Microsoft.CodeAnalysis.CSharp` to the csproj: ```xml ``` The `` should look like: ```xml ``` **Step 2: Restore and build** Run: `dotnet build --project tools/NatsNet.PortTracker` Expected: Build succeeded. 0 Error(s). **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj git commit -m "chore: add Roslyn package to PortTracker for feature audit" ``` --- ### Task 1: Create SourceIndexer — data model and file parsing **Files:** - Create: `tools/NatsNet.PortTracker/Audit/SourceIndexer.cs` **Step 1: Create the SourceIndexer with MethodInfo record and indexing logic** Create `tools/NatsNet.PortTracker/Audit/SourceIndexer.cs`: ```csharp using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NatsNet.PortTracker.Audit; /// /// Parses .cs files using Roslyn syntax trees and builds a lookup index /// of (className, memberName) -> list of MethodInfo. /// public sealed class SourceIndexer { public record MethodInfo( string FilePath, int LineNumber, int BodyLineCount, bool IsStub, bool IsPartial, int StatementCount); // Key: (className lowercase, memberName lowercase) private readonly Dictionary<(string, string), List> _index = new(); public int FilesIndexed { get; private set; } public int MethodsIndexed { get; private set; } /// /// Recursively parses all .cs files under /// (skipping obj/ and bin/) and populates the index. /// public void IndexDirectory(string sourceDir) { var files = Directory.EnumerateFiles(sourceDir, "*.cs", SearchOption.AllDirectories) .Where(f => { var rel = Path.GetRelativePath(sourceDir, f); return !rel.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") && !rel.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}") && !rel.StartsWith($"obj{Path.DirectorySeparatorChar}") && !rel.StartsWith($"bin{Path.DirectorySeparatorChar}"); }); foreach (var file in files) { IndexFile(file); FilesIndexed++; } } /// /// Looks up all method declarations for a given class and member name. /// Case-insensitive. Returns empty list if not found. /// public List Lookup(string className, string memberName) { var key = (className.ToLowerInvariant(), memberName.ToLowerInvariant()); return _index.TryGetValue(key, out var list) ? list : []; } /// /// Returns true if the class exists anywhere in the index (any member). /// public bool HasClass(string className) { var lower = className.ToLowerInvariant(); return _index.Keys.Any(k => k.Item1 == lower); } private void IndexFile(string filePath) { var source = File.ReadAllText(filePath); var tree = CSharpSyntaxTree.ParseText(source, path: filePath); var root = tree.GetCompilationUnitRoot(); foreach (var typeDecl in root.DescendantNodes().OfType()) { var className = typeDecl.Identifier.Text.ToLowerInvariant(); // Methods foreach (var method in typeDecl.Members.OfType()) { var info = AnalyzeMethod(filePath, method.Body, method.ExpressionBody, method.GetLocation()); AddToIndex(className, method.Identifier.Text.ToLowerInvariant(), info); } // Properties (get/set are like methods) foreach (var prop in typeDecl.Members.OfType()) { var info = AnalyzeProperty(filePath, prop); AddToIndex(className, prop.Identifier.Text.ToLowerInvariant(), info); } // Constructors — index as class name foreach (var ctor in typeDecl.Members.OfType()) { var info = AnalyzeMethod(filePath, ctor.Body, ctor.ExpressionBody, ctor.GetLocation()); AddToIndex(className, ctor.Identifier.Text.ToLowerInvariant(), info); } } } private MethodInfo AnalyzeMethod(string filePath, BlockSyntax? body, ArrowExpressionClauseSyntax? expressionBody, Location location) { var lineSpan = location.GetLineSpan(); var lineNumber = lineSpan.StartLinePosition.Line + 1; if (expressionBody is not null) { // Expression-bodied: => expr; var isStub = IsNotImplementedExpression(expressionBody.Expression); return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1); } if (body is null || body.Statements.Count == 0) { // No body or empty body return new MethodInfo(filePath, lineNumber, 0, IsStub: true, IsPartial: false, StatementCount: 0); } var bodyLines = body.GetLocation().GetLineSpan(); var bodyLineCount = bodyLines.EndLinePosition.Line - bodyLines.StartLinePosition.Line - 1; // exclude braces var statements = body.Statements; var hasNotImplemented = statements.Any(s => IsNotImplementedStatement(s)); var meaningfulCount = statements.Count(s => !IsNotImplementedStatement(s)); // Pure stub: single throw NotImplementedException if (statements.Count == 1 && hasNotImplemented) return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: true, IsPartial: false, StatementCount: 0); // Partial: has some logic AND a NotImplementedException if (hasNotImplemented && meaningfulCount > 0) return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: true, StatementCount: meaningfulCount); // Real logic return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: false, StatementCount: meaningfulCount); } private MethodInfo AnalyzeProperty(string filePath, PropertyDeclarationSyntax prop) { var lineSpan = prop.GetLocation().GetLineSpan(); var lineNumber = lineSpan.StartLinePosition.Line + 1; // Expression-bodied property: int Foo => expr; if (prop.ExpressionBody is not null) { var isStub = IsNotImplementedExpression(prop.ExpressionBody.Expression); return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1); } // Auto-property: int Foo { get; set; } — this is valid, not a stub if (prop.AccessorList is not null && prop.AccessorList.Accessors.All(a => a.Body is null && a.ExpressionBody is null)) return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1); // Property with accessor bodies — check if any are stubs if (prop.AccessorList is not null) { var hasStub = prop.AccessorList.Accessors.Any(a => (a.ExpressionBody is not null && IsNotImplementedExpression(a.ExpressionBody.Expression)) || (a.Body is not null && a.Body.Statements.Count == 1 && IsNotImplementedStatement(a.Body.Statements[0]))); return new MethodInfo(filePath, lineNumber, 0, IsStub: hasStub, IsPartial: false, StatementCount: hasStub ? 0 : 1); } return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1); } private static bool IsNotImplementedExpression(ExpressionSyntax expr) { // throw new NotImplementedException(...) if (expr is ThrowExpressionSyntax throwExpr) return throwExpr.Expression is ObjectCreationExpressionSyntax oc && oc.Type.ToString().Contains("NotImplementedException"); // new NotImplementedException() — shouldn't normally be standalone but handle it return expr is ObjectCreationExpressionSyntax oc2 && oc2.Type.ToString().Contains("NotImplementedException"); } private static bool IsNotImplementedStatement(StatementSyntax stmt) { // throw new NotImplementedException(...); if (stmt is ThrowStatementSyntax throwStmt && throwStmt.Expression is not null) return throwStmt.Expression is ObjectCreationExpressionSyntax oc && oc.Type.ToString().Contains("NotImplementedException"); // Expression statement containing throw expression if (stmt is ExpressionStatementSyntax exprStmt) return IsNotImplementedExpression(exprStmt.Expression); return false; } private void AddToIndex(string className, string memberName, MethodInfo info) { var key = (className, memberName); if (!_index.TryGetValue(key, out var list)) { list = []; _index[key] = list; } list.Add(info); MethodsIndexed++; } } ``` **Step 2: Build to verify compilation** Run: `dotnet build --project tools/NatsNet.PortTracker` Expected: Build succeeded. 0 Error(s). **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Audit/SourceIndexer.cs git commit -m "feat: add SourceIndexer — Roslyn-based .NET source parser for audit" ``` --- ### Task 2: Create FeatureClassifier — classification heuristics **Files:** - Create: `tools/NatsNet.PortTracker/Audit/FeatureClassifier.cs` **Step 1: Create the FeatureClassifier with n_a lookup and heuristics** Create `tools/NatsNet.PortTracker/Audit/FeatureClassifier.cs`: ```csharp namespace NatsNet.PortTracker.Audit; /// /// Classifies features by inspecting the SourceIndexer for their .NET implementation status. /// Priority: n_a lookup → method-not-found → stub detection → verified. /// public sealed class FeatureClassifier { public record ClassificationResult(string Status, string Reason); public record FeatureRecord( long Id, string DotnetClass, string DotnetMethod, string GoFile, string GoMethod); private readonly SourceIndexer _indexer; // N/A lookup: (goMethod pattern) -> reason // Checked case-insensitively against go_method private static readonly Dictionary NaByGoMethod = new(StringComparer.OrdinalIgnoreCase) { ["Noticef"] = ".NET uses Microsoft.Extensions.Logging", ["Debugf"] = ".NET uses Microsoft.Extensions.Logging", ["Tracef"] = ".NET uses Microsoft.Extensions.Logging", ["Warnf"] = ".NET uses Microsoft.Extensions.Logging", ["Errorf"] = ".NET uses Microsoft.Extensions.Logging", ["Fatalf"] = ".NET uses Microsoft.Extensions.Logging", }; // N/A lookup: go_file + go_method patterns private static readonly List<(Func Match, string Reason)> NaPatterns = [ // Signal handling — .NET uses IHostApplicationLifetime (f => f.GoMethod.Equals("handleSignals", StringComparison.OrdinalIgnoreCase), ".NET uses IHostApplicationLifetime"), (f => f.GoMethod.Equals("processSignal", StringComparison.OrdinalIgnoreCase), ".NET uses IHostApplicationLifetime"), ]; public FeatureClassifier(SourceIndexer indexer) { _indexer = indexer; } /// /// Classify a single feature. Returns status and reason. /// public ClassificationResult Classify(FeatureRecord feature) { // 1. N/A lookup — check go_method against known patterns if (NaByGoMethod.TryGetValue(feature.GoMethod, out var naReason)) return new ClassificationResult("n_a", naReason); foreach (var (match, reason) in NaPatterns) { if (match(feature)) return new ClassificationResult("n_a", reason); } // 2. Handle comma-separated dotnet_class (e.g. "ClosedRingBuffer,ClosedClient") var classNames = feature.DotnetClass.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var methodName = feature.DotnetMethod; // Try each class name foreach (var className in classNames) { var methods = _indexer.Lookup(className, methodName); if (methods.Count > 0) { // Found the method — classify based on body analysis // Use the "best" match: prefer non-stub over stub var best = methods.OrderByDescending(m => m.StatementCount).First(); if (best.IsStub) return new ClassificationResult("stub", $"Body is throw NotImplementedException at {Path.GetFileName(best.FilePath)}:{best.LineNumber}"); if (best.IsPartial) return new ClassificationResult("stub", $"Partial implementation with NotImplementedException at {Path.GetFileName(best.FilePath)}:{best.LineNumber}"); return new ClassificationResult("verified", $"Method found with {best.StatementCount} statement(s) at {Path.GetFileName(best.FilePath)}:{best.LineNumber}"); } } // 3. Method not found — check if any class exists var anyClassFound = classNames.Any(c => _indexer.HasClass(c)); if (anyClassFound) return new ClassificationResult("deferred", "Class exists but method not found"); return new ClassificationResult("deferred", "Class not found in .NET source"); } } ``` **Step 2: Build to verify compilation** Run: `dotnet build --project tools/NatsNet.PortTracker` Expected: Build succeeded. 0 Error(s). **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Audit/FeatureClassifier.cs git commit -m "feat: add FeatureClassifier — heuristic-based feature classification" ``` --- ### Task 3: Create AuditCommand — CLI wiring and orchestration **Files:** - Create: `tools/NatsNet.PortTracker/Commands/AuditCommand.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs:36` — add `AuditCommand` to root command **Step 1: Create the AuditCommand** Create `tools/NatsNet.PortTracker/Commands/AuditCommand.cs`: ```csharp using System.CommandLine; using System.Text; using NatsNet.PortTracker.Audit; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class AuditCommand { public static Command Create(Option dbOption) { var sourceOpt = new Option("--source") { Description = "Path to the .NET source directory", DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server") }; var outputOpt = new Option("--output") { Description = "CSV report output path", DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results.csv") }; var moduleOpt = new Option("--module") { Description = "Restrict to a specific module ID" }; var executeOpt = new Option("--execute") { Description = "Apply DB updates (default: dry-run preview)", DefaultValueFactory = _ => false }; var cmd = new Command("audit", "Classify unknown features by inspecting .NET source code"); cmd.Add(sourceOpt); cmd.Add(outputOpt); cmd.Add(moduleOpt); cmd.Add(executeOpt); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var sourcePath = parseResult.GetValue(sourceOpt)!; var outputPath = parseResult.GetValue(outputOpt)!; var moduleId = parseResult.GetValue(moduleOpt); var execute = parseResult.GetValue(executeOpt); RunAudit(dbPath, sourcePath, outputPath, moduleId, execute); }); return cmd; } private static void RunAudit(string dbPath, string sourcePath, string outputPath, int? moduleId, bool execute) { // Validate source directory if (!Directory.Exists(sourcePath)) { Console.WriteLine($"Error: source directory not found: {sourcePath}"); return; } // 1. Build source index Console.WriteLine($"Parsing .NET source files in {sourcePath}..."); var indexer = new SourceIndexer(); indexer.IndexDirectory(sourcePath); Console.WriteLine($"Indexed {indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods/properties."); // 2. Query unknown features using var db = new Database(dbPath); var sql = "SELECT id, dotnet_class, dotnet_method, go_file, go_method FROM features WHERE status = 'unknown'"; var parameters = new List<(string, object?)>(); if (moduleId is not null) { sql += " AND module_id = @module"; parameters.Add(("@module", moduleId)); } sql += " ORDER BY id"; var rows = db.Query(sql, parameters.ToArray()); if (rows.Count == 0) { Console.WriteLine("No unknown features found."); return; } Console.WriteLine($"Found {rows.Count} unknown features to classify.\n"); // 3. Classify each feature var classifier = new FeatureClassifier(indexer); var results = new List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)>(); foreach (var row in rows) { var feature = 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 result = classifier.Classify(feature); results.Add((feature, result)); } // 4. Write CSV report WriteCsvReport(outputPath, results); // 5. Print console summary var grouped = results.GroupBy(r => r.Result.Status) .ToDictionary(g => g.Key, g => g.Count()); Console.WriteLine("Feature Status Audit Results"); Console.WriteLine("============================="); Console.WriteLine($"Source: {sourcePath} ({indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods indexed)"); Console.WriteLine($"Features audited: {results.Count}"); Console.WriteLine(); Console.WriteLine($" verified: {grouped.GetValueOrDefault("verified", 0)}"); Console.WriteLine($" stub: {grouped.GetValueOrDefault("stub", 0)}"); Console.WriteLine($" n_a: {grouped.GetValueOrDefault("n_a", 0)}"); Console.WriteLine($" deferred: {grouped.GetValueOrDefault("deferred", 0)}"); Console.WriteLine(); if (!execute) { Console.WriteLine("Dry-run mode. Add --execute to apply changes."); Console.WriteLine($"Report: {outputPath}"); return; } // 6. Apply DB updates ApplyUpdates(db, results); Console.WriteLine($"Report: {outputPath}"); } private static void WriteCsvReport( string outputPath, List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results) { // Ensure directory exists var dir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); var sb = new StringBuilder(); sb.AppendLine("id,dotnet_class,dotnet_method,go_file,go_method,old_status,new_status,reason"); foreach (var (feature, result) in results) { sb.AppendLine($"{feature.Id},{CsvEscape(feature.DotnetClass)},{CsvEscape(feature.DotnetMethod)},{CsvEscape(feature.GoFile)},{CsvEscape(feature.GoMethod)},unknown,{result.Status},{CsvEscape(result.Reason)}"); } File.WriteAllText(outputPath, sb.ToString()); } private static void ApplyUpdates( Database db, List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results) { // Group by (status, notes) for efficient batch updates var groups = results .GroupBy(r => (r.Result.Status, Notes: r.Result.Status == "n_a" ? r.Result.Reason : (string?)null)) .ToList(); var totalUpdated = 0; using var transaction = db.Connection.BeginTransaction(); try { foreach (var group in groups) { var ids = group.Select(r => r.Feature.Id).ToList(); var status = group.Key.Status; var notes = group.Key.Notes; // Build parameterized IN clause var placeholders = new List(); using var cmd = db.CreateCommand(""); for (var i = 0; i < ids.Count; i++) { placeholders.Add($"@id{i}"); cmd.Parameters.AddWithValue($"@id{i}", ids[i]); } cmd.Parameters.AddWithValue("@status", status); if (notes is not null) { cmd.CommandText = $"UPDATE features SET status = @status, notes = @notes WHERE id IN ({string.Join(", ", placeholders)})"; cmd.Parameters.AddWithValue("@notes", notes); } else { cmd.CommandText = $"UPDATE features SET status = @status WHERE id IN ({string.Join(", ", placeholders)})"; } cmd.Transaction = transaction; var affected = cmd.ExecuteNonQuery(); totalUpdated += affected; } transaction.Commit(); Console.WriteLine($"Updated {totalUpdated} features."); } catch { transaction.Rollback(); Console.WriteLine("Error: transaction rolled back."); throw; } } private static string CsvEscape(string value) { if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return $"\"{value.Replace("\"", "\"\"")}\""; return value; } } ``` **Step 2: Wire the command into Program.cs** In `tools/NatsNet.PortTracker/Program.cs`, add after the existing command registrations (after line 41, before `var parseResult`): Find this line: ```csharp rootCommand.Add(PhaseCommands.Create(dbOption, schemaOption)); ``` Add immediately after it: ```csharp rootCommand.Add(AuditCommand.Create(dbOption)); ``` Also add the import — but since the file uses top-level statements and already imports `NatsNet.PortTracker.Commands`, no new using is needed (AuditCommand is in the same namespace). **Step 3: Build to verify compilation** Run: `dotnet build --project tools/NatsNet.PortTracker` Expected: Build succeeded. 0 Error(s). **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/AuditCommand.cs tools/NatsNet.PortTracker/Program.cs git commit -m "feat: add audit command — orchestrates feature status classification" ``` --- ### Task 4: Smoke test — dry-run on the real database **Files:** None — testing only. **Step 1: Run the audit in dry-run mode** ```bash dotnet run --project tools/NatsNet.PortTracker -- audit --source dotnet/src/ZB.MOM.NatsNet.Server/ --db porting.db --output reports/audit-results.csv ``` Expected output similar to: ``` Parsing .NET source files in dotnet/src/ZB.MOM.NatsNet.Server/... Indexed ~92 files, ~NNNN methods/properties. Found 3394 unknown features to classify. Feature Status Audit Results ============================= Source: dotnet/src/ZB.MOM.NatsNet.Server/ (92 files, NNNN methods indexed) Features audited: 3394 verified: NNNN stub: NNNN n_a: NNNN deferred: NNNN Dry-run mode. Add --execute to apply changes. Report: reports/audit-results.csv ``` **Step 2: Inspect the CSV report** ```bash head -20 reports/audit-results.csv ``` Verify: - Header row matches: `id,dotnet_class,dotnet_method,go_file,go_method,old_status,new_status,reason` - Each row has a classification and reason - The known n_a features (Noticef, Debugf etc.) show as `n_a` **Step 3: Spot-check a few classifications** Pick 3-5 features from the CSV and manually verify: - A `verified` feature: check the .NET method has real logic - A `stub` feature: check the .NET method is `throw new NotImplementedException` - A `deferred` feature: check the class/method doesn't exist - An `n_a` feature: check it's a Go logging function If any classifications are wrong, fix the heuristics before proceeding. **Step 4: Check the counts add up** ```bash wc -l reports/audit-results.csv ``` Expected: 3395 lines (3394 data rows + 1 header). --- ### Task 5: Execute the audit and update the database **Files:** None — execution only. **Step 1: Back up the database** ```bash cp porting.db porting.db.pre-audit-backup ``` **Step 2: Run with --execute** ```bash dotnet run --project tools/NatsNet.PortTracker -- audit --source dotnet/src/ZB.MOM.NatsNet.Server/ --db porting.db --output reports/audit-results.csv --execute ``` Expected: `Updated 3394 features.` **Step 3: Verify zero unknown features remain** ```bash dotnet run --project tools/NatsNet.PortTracker -- feature list --status unknown --db porting.db ``` Expected: `Total: 0 features` **Step 4: Verify status breakdown** ```bash dotnet run --project tools/NatsNet.PortTracker -- report summary --db porting.db ``` Review the numbers match the dry-run output. **Step 5: Generate updated porting report** ```bash ./reports/generate-report.sh ``` **Step 6: Commit everything** ```bash git add porting.db reports/ tools/NatsNet.PortTracker/ git commit -m "feat: run feature status audit — classify 3394 unknown features Automated classification using Roslyn syntax tree analysis: verified: NNNN (update with actual numbers) stub: NNNN n_a: NNNN deferred: NNNN" ``` (Update the commit message with the actual numbers from the output.) --- ### Task 6: Cleanup — remove backup **Files:** None. **Step 1: Verify everything is committed and the database is correct** ```bash git status dotnet run --project tools/NatsNet.PortTracker -- feature list --status unknown --db porting.db ``` Expected: clean working tree, 0 unknown features. **Step 2: Remove the pre-audit backup** ```bash rm porting.db.pre-audit-backup ``` **Step 3: Final summary** Print: ``` Feature Status Audit Complete ============================= Total features audited: 3394 verified: NNNN stub: NNNN n_a: NNNN deferred: NNNN ```