diff --git a/docs/plans/2026-02-27-feature-audit-script-plan.md b/docs/plans/2026-02-27-feature-audit-script-plan.md new file mode 100644 index 0000000..35524aa --- /dev/null +++ b/docs/plans/2026-02-27-feature-audit-script-plan.md @@ -0,0 +1,813 @@ +# 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 +``` diff --git a/docs/plans/2026-02-27-feature-audit-script-plan.md.tasks.json b/docs/plans/2026-02-27-feature-audit-script-plan.md.tasks.json new file mode 100644 index 0000000..c83ae94 --- /dev/null +++ b/docs/plans/2026-02-27-feature-audit-script-plan.md.tasks.json @@ -0,0 +1,13 @@ +{ + "planPath": "docs/plans/2026-02-27-feature-audit-script-plan.md", + "tasks": [ + {"id": 0, "subject": "Task 0: Add Roslyn NuGet package", "status": "pending"}, + {"id": 1, "subject": "Task 1: Create SourceIndexer", "status": "pending", "blockedBy": [0]}, + {"id": 2, "subject": "Task 2: Create FeatureClassifier", "status": "pending", "blockedBy": [1]}, + {"id": 3, "subject": "Task 3: Create AuditCommand + wire CLI", "status": "pending", "blockedBy": [2]}, + {"id": 4, "subject": "Task 4: Smoke test dry-run", "status": "pending", "blockedBy": [3]}, + {"id": 5, "subject": "Task 5: Execute audit and update DB", "status": "pending", "blockedBy": [4]}, + {"id": 6, "subject": "Task 6: Cleanup and final verification", "status": "pending", "blockedBy": [5]} + ], + "lastUpdated": "2026-02-27T00:00:00Z" +} diff --git a/reports/current.md b/reports/current.md index b65eb4a..6cdf904 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-27 10:09:38 UTC +Generated: 2026-02-27 10:12:50 UTC ## Modules (12 total) diff --git a/reports/report_60dce2d.md b/reports/report_60dce2d.md new file mode 100644 index 0000000..6cdf904 --- /dev/null +++ b/reports/report_60dce2d.md @@ -0,0 +1,35 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-27 10:12:50 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| unknown | 3394 | +| verified | 279 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2680 | +| n_a | 187 | +| verified | 390 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**868/6942 items complete (12.5%)**