7 tasks: Database transaction helper, BatchFilters infrastructure, batch commands for feature/test/module/library, and smoke tests.
33 KiB
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):
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
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:
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class BatchFilters
{
public static Option<string?> IdsOption() => new("--ids")
{
Description = "ID range: 100-200, 1,5,10, or mixed 1-5,10,20-25"
};
public static Option<int?> ModuleOption() => new("--module")
{
Description = "Filter by module ID"
};
public static Option<string?> StatusOption() => new("--status")
{
Description = "Filter by current status"
};
public static Option<bool> 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<int> ParseIds(string? idsSpec)
{
if (string.IsNullOrWhiteSpace(idsSpec)) return [];
var ids = new List<int>();
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<string>();
var parameters = new List<(string name, object? value)>();
if (!string.IsNullOrWhiteSpace(idsSpec))
{
var ids = ParseIds(idsSpec);
if (ids.Count > 0)
{
var placeholders = new List<string>();
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<long>(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<Dictionary<string, object?>> 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
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:
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):
private static Command CreateBatchUpdate(Option<string> 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<string>("--set-status") { Description = "New status to set", Required = true };
var setNotes = new Option<string?>("--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<string> { "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<string> 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<string?>("--set-project") { Description = ".NET project" };
var setClass = new Option<string?>("--set-class") { Description = ".NET class" };
var setMethod = new Option<string?>("--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<string>();
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
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:
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):
private static Command CreateBatchUpdate(Option<string> 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<string>("--set-status") { Description = "New status to set", Required = true };
var setNotes = new Option<string?>("--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<string> { "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<string> 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<string?>("--set-project") { Description = ".NET test project" };
var setClass = new Option<string?>("--set-class") { Description = ".NET test class" };
var setMethod = new Option<string?>("--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<string>();
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
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:
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:
private static Command CreateBatchUpdate(Option<string> 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<string>("--set-status") { Description = "New status to set", Required = true };
var setNotes = new Option<string?>("--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<string> { "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<string> 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<string?>("--set-project") { Description = ".NET project" };
var setNamespace = new Option<string?>("--set-namespace") { Description = ".NET namespace" };
var setClass = new Option<string?>("--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<string>();
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
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:
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:
private static Command CreateBatchUpdate(Option<string> 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<string>("--set-status") { Description = "New status to set", Required = true };
var setNotes = new Option<string?>("--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<string> { "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<string> 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<string?>("--set-package") { Description = ".NET NuGet package" };
var setNamespace = new Option<string?>("--set-namespace") { Description = ".NET namespace" };
var setNotes = new Option<string?>("--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<string>();
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
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.