# 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
```
