# PortTracker Batch Operations Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Add batch-update and batch-map subcommands to all PortTracker entity commands (feature, test, module, library) with shared filter infrastructure and dry-run-by-default safety. **Architecture:** A shared `BatchFilters` static class provides reusable filter options (`--ids`, `--module`, `--status`), WHERE clause building, and the dry-run/execute pattern. Each entity command file gets two new subcommands that delegate filtering and execution to `BatchFilters`. The `Database` class gets an `ExecuteInTransaction` helper. **Tech Stack:** .NET 10, System.CommandLine v3 preview, Microsoft.Data.Sqlite **Design doc:** `docs/plans/2026-02-27-porttracker-batch-design.md` --- ### Task 0: Add ExecuteInTransaction to Database **Files:** - Modify: `tools/NatsNet.PortTracker/Data/Database.cs:73` (before Dispose) **Step 1: Add the method** Add this method to `Database.cs` before the `Dispose()` method (line 73): ```csharp public int ExecuteInTransaction(string sql, params (string name, object? value)[] parameters) { using var transaction = _connection.BeginTransaction(); try { using var cmd = CreateCommand(sql); cmd.Transaction = transaction; foreach (var (name, value) in parameters) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); var affected = cmd.ExecuteNonQuery(); transaction.Commit(); return affected; } catch { transaction.Rollback(); throw; } } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Data/Database.cs git commit -m "feat(porttracker): add ExecuteInTransaction to Database" ``` --- ### Task 1: Create BatchFilters shared infrastructure **Files:** - Create: `tools/NatsNet.PortTracker/Commands/BatchFilters.cs` **Step 1: Create the file** Create `tools/NatsNet.PortTracker/Commands/BatchFilters.cs` with this content: ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class BatchFilters { public static Option IdsOption() => new("--ids") { Description = "ID range: 100-200, 1,5,10, or mixed 1-5,10,20-25" }; public static Option ModuleOption() => new("--module") { Description = "Filter by module ID" }; public static Option StatusOption() => new("--status") { Description = "Filter by current status" }; public static Option ExecuteOption() => new("--execute") { Description = "Actually apply changes (default is dry-run preview)", DefaultValueFactory = _ => false }; public static void AddFilterOptions(Command cmd, bool includeModuleFilter) { cmd.Add(IdsOption()); if (includeModuleFilter) cmd.Add(ModuleOption()); cmd.Add(StatusOption()); cmd.Add(ExecuteOption()); } public static List ParseIds(string? idsSpec) { if (string.IsNullOrWhiteSpace(idsSpec)) return []; var ids = new List(); foreach (var part in idsSpec.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { if (part.Contains('-')) { var range = part.Split('-', 2); if (int.TryParse(range[0], out var start) && int.TryParse(range[1], out var end)) { for (var i = start; i <= end; i++) ids.Add(i); } else { Console.WriteLine($"Warning: invalid range '{part}', skipping."); } } else if (int.TryParse(part, out var id)) { ids.Add(id); } else { Console.WriteLine($"Warning: invalid ID '{part}', skipping."); } } return ids; } public static (string whereClause, List<(string name, object? value)> parameters) BuildWhereClause( string? idsSpec, int? moduleId, string? status, string idColumn = "id", string moduleColumn = "module_id") { var clauses = new List(); var parameters = new List<(string name, object? value)>(); if (!string.IsNullOrWhiteSpace(idsSpec)) { var ids = ParseIds(idsSpec); if (ids.Count > 0) { var placeholders = new List(); for (var i = 0; i < ids.Count; i++) { placeholders.Add($"@id{i}"); parameters.Add(($"@id{i}", ids[i])); } clauses.Add($"{idColumn} IN ({string.Join(", ", placeholders)})"); } } if (moduleId is not null) { clauses.Add($"{moduleColumn} = @moduleFilter"); parameters.Add(("@moduleFilter", moduleId)); } if (!string.IsNullOrWhiteSpace(status)) { clauses.Add("status = @statusFilter"); parameters.Add(("@statusFilter", status)); } if (clauses.Count == 0) return ("", parameters); return (" WHERE " + string.Join(" AND ", clauses), parameters); } public static void PreviewOrExecute( Database db, string table, string displayColumns, string updateSetClause, List<(string name, object? value)> updateParams, string whereClause, List<(string name, object? value)> filterParams, bool execute) { // Count matching rows var countSql = $"SELECT COUNT(*) FROM {table}{whereClause}"; var count = db.ExecuteScalar(countSql, filterParams.ToArray()); if (count == 0) { Console.WriteLine("No items match the specified filters."); return; } // Preview var previewSql = $"SELECT {displayColumns} FROM {table}{whereClause} ORDER BY id"; var rows = db.Query(previewSql, filterParams.ToArray()); if (!execute) { Console.WriteLine($"Would affect {count} items:"); Console.WriteLine(); PrintPreviewTable(rows); Console.WriteLine(); Console.WriteLine("Add --execute to apply these changes."); return; } // Execute var allParams = new List<(string name, object? value)>(); allParams.AddRange(updateParams); allParams.AddRange(filterParams); var updateSql = $"UPDATE {table} SET {updateSetClause}{whereClause}"; var affected = db.ExecuteInTransaction(updateSql, allParams.ToArray()); Console.WriteLine($"Updated {affected} items."); } private static void PrintPreviewTable(List> rows) { if (rows.Count == 0) return; var columns = rows[0].Keys.ToList(); var widths = columns.Select(c => c.Length).ToList(); foreach (var row in rows) { for (var i = 0; i < columns.Count; i++) { var val = row[columns[i]]?.ToString() ?? ""; if (val.Length > widths[i]) widths[i] = Math.Min(val.Length, 40); } } // Header var header = string.Join(" ", columns.Select((c, i) => Truncate(c, widths[i]).PadRight(widths[i]))); Console.WriteLine(header); Console.WriteLine(new string('-', header.Length)); // Rows (cap at 50 for preview) var displayRows = rows.Take(50).ToList(); foreach (var row in displayRows) { var line = string.Join(" ", columns.Select((c, i) => Truncate(row[c]?.ToString() ?? "", widths[i]).PadRight(widths[i]))); Console.WriteLine(line); } if (rows.Count > 50) Console.WriteLine($" ... and {rows.Count - 50} more"); } private static string Truncate(string s, int maxLen) { return s.Length <= maxLen ? s : s[..(maxLen - 2)] + ".."; } } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/BatchFilters.cs git commit -m "feat(porttracker): add BatchFilters shared infrastructure" ``` --- ### Task 2: Add batch commands to FeatureCommands **Files:** - Modify: `tools/NatsNet.PortTracker/Commands/FeatureCommands.cs:169-175` **Step 1: Add batch-update and batch-map subcommands** In `FeatureCommands.cs`, insert the batch commands before the `return featureCommand;` line (line 175). Add them after the existing `featureCommand.Add(naCmd);` at line 173. Replace lines 169-175 with: ```csharp featureCommand.Add(listCmd); featureCommand.Add(showCmd); featureCommand.Add(updateCmd); featureCommand.Add(mapCmd); featureCommand.Add(naCmd); featureCommand.Add(CreateBatchUpdate(dbOption)); featureCommand.Add(CreateBatchMap(dbOption)); return featureCommand; ``` Then add these two static methods to the class (before the `Truncate` method at line 178): ```csharp private static Command CreateBatchUpdate(Option dbOption) { var cmd = new Command("batch-update", "Bulk update feature status"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) { setClauses.Add("notes = @newNotes"); updateParams.Add(("@newNotes", notes)); } BatchFilters.PreviewOrExecute(db, "features", "id, name, status, module_id, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } private static Command CreateBatchMap(Option dbOption) { var cmd = new Command("batch-map", "Bulk map features to .NET methods"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setProject = new Option("--set-project") { Description = ".NET project" }; var setClass = new Option("--set-class") { Description = ".NET class" }; var setMethod = new Option("--set-method") { Description = ".NET method" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setProject); cmd.Add(setClass); cmd.Add(setMethod); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var project = parseResult.GetValue(setProject); var cls = parseResult.GetValue(setClass); var method = parseResult.GetValue(setMethod); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } if (project is null && cls is null && method is null) { Console.WriteLine("Error: at least one of --set-project, --set-class, --set-method is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); var setClauses = new List(); var updateParams = new List<(string, object?)>(); if (project is not null) { setClauses.Add("dotnet_project = @setProject"); updateParams.Add(("@setProject", project)); } if (cls is not null) { setClauses.Add("dotnet_class = @setClass"); updateParams.Add(("@setClass", cls)); } if (method is not null) { setClauses.Add("dotnet_method = @setMethod"); updateParams.Add(("@setMethod", method)); } BatchFilters.PreviewOrExecute(db, "features", "id, name, status, dotnet_project, dotnet_class, dotnet_method", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Smoke test dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- feature batch-update --module 1 --status not_started --set-status deferred --db porting.db` Expected: Preview output showing matching features (or "No items match"). **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/FeatureCommands.cs git commit -m "feat(porttracker): add feature batch-update and batch-map commands" ``` --- ### Task 3: Add batch commands to TestCommands **Files:** - Modify: `tools/NatsNet.PortTracker/Commands/TestCommands.cs:130-135` **Step 1: Add batch-update and batch-map subcommands** In `TestCommands.cs`, replace lines 130-135 with: ```csharp testCommand.Add(listCmd); testCommand.Add(showCmd); testCommand.Add(updateCmd); testCommand.Add(mapCmd); testCommand.Add(CreateBatchUpdate(dbOption)); testCommand.Add(CreateBatchMap(dbOption)); return testCommand; ``` Then add these two static methods before the `Truncate` method (line 138): ```csharp private static Command CreateBatchUpdate(Option dbOption) { var cmd = new Command("batch-update", "Bulk update test status"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) { setClauses.Add("notes = @newNotes"); updateParams.Add(("@newNotes", notes)); } BatchFilters.PreviewOrExecute(db, "unit_tests", "id, name, status, module_id, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } private static Command CreateBatchMap(Option dbOption) { var cmd = new Command("batch-map", "Bulk map tests to .NET test methods"); var idsOpt = BatchFilters.IdsOption(); var moduleOpt = BatchFilters.ModuleOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setProject = new Option("--set-project") { Description = ".NET test project" }; var setClass = new Option("--set-class") { Description = ".NET test class" }; var setMethod = new Option("--set-method") { Description = ".NET test method" }; cmd.Add(idsOpt); cmd.Add(moduleOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setProject); cmd.Add(setClass); cmd.Add(setMethod); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var module = parseResult.GetValue(moduleOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var project = parseResult.GetValue(setProject); var cls = parseResult.GetValue(setClass); var method = parseResult.GetValue(setMethod); if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required."); return; } if (project is null && cls is null && method is null) { Console.WriteLine("Error: at least one of --set-project, --set-class, --set-method is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status); var setClauses = new List(); var updateParams = new List<(string, object?)>(); if (project is not null) { setClauses.Add("dotnet_project = @setProject"); updateParams.Add(("@setProject", project)); } if (cls is not null) { setClauses.Add("dotnet_class = @setClass"); updateParams.Add(("@setClass", cls)); } if (method is not null) { setClauses.Add("dotnet_method = @setMethod"); updateParams.Add(("@setMethod", method)); } BatchFilters.PreviewOrExecute(db, "unit_tests", "id, name, status, dotnet_project, dotnet_class, dotnet_method", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Smoke test dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- test batch-update --status not_started --set-status deferred --db porting.db` Expected: Preview output showing matching tests (or "No items match"). **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/TestCommands.cs git commit -m "feat(porttracker): add test batch-update and batch-map commands" ``` --- ### Task 4: Add batch commands to ModuleCommands **Files:** - Modify: `tools/NatsNet.PortTracker/Commands/ModuleCommands.cs:145-152` **Step 1: Add batch-update and batch-map subcommands** In `ModuleCommands.cs`, replace lines 145-152 with: ```csharp moduleCommand.Add(listCmd); moduleCommand.Add(showCmd); moduleCommand.Add(updateCmd); moduleCommand.Add(mapCmd); moduleCommand.Add(naCmd); moduleCommand.Add(CreateBatchUpdate(dbOption)); moduleCommand.Add(CreateBatchMap(dbOption)); return moduleCommand; } ``` Then add these two static methods before the closing `}` of the class: ```csharp private static Command CreateBatchUpdate(Option dbOption) { var cmd = new Command("batch-update", "Bulk update module status"); var idsOpt = BatchFilters.IdsOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Notes to set" }; cmd.Add(idsOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); if (string.IsNullOrWhiteSpace(ids) && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --status) is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, null, status); var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) { setClauses.Add("notes = @newNotes"); updateParams.Add(("@newNotes", notes)); } BatchFilters.PreviewOrExecute(db, "modules", "id, name, status, notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } private static Command CreateBatchMap(Option dbOption) { var cmd = new Command("batch-map", "Bulk map modules to .NET projects"); var idsOpt = BatchFilters.IdsOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setProject = new Option("--set-project") { Description = ".NET project" }; var setNamespace = new Option("--set-namespace") { Description = ".NET namespace" }; var setClass = new Option("--set-class") { Description = ".NET class" }; cmd.Add(idsOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setProject); cmd.Add(setNamespace); cmd.Add(setClass); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var project = parseResult.GetValue(setProject); var ns = parseResult.GetValue(setNamespace); var cls = parseResult.GetValue(setClass); if (string.IsNullOrWhiteSpace(ids) && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --status) is required."); return; } if (project is null && ns is null && cls is null) { Console.WriteLine("Error: at least one of --set-project, --set-namespace, --set-class is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, null, status); var setClauses = new List(); var updateParams = new List<(string, object?)>(); if (project is not null) { setClauses.Add("dotnet_project = @setProject"); updateParams.Add(("@setProject", project)); } if (ns is not null) { setClauses.Add("dotnet_namespace = @setNamespace"); updateParams.Add(("@setNamespace", ns)); } if (cls is not null) { setClauses.Add("dotnet_class = @setClass"); updateParams.Add(("@setClass", cls)); } BatchFilters.PreviewOrExecute(db, "modules", "id, name, status, dotnet_project, dotnet_namespace, dotnet_class", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/ModuleCommands.cs git commit -m "feat(porttracker): add module batch-update and batch-map commands" ``` --- ### Task 5: Add batch commands to LibraryCommands **Files:** - Modify: `tools/NatsNet.PortTracker/Commands/LibraryCommands.cs:86-91` **Step 1: Add batch-update and batch-map subcommands** In `LibraryCommands.cs`, replace lines 86-91 with: ```csharp libraryCommand.Add(listCmd); libraryCommand.Add(mapCmd); libraryCommand.Add(suggestCmd); libraryCommand.Add(CreateBatchUpdate(dbOption)); libraryCommand.Add(CreateBatchMap(dbOption)); return libraryCommand; } ``` Then add these two static methods before the `Truncate` method: ```csharp private static Command CreateBatchUpdate(Option dbOption) { var cmd = new Command("batch-update", "Bulk update library status"); var idsOpt = BatchFilters.IdsOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setStatus = new Option("--set-status") { Description = "New status to set", Required = true }; var setNotes = new Option("--set-notes") { Description = "Usage notes to set" }; cmd.Add(idsOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setStatus); cmd.Add(setNotes); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var newStatus = parseResult.GetValue(setStatus)!; var notes = parseResult.GetValue(setNotes); if (string.IsNullOrWhiteSpace(ids) && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --status) is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, null, status); var setClauses = new List { "status = @newStatus" }; var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) }; if (notes is not null) { setClauses.Add("dotnet_usage_notes = @newNotes"); updateParams.Add(("@newNotes", notes)); } BatchFilters.PreviewOrExecute(db, "library_mappings", "id, go_import_path, status, dotnet_usage_notes", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } private static Command CreateBatchMap(Option dbOption) { var cmd = new Command("batch-map", "Bulk map libraries to .NET packages"); var idsOpt = BatchFilters.IdsOption(); var statusOpt = BatchFilters.StatusOption(); var executeOpt = BatchFilters.ExecuteOption(); var setPackage = new Option("--set-package") { Description = ".NET NuGet package" }; var setNamespace = new Option("--set-namespace") { Description = ".NET namespace" }; var setNotes = new Option("--set-notes") { Description = "Usage notes" }; cmd.Add(idsOpt); cmd.Add(statusOpt); cmd.Add(executeOpt); cmd.Add(setPackage); cmd.Add(setNamespace); cmd.Add(setNotes); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var ids = parseResult.GetValue(idsOpt); var status = parseResult.GetValue(statusOpt); var execute = parseResult.GetValue(executeOpt); var package = parseResult.GetValue(setPackage); var ns = parseResult.GetValue(setNamespace); var notes = parseResult.GetValue(setNotes); if (string.IsNullOrWhiteSpace(ids) && string.IsNullOrWhiteSpace(status)) { Console.WriteLine("Error: at least one filter (--ids, --status) is required."); return; } if (package is null && ns is null && notes is null) { Console.WriteLine("Error: at least one of --set-package, --set-namespace, --set-notes is required."); return; } using var db = new Database(dbPath); var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, null, status); var setClauses = new List(); var updateParams = new List<(string, object?)>(); if (package is not null) { setClauses.Add("dotnet_package = @setPackage"); updateParams.Add(("@setPackage", package)); } if (ns is not null) { setClauses.Add("dotnet_namespace = @setNamespace"); updateParams.Add(("@setNamespace", ns)); } if (notes is not null) { setClauses.Add("dotnet_usage_notes = @setNotes"); updateParams.Add(("@setNotes", notes)); } BatchFilters.PreviewOrExecute(db, "library_mappings", "id, go_import_path, status, dotnet_package, dotnet_namespace", string.Join(", ", setClauses), updateParams, whereClause, filterParams, execute); }); return cmd; } ``` **Step 2: Verify it compiles** Run: `dotnet build tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` Expected: Build succeeded. **Step 3: Commit** ```bash git add tools/NatsNet.PortTracker/Commands/LibraryCommands.cs git commit -m "feat(porttracker): add library batch-update and batch-map commands" ``` --- ### Task 6: End-to-end smoke test **Files:** None — testing only. **Step 1: Test feature batch-update dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- feature batch-update --status deferred --set-status deferred --db porting.db` Expected: Preview showing deferred features. **Step 2: Test test batch-update dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- test batch-update --ids 1-5 --set-status verified --db porting.db` Expected: Preview showing tests 1-5. **Step 3: Test module batch-update dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- module batch-update --status verified --set-status verified --db porting.db` Expected: Preview showing verified modules. **Step 4: Test library batch-map dry-run** Run: `dotnet run --project tools/NatsNet.PortTracker -- library batch-map --status mapped --set-package "test" --db porting.db` Expected: Preview showing mapped libraries. **Step 5: Test error cases** Run: `dotnet run --project tools/NatsNet.PortTracker -- feature batch-update --set-status deferred --db porting.db` Expected: "Error: at least one filter (--ids, --module, --status) is required." Run: `dotnet run --project tools/NatsNet.PortTracker -- feature batch-map --ids 1-5 --db porting.db` Expected: "Error: at least one of --set-project, --set-class, --set-method is required." **Step 6: Test help output** Run: `dotnet run --project tools/NatsNet.PortTracker -- feature batch-update --help` Expected: Shows all options with descriptions. **Step 7: Final commit** No code changes — this task is verification only. If any issues found, fix and commit with appropriate message.