7 tasks: Database transaction helper, BatchFilters infrastructure, batch commands for feature/test/module/library, and smoke tests.
920 lines
33 KiB
Markdown
920 lines
33 KiB
Markdown
# 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<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**
|
|
|
|
```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<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**
|
|
|
|
```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<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**
|
|
|
|
```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<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**
|
|
|
|
```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<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**
|
|
|
|
```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.
|