Files
natsnet/docs/plans/2026-02-27-feature-audit-script-plan.md
Joseph Doherty 84dc9d1e1d docs: add feature audit script implementation plan
7 tasks: add Roslyn package, create SourceIndexer, FeatureClassifier,
AuditCommand, smoke test, execute audit, cleanup.
2026-02-27 05:12:49 -05:00

814 lines
28 KiB
Markdown

# 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<T>()`, `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
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
```
The `<ItemGroup>` should look like:
```xml
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
<PackageReference Include="System.CommandLine" Version="3.0.0-preview.1.26104.118" />
</ItemGroup>
```
**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;
/// <summary>
/// Parses .cs files using Roslyn syntax trees and builds a lookup index
/// of (className, memberName) -> list of MethodInfo.
/// </summary>
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<MethodInfo>> _index = new();
public int FilesIndexed { get; private set; }
public int MethodsIndexed { get; private set; }
/// <summary>
/// Recursively parses all .cs files under <paramref name="sourceDir"/>
/// (skipping obj/ and bin/) and populates the index.
/// </summary>
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++;
}
}
/// <summary>
/// Looks up all method declarations for a given class and member name.
/// Case-insensitive. Returns empty list if not found.
/// </summary>
public List<MethodInfo> Lookup(string className, string memberName)
{
var key = (className.ToLowerInvariant(), memberName.ToLowerInvariant());
return _index.TryGetValue(key, out var list) ? list : [];
}
/// <summary>
/// Returns true if the class exists anywhere in the index (any member).
/// </summary>
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<TypeDeclarationSyntax>())
{
var className = typeDecl.Identifier.Text.ToLowerInvariant();
// Methods
foreach (var method in typeDecl.Members.OfType<MethodDeclarationSyntax>())
{
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<PropertyDeclarationSyntax>())
{
var info = AnalyzeProperty(filePath, prop);
AddToIndex(className, prop.Identifier.Text.ToLowerInvariant(), info);
}
// Constructors — index as class name
foreach (var ctor in typeDecl.Members.OfType<ConstructorDeclarationSyntax>())
{
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;
/// <summary>
/// Classifies features by inspecting the SourceIndexer for their .NET implementation status.
/// Priority: n_a lookup → method-not-found → stub detection → verified.
/// </summary>
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<string, string> 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<FeatureRecord, bool> 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;
}
/// <summary>
/// Classify a single feature. Returns status and reason.
/// </summary>
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<string> dbOption)
{
var sourceOpt = new Option<string>("--source")
{
Description = "Path to the .NET source directory",
DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server")
};
var outputOpt = new Option<string>("--output")
{
Description = "CSV report output path",
DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results.csv")
};
var moduleOpt = new Option<int?>("--module")
{
Description = "Restrict to a specific module ID"
};
var executeOpt = new Option<bool>("--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<string>();
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
```