docs: add PortTracker batch operations implementation plan
7 tasks: Database transaction helper, BatchFilters infrastructure, batch commands for feature/test/module/library, and smoke tests.
This commit is contained in:
919
docs/plans/2026-02-27-porttracker-batch-plan.md
Normal file
919
docs/plans/2026-02-27-porttracker-batch-plan.md
Normal file
@@ -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<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.
|
||||
13
docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json
Normal file
13
docs/plans/2026-02-27-porttracker-batch-plan.md.tasks.json
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
35
reports/report_97be7a2.md
Normal file
35
reports/report_97be7a2.md
Normal file
@@ -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%)**
|
||||
Reference in New Issue
Block a user