From a99092d0bdef0fa4402a10307c7094a51f608f09 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 27 Feb 2026 04:37:36 -0500 Subject: [PATCH] docs: add PortTracker batch operations implementation plan 7 tasks: Database transaction helper, BatchFilters infrastructure, batch commands for feature/test/module/library, and smoke tests. --- .../2026-02-27-porttracker-batch-plan.md | 919 ++++++++++++++++++ ...02-27-porttracker-batch-plan.md.tasks.json | 13 + reports/current.md | 2 +- reports/report_97be7a2.md | 35 + 4 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-02-27-porttracker-batch-plan.md create mode 100644 docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json create mode 100644 reports/report_97be7a2.md diff --git a/docs/plans/2026-02-27-porttracker-batch-plan.md b/docs/plans/2026-02-27-porttracker-batch-plan.md new file mode 100644 index 0000000..9bd1936 --- /dev/null +++ b/docs/plans/2026-02-27-porttracker-batch-plan.md @@ -0,0 +1,919 @@ +# 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. diff --git a/docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json b/docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json new file mode 100644 index 0000000..f12035a --- /dev/null +++ b/docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json @@ -0,0 +1,13 @@ +{ + "planPath": "docs/plans/2026-02-27-porttracker-batch-plan.md", + "tasks": [ + {"id": 0, "nativeId": 7, "subject": "Task 0: Add ExecuteInTransaction to Database", "status": "pending"}, + {"id": 1, "nativeId": 8, "subject": "Task 1: Create BatchFilters shared infrastructure", "status": "pending", "blockedBy": [0]}, + {"id": 2, "nativeId": 9, "subject": "Task 2: Add batch commands to FeatureCommands", "status": "pending", "blockedBy": [1]}, + {"id": 3, "nativeId": 10, "subject": "Task 3: Add batch commands to TestCommands", "status": "pending", "blockedBy": [1]}, + {"id": 4, "nativeId": 11, "subject": "Task 4: Add batch commands to ModuleCommands", "status": "pending", "blockedBy": [1]}, + {"id": 5, "nativeId": 12, "subject": "Task 5: Add batch commands to LibraryCommands", "status": "pending", "blockedBy": [1]}, + {"id": 6, "nativeId": 13, "subject": "Task 6: End-to-end smoke test", "status": "pending", "blockedBy": [2, 3, 4, 5]} + ], + "lastUpdated": "2026-02-27T00:00:00Z" +} diff --git a/reports/current.md b/reports/current.md index 5989d9d..e196070 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-27 09:34:53 UTC +Generated: 2026-02-27 09:37:37 UTC ## Modules (12 total) diff --git a/reports/report_97be7a2.md b/reports/report_97be7a2.md new file mode 100644 index 0000000..e196070 --- /dev/null +++ b/reports/report_97be7a2.md @@ -0,0 +1,35 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-27 09:37:37 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 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%)**