Add batch commands to PortTracker CLI and migrate batch tables into porting.db

Implements batch list/show/ready/next/start/complete commands with dependency
validation, migrates 42 implementation batches (2377 features, 2087 tests) from
porting_batches.db into the live tracking database, and documents the batch
workflow in AGENTS.md.
This commit is contained in:
Joseph Doherty
2026-02-27 13:02:43 -05:00
parent fe3fd7c74d
commit c5e0416793
7 changed files with 566 additions and 1 deletions

View File

@@ -92,6 +92,18 @@ dotnet run --project tools/NatsNet.PortTracker -- <command> --db porting.db
| `test update <id> --status <s>` | Update one test |
| `test batch-update --ids "1-10" --set-status <s> --execute` | Bulk update tests |
| `module update <id> --status <s>` | Update module status |
| `batch start <id>` | Mark batch as in-progress (validates deps) |
| `batch complete <id>` | Mark batch as complete (validates all items done) |
### Batch Querying
| Command | Purpose |
|---------|---------|
| `batch list` | List all implementation batches |
| `batch list --status <s>` | Filter batches by status |
| `batch show <id>` | Show batch details with features and tests |
| `batch ready` | List batches ready to start (deps met) |
| `batch next` | Show next recommended batch |
### Audit Verification
@@ -147,6 +159,38 @@ dotnet run --project tools/NatsNet.PortTracker -- test list --status stub --db p
dotnet run --project tools/NatsNet.PortTracker -- test list --status deferred --db porting.db
```
### Batch Workflow
Work is organized into 42 implementation batches with dependency ordering. Use batches to find and track work:
1. **Find the next batch**`batch next` returns the lowest-priority ready batch:
```bash
dotnet run --project tools/NatsNet.PortTracker -- batch next --db porting.db
```
2. **Start it** — marks the batch as in-progress (validates dependencies are met):
```bash
dotnet run --project tools/NatsNet.PortTracker -- batch start <id> --db porting.db
```
3. **See all items** — lists every feature and test in the batch:
```bash
dotnet run --project tools/NatsNet.PortTracker -- batch show <id> --db porting.db
```
4. **Implement features first**, then write/port the tests.
5. **Complete it** — validates all features and tests are verified/complete/n_a:
```bash
dotnet run --project tools/NatsNet.PortTracker -- batch complete <id> --db porting.db
```
The system enforces dependency ordering — you cannot start a batch until all batches it depends on are complete. Use `batch ready` to see all currently available batches.
### Implementing a Feature
1. **Claim it** — mark as stub before starting:

View File

@@ -99,6 +99,31 @@ CREATE TABLE IF NOT EXISTS status_overrides (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS implementation_batches (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
priority INTEGER NOT NULL,
feature_count INTEGER DEFAULT 0,
test_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
depends_on TEXT,
go_files TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS batch_features (
batch_id INTEGER NOT NULL REFERENCES implementation_batches(id),
feature_id INTEGER NOT NULL REFERENCES features(id),
PRIMARY KEY (batch_id, feature_id)
);
CREATE TABLE IF NOT EXISTS batch_tests (
batch_id INTEGER NOT NULL REFERENCES implementation_batches(id),
test_id INTEGER NOT NULL REFERENCES unit_tests(id),
PRIMARY KEY (batch_id, test_id)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_features_module ON features(module_id);
CREATE INDEX IF NOT EXISTS idx_features_status ON features(status);
@@ -109,6 +134,10 @@ CREATE INDEX IF NOT EXISTS idx_deps_source ON dependencies(source_type, source_i
CREATE INDEX IF NOT EXISTS idx_deps_target ON dependencies(target_type, target_id);
CREATE INDEX IF NOT EXISTS idx_library_status ON library_mappings(status);
CREATE INDEX IF NOT EXISTS idx_modules_status ON modules(status);
CREATE INDEX IF NOT EXISTS idx_batch_features_feature ON batch_features(feature_id);
CREATE INDEX IF NOT EXISTS idx_batch_tests_test ON batch_tests(test_id);
CREATE INDEX IF NOT EXISTS idx_batches_status ON implementation_batches(status);
CREATE INDEX IF NOT EXISTS idx_batches_priority ON implementation_batches(priority);
-- Triggers to auto-update updated_at
CREATE TRIGGER IF NOT EXISTS trg_modules_updated AFTER UPDATE ON modules

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
Generated: 2026-02-27 17:42:32 UTC
Generated: 2026-02-27 18:02:44 UTC
## Modules (12 total)

37
reports/report_fe3fd7c.md Normal file
View File

@@ -0,0 +1,37 @@
# NATS .NET Porting Status Report
Generated: 2026-02-27 18:02:44 UTC
## Modules (12 total)
| Status | Count |
|--------|-------|
| verified | 12 |
## Features (3673 total)
| Status | Count |
|--------|-------|
| deferred | 2377 |
| n_a | 24 |
| stub | 1 |
| verified | 1271 |
## Unit Tests (3257 total)
| Status | Count |
|--------|-------|
| deferred | 2640 |
| n_a | 187 |
| verified | 430 |
## Library Mappings (36 total)
| Status | Count |
|--------|-------|
| mapped | 36 |
## Overall Progress
**1924/6942 items complete (27.7%)**

View File

@@ -0,0 +1,454 @@
using System.CommandLine;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class BatchCommands
{
public static Command Create(Option<string> dbOption)
{
var batchCommand = new Command("batch", "Manage implementation batches");
batchCommand.Add(CreateList(dbOption));
batchCommand.Add(CreateShow(dbOption));
batchCommand.Add(CreateReady(dbOption));
batchCommand.Add(CreateNext(dbOption));
batchCommand.Add(CreateStart(dbOption));
batchCommand.Add(CreateComplete(dbOption));
return batchCommand;
}
private static Command CreateList(Option<string> dbOption)
{
var statusOpt = new Option<string?>("--status")
{
Description = "Filter by status: pending, in_progress, complete"
};
var cmd = new Command("list", "List all implementation batches");
cmd.Add(statusOpt);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var status = parseResult.GetValue(statusOpt);
using var db = new Database(dbPath);
var sql = "SELECT id, name, feature_count, test_count, status, depends_on FROM implementation_batches";
var parameters = new List<(string, object?)>();
if (status is not null)
{
sql += " WHERE status = @status";
parameters.Add(("@status", status));
}
sql += " ORDER BY priority, id";
var rows = db.Query(sql, parameters.ToArray());
if (rows.Count == 0)
{
Console.WriteLine("No batches found.");
return;
}
Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Status",-12} {"Depends On"}");
Console.WriteLine(new string('-', 100));
foreach (var row in rows)
{
Console.WriteLine(
$"{row["id"],-4} " +
$"{Truncate(row["name"]?.ToString(), 44),-45} " +
$"{row["feature_count"],-5} " +
$"{row["test_count"],-5} " +
$"{row["status"],-12} " +
$"{row["depends_on"] ?? ""}");
}
Console.WriteLine($"\nTotal: {rows.Count} batches");
});
return cmd;
}
private static Command CreateShow(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to show" };
var cmd = new Command("show", "Show batch details with features and tests");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
Console.WriteLine($"Batch {batch["id"]}: {batch["name"]}");
Console.WriteLine($" Description: {batch["description"] ?? "(none)"}");
Console.WriteLine($" Priority: {batch["priority"]}");
Console.WriteLine($" Status: {batch["status"]}");
Console.WriteLine($" Depends On: {batch["depends_on"] ?? "(none)"}");
Console.WriteLine($" Go Files: {batch["go_files"] ?? "(none)"}");
Console.WriteLine($" Features: {batch["feature_count"]}");
Console.WriteLine($" Tests: {batch["test_count"]}");
Console.WriteLine();
// Show features
var features = db.Query(
"""
SELECT f.id, f.name, f.status, f.go_file
FROM batch_features bf
JOIN features f ON f.id = bf.feature_id
WHERE bf.batch_id = @id
ORDER BY f.id
""",
("@id", id));
if (features.Count > 0)
{
Console.WriteLine($"Features ({features.Count}):");
Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}");
Console.WriteLine(" " + new string('-', 90));
foreach (var f in features)
{
Console.WriteLine(
$" {f["id"],-6} " +
$"{f["status"],-10} " +
$"{Truncate(f["name"]?.ToString(), 49),-50} " +
$"{Truncate(f["go_file"]?.ToString(), 30)}");
}
}
Console.WriteLine();
// Show tests
var tests = db.Query(
"""
SELECT t.id, t.name, t.status, t.go_file
FROM batch_tests bt
JOIN unit_tests t ON t.id = bt.test_id
WHERE bt.batch_id = @id
ORDER BY t.id
""",
("@id", id));
if (tests.Count > 0)
{
Console.WriteLine($"Tests ({tests.Count}):");
Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}");
Console.WriteLine(" " + new string('-', 90));
foreach (var t in tests)
{
Console.WriteLine(
$" {t["id"],-6} " +
$"{t["status"],-10} " +
$"{Truncate(t["name"]?.ToString(), 49),-50} " +
$"{Truncate(t["go_file"]?.ToString(), 30)}");
}
}
});
return cmd;
}
private static Command CreateReady(Option<string> dbOption)
{
var cmd = new Command("ready", "List batches ready to start (all dependencies complete)");
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var readyBatches = GetReadyBatches(db);
if (readyBatches.Count == 0)
{
Console.WriteLine("No batches ready.");
return;
}
Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Priority"}");
Console.WriteLine(new string('-', 70));
foreach (var row in readyBatches)
{
Console.WriteLine(
$"{row["id"],-4} " +
$"{Truncate(row["name"]?.ToString(), 44),-45} " +
$"{row["feature_count"],-5} " +
$"{row["test_count"],-5} " +
$"{row["priority"]}");
}
Console.WriteLine($"\n{readyBatches.Count} batches ready");
});
return cmd;
}
private static Command CreateNext(Option<string> dbOption)
{
var cmd = new Command("next", "Show the next recommended batch (lowest priority ready)");
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
using var db = new Database(dbPath);
var readyBatches = GetReadyBatches(db);
if (readyBatches.Count == 0)
{
Console.WriteLine("No batches ready.");
return;
}
var next = readyBatches[0]; // Already sorted by priority
Console.WriteLine($"Next batch: #{next["id"]} — {next["name"]}");
Console.WriteLine($" Priority: {next["priority"]}");
Console.WriteLine($" Features: {next["feature_count"]}");
Console.WriteLine($" Tests: {next["test_count"]}");
Console.WriteLine($" Depends On: {next["depends_on"] ?? "(none)"}");
Console.WriteLine($"\nRun: batch start {next["id"]}");
});
return cmd;
}
private static Command CreateStart(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to start" };
var cmd = new Command("start", "Mark batch as in-progress (validates dependencies)");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
var status = batch["status"]?.ToString();
if (status == "complete")
{
Console.WriteLine($"Batch {id} is already complete.");
return;
}
if (status == "in_progress")
{
Console.WriteLine($"Batch {id} is already in progress.");
return;
}
// Check dependencies
if (!AreDependenciesMet(db, batch))
{
var deps = batch["depends_on"]?.ToString() ?? "";
Console.WriteLine($"Cannot start batch {id}: dependencies not met.");
Console.WriteLine($" Depends on: {deps}");
PrintDependencyStatus(db, deps);
return;
}
db.Execute(
"UPDATE implementation_batches SET status = 'in_progress' WHERE id = @id",
("@id", id));
Console.WriteLine($"Batch {id} started: {batch["name"]}");
Console.WriteLine($" Features: {batch["feature_count"]}, Tests: {batch["test_count"]}");
Console.WriteLine($"\nRun: batch show {id} — to see all items");
});
return cmd;
}
private static Command CreateComplete(Option<string> dbOption)
{
var idArg = new Argument<int>("id") { Description = "Batch ID to complete" };
var cmd = new Command("complete", "Mark batch as complete (validates all items done)");
cmd.Add(idArg);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(idArg);
using var db = new Database(dbPath);
var batches = db.Query(
"SELECT * FROM implementation_batches WHERE id = @id",
("@id", id));
if (batches.Count == 0)
{
Console.WriteLine($"Batch {id} not found.");
return;
}
var batch = batches[0];
if (batch["status"]?.ToString() == "complete")
{
Console.WriteLine($"Batch {id} is already complete.");
return;
}
// Check all features are done
var incompleteFeatures = db.Query(
"""
SELECT f.id, f.name, f.status
FROM batch_features bf
JOIN features f ON f.id = bf.feature_id
WHERE bf.batch_id = @id AND f.status NOT IN ('verified', 'complete', 'n_a')
ORDER BY f.id
""",
("@id", id));
// Check all tests are done
var incompleteTests = db.Query(
"""
SELECT t.id, t.name, t.status
FROM batch_tests bt
JOIN unit_tests t ON t.id = bt.test_id
WHERE bt.batch_id = @id AND t.status NOT IN ('verified', 'complete', 'n_a')
ORDER BY t.id
""",
("@id", id));
if (incompleteFeatures.Count > 0 || incompleteTests.Count > 0)
{
Console.WriteLine($"Cannot complete batch {id}: items remain.");
if (incompleteFeatures.Count > 0)
{
Console.WriteLine($"\n Incomplete features ({incompleteFeatures.Count}):");
foreach (var f in incompleteFeatures.Take(20))
{
Console.WriteLine($" {f["id"],-6} {f["status"],-10} {Truncate(f["name"]?.ToString(), 50)}");
}
if (incompleteFeatures.Count > 20)
Console.WriteLine($" ... and {incompleteFeatures.Count - 20} more");
}
if (incompleteTests.Count > 0)
{
Console.WriteLine($"\n Incomplete tests ({incompleteTests.Count}):");
foreach (var t in incompleteTests.Take(20))
{
Console.WriteLine($" {t["id"],-6} {t["status"],-10} {Truncate(t["name"]?.ToString(), 50)}");
}
if (incompleteTests.Count > 20)
Console.WriteLine($" ... and {incompleteTests.Count - 20} more");
}
return;
}
db.Execute(
"UPDATE implementation_batches SET status = 'complete' WHERE id = @id",
("@id", id));
Console.WriteLine($"Batch {id} completed: {batch["name"]}");
});
return cmd;
}
private static List<Dictionary<string, object?>> GetReadyBatches(Database db)
{
// Get all pending batches
var pending = db.Query(
"SELECT * FROM implementation_batches WHERE status = 'pending' ORDER BY priority, id");
// Get statuses of all batches for dependency checking
var allStatuses = db.Query("SELECT id, status FROM implementation_batches");
var statusMap = new Dictionary<long, string>();
foreach (var row in allStatuses)
{
statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending";
}
var ready = new List<Dictionary<string, object?>>();
foreach (var batch in pending)
{
if (AreDependenciesMet(statusMap, batch["depends_on"]?.ToString()))
ready.Add(batch);
}
return ready;
}
private static bool AreDependenciesMet(Database db, Dictionary<string, object?> batch)
{
var depsStr = batch["depends_on"]?.ToString();
if (string.IsNullOrWhiteSpace(depsStr))
return true;
var allStatuses = db.Query("SELECT id, status FROM implementation_batches");
var statusMap = new Dictionary<long, string>();
foreach (var row in allStatuses)
{
statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending";
}
return AreDependenciesMet(statusMap, depsStr);
}
private static bool AreDependenciesMet(Dictionary<long, string> statusMap, string? depsStr)
{
if (string.IsNullOrWhiteSpace(depsStr))
return true;
var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var depIdStr in depIds)
{
if (long.TryParse(depIdStr, out var depId))
{
if (!statusMap.TryGetValue(depId, out var depStatus) || depStatus != "complete")
return false;
}
}
return true;
}
private static void PrintDependencyStatus(Database db, string depsStr)
{
if (string.IsNullOrWhiteSpace(depsStr)) return;
var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var depIdStr in depIds)
{
if (int.TryParse(depIdStr, out var depId))
{
var rows = db.Query(
"SELECT id, name, status FROM implementation_batches WHERE id = @id",
("@id", depId));
if (rows.Count > 0)
{
var r = rows[0];
Console.WriteLine($" Batch {r["id"]}: {r["status"]} — {r["name"]}");
}
}
}
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}

View File

@@ -41,6 +41,7 @@ rootCommand.Add(ReportCommands.Create(dbOption, schemaOption));
rootCommand.Add(PhaseCommands.Create(dbOption, schemaOption));
rootCommand.Add(AuditCommand.Create(dbOption));
rootCommand.Add(OverrideCommands.Create(dbOption));
rootCommand.Add(BatchCommands.Create(dbOption));
var parseResult = rootCommand.Parse(args);
return await parseResult.InvokeAsync();