diff --git a/AGENTS.md b/AGENTS.md index 198fb29..e990b6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,18 @@ dotnet run --project tools/NatsNet.PortTracker -- --db porting.db | `test update --status ` | Update one test | | `test batch-update --ids "1-10" --set-status --execute` | Bulk update tests | | `module update --status ` | Update module status | +| `batch start ` | Mark batch as in-progress (validates deps) | +| `batch complete ` | Mark batch as complete (validates all items done) | + +### Batch Querying + +| Command | Purpose | +|---------|---------| +| `batch list` | List all implementation batches | +| `batch list --status ` | Filter batches by status | +| `batch show ` | 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 --db porting.db +``` + +3. **See all items** — lists every feature and test in the batch: + +```bash +dotnet run --project tools/NatsNet.PortTracker -- batch show --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 --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: diff --git a/porting-schema.sql b/porting-schema.sql index f016122..b026a87 100644 --- a/porting-schema.sql +++ b/porting-schema.sql @@ -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 diff --git a/porting.db b/porting.db index d7dc49e..c5e2d1c 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index accf510..df8ed34 100644 --- a/reports/current.md +++ b/reports/current.md @@ -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) diff --git a/reports/report_fe3fd7c.md b/reports/report_fe3fd7c.md new file mode 100644 index 0000000..df8ed34 --- /dev/null +++ b/reports/report_fe3fd7c.md @@ -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%)** diff --git a/tools/NatsNet.PortTracker/Commands/BatchCommands.cs b/tools/NatsNet.PortTracker/Commands/BatchCommands.cs new file mode 100644 index 0000000..8a614a8 --- /dev/null +++ b/tools/NatsNet.PortTracker/Commands/BatchCommands.cs @@ -0,0 +1,454 @@ +using System.CommandLine; +using NatsNet.PortTracker.Data; + +namespace NatsNet.PortTracker.Commands; + +public static class BatchCommands +{ + public static Command Create(Option 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 dbOption) + { + var statusOpt = new Option("--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 dbOption) + { + var idArg = new Argument("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 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 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 dbOption) + { + var idArg = new Argument("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 dbOption) + { + var idArg = new Argument("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> 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(); + foreach (var row in allStatuses) + { + statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending"; + } + + var ready = new List>(); + foreach (var batch in pending) + { + if (AreDependenciesMet(statusMap, batch["depends_on"]?.ToString())) + ready.Add(batch); + } + + return ready; + } + + private static bool AreDependenciesMet(Database db, Dictionary 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(); + foreach (var row in allStatuses) + { + statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending"; + } + + return AreDependenciesMet(statusMap, depsStr); + } + + private static bool AreDependenciesMet(Dictionary 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)] + ".."; + } +} diff --git a/tools/NatsNet.PortTracker/Program.cs b/tools/NatsNet.PortTracker/Program.cs index 49f1a8d..333f712 100644 --- a/tools/NatsNet.PortTracker/Program.cs +++ b/tools/NatsNet.PortTracker/Program.cs @@ -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();