# Porting Tracker Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Build the SQLite database, Go AST analyzer, .NET PortTracker CLI, and 7 phase instruction guides for tracking the NATS server Go-to-.NET port. **Architecture:** Two tools + one DB. A Go program parses Go source via AST and populates SQLite. A .NET 10 CLI app manages the DB for all subsequent phases. Seven markdown guides provide step-by-step instructions. **Tech Stack:** Go 1.25 (go/ast, go/parser, golang.org/x/tools/go/callgraph, mattn/go-sqlite3), .NET 10 (System.CommandLine, Microsoft.Data.Sqlite), SQLite 3. --- ### Task 0: Project scaffolding and .gitignore **Files:** - Create: `porting-schema.sql` - Create: `.gitignore` - Create: `tools/go-analyzer/go.mod` - Create: `tools/NatsNet.PortTracker/NatsNet.PortTracker.csproj` **Step 1: Create .gitignore** ```bash cat > .gitignore << 'GITEOF' # SQLite database (local state) porting.db porting.db-journal porting.db-wal porting.db-shm # .NET build output tools/NatsNet.PortTracker/bin/ tools/NatsNet.PortTracker/obj/ # Go build output tools/go-analyzer/go-analyzer # OS files .DS_Store Thumbs.db GITEOF ``` **Step 2: Create SQLite schema file** ```sql -- porting-schema.sql -- Schema for NATS server Go-to-.NET porting tracker PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS modules ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, go_package TEXT, go_file TEXT, go_line_start INTEGER, go_line_count INTEGER, status TEXT NOT NULL DEFAULT 'not_started' CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a')), dotnet_project TEXT, dotnet_namespace TEXT, dotnet_class TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS features ( id INTEGER PRIMARY KEY AUTOINCREMENT, module_id INTEGER NOT NULL REFERENCES modules(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, go_file TEXT, go_class TEXT, go_method TEXT, go_line_number INTEGER, go_line_count INTEGER, status TEXT NOT NULL DEFAULT 'not_started' CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a')), dotnet_project TEXT, dotnet_class TEXT, dotnet_method TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS unit_tests ( id INTEGER PRIMARY KEY AUTOINCREMENT, module_id INTEGER NOT NULL REFERENCES modules(id) ON DELETE CASCADE, feature_id INTEGER REFERENCES features(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT, go_file TEXT, go_class TEXT, go_method TEXT, go_line_number INTEGER, go_line_count INTEGER, status TEXT NOT NULL DEFAULT 'not_started' CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a')), dotnet_project TEXT, dotnet_class TEXT, dotnet_method TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS dependencies ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_type TEXT NOT NULL CHECK (source_type IN ('module', 'feature', 'unit_test')), source_id INTEGER NOT NULL, target_type TEXT NOT NULL CHECK (target_type IN ('module', 'feature', 'unit_test')), target_id INTEGER NOT NULL, dependency_kind TEXT DEFAULT 'calls', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE (source_type, source_id, target_type, target_id) ); CREATE TABLE IF NOT EXISTS library_mappings ( id INTEGER PRIMARY KEY AUTOINCREMENT, go_import_path TEXT NOT NULL UNIQUE, go_library_name TEXT, go_usage_description TEXT, dotnet_package TEXT, dotnet_namespace TEXT, dotnet_usage_notes TEXT, status TEXT NOT NULL DEFAULT 'not_mapped' CHECK (status IN ('not_mapped', 'mapped', 'verified')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Indexes CREATE INDEX IF NOT EXISTS idx_features_module ON features(module_id); CREATE INDEX IF NOT EXISTS idx_features_status ON features(status); CREATE INDEX IF NOT EXISTS idx_unit_tests_module ON unit_tests(module_id); CREATE INDEX IF NOT EXISTS idx_unit_tests_feature ON unit_tests(feature_id); CREATE INDEX IF NOT EXISTS idx_unit_tests_status ON unit_tests(status); CREATE INDEX IF NOT EXISTS idx_deps_source ON dependencies(source_type, source_id); 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); -- Triggers to auto-update updated_at CREATE TRIGGER IF NOT EXISTS trg_modules_updated AFTER UPDATE ON modules BEGIN UPDATE modules SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; CREATE TRIGGER IF NOT EXISTS trg_features_updated AFTER UPDATE ON features BEGIN UPDATE features SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; CREATE TRIGGER IF NOT EXISTS trg_unit_tests_updated AFTER UPDATE ON unit_tests BEGIN UPDATE unit_tests SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; CREATE TRIGGER IF NOT EXISTS trg_library_mappings_updated AFTER UPDATE ON library_mappings BEGIN UPDATE library_mappings SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; ``` **Step 3: Initialize Go module** ```bash mkdir -p tools/go-analyzer cd tools/go-analyzer go mod init github.com/natsnet/go-analyzer ``` **Step 4: Initialize .NET project** ```bash mkdir -p tools/NatsNet.PortTracker cd tools/NatsNet.PortTracker dotnet new console --framework net10.0 dotnet add package Microsoft.Data.Sqlite --version 9.* dotnet add package System.CommandLine --version 2.* ``` **Step 5: Commit scaffolding** ```bash git add .gitignore porting-schema.sql tools/go-analyzer/go.mod tools/NatsNet.PortTracker/ git commit -m "scaffold: add project structure, schema, and gitignore" ``` --- ### Task 1: Go AST Analyzer — main.go (CLI entry point) **Files:** - Create: `tools/go-analyzer/main.go` **Step 1: Write main.go** This is the entry point that parses CLI flags, opens the SQLite DB, runs the analyzer, and writes results. ```go package main import ( "flag" "fmt" "log" "os" ) func main() { sourceDir := flag.String("source", "", "Path to Go source root (e.g., ../../golang/nats-server)") dbPath := flag.String("db", "", "Path to SQLite database file (e.g., ../../porting.db)") schemaPath := flag.String("schema", "", "Path to SQL schema file (e.g., ../../porting-schema.sql)") flag.Parse() if *sourceDir == "" || *dbPath == "" || *schemaPath == "" { fmt.Fprintf(os.Stderr, "Usage: go-analyzer --source --db --schema \n") flag.PrintDefaults() os.Exit(1) } // Open DB and apply schema db, err := OpenDB(*dbPath, *schemaPath) if err != nil { log.Fatalf("Failed to open database: %v", err) } defer db.Close() // Run analysis analyzer := NewAnalyzer(*sourceDir) result, err := analyzer.Analyze() if err != nil { log.Fatalf("Analysis failed: %v", err) } // Write to DB writer := NewDBWriter(db) if err := writer.WriteAll(result); err != nil { log.Fatalf("Failed to write results: %v", err) } fmt.Printf("Analysis complete:\n") fmt.Printf(" Modules: %d\n", len(result.Modules)) fmt.Printf(" Features: %d\n", result.TotalFeatures()) fmt.Printf(" Unit Tests: %d\n", result.TotalTests()) fmt.Printf(" Dependencies: %d\n", len(result.Dependencies)) fmt.Printf(" Imports: %d\n", len(result.Imports)) } ``` **Step 2: Verify it compiles (with stubs)** We need the stubs from Tasks 2-4 first, so just verify syntax: ```bash cd tools/go-analyzer go vet ./... 2>&1 || echo "Expected errors — stubs not yet written" ``` **Step 3: Commit** ```bash git add tools/go-analyzer/main.go git commit -m "feat(go-analyzer): add CLI entry point" ``` --- ### Task 2: Go AST Analyzer — types.go (data model) **Files:** - Create: `tools/go-analyzer/types.go` **Step 1: Write types.go** Defines the data structures that the analyzer produces and the DB writer consumes. ```go package main // AnalysisResult holds all extracted data from Go source analysis. type AnalysisResult struct { Modules []Module Dependencies []Dependency Imports []ImportInfo } // TotalFeatures returns the count of all features across all modules. func (r *AnalysisResult) TotalFeatures() int { count := 0 for _, m := range r.Modules { count += len(m.Features) } return count } // TotalTests returns the count of all tests across all modules. func (r *AnalysisResult) TotalTests() int { count := 0 for _, m := range r.Modules { count += len(m.Tests) } return count } // Module represents a logical grouping of Go source files. type Module struct { Name string Description string GoPackage string GoFile string // primary file or directory GoLineCount int Features []Feature Tests []TestFunc } // Feature represents a function or method extracted from Go source. type Feature struct { Name string Description string GoFile string GoClass string // receiver type, empty for package-level functions GoMethod string GoLineNumber int GoLineCount int } // TestFunc represents a test function extracted from Go source. type TestFunc struct { Name string Description string GoFile string GoClass string GoMethod string GoLineNumber int GoLineCount int // FeatureName links this test to a feature by naming convention FeatureName string } // Dependency represents a call relationship between two items. type Dependency struct { SourceModule string SourceFeature string // empty for module-level deps TargetModule string TargetFeature string // empty for module-level deps DependencyKind string // "calls" } // ImportInfo represents a Go import path found in source files. type ImportInfo struct { ImportPath string IsStdlib bool UsedInFiles []string } ``` **Step 2: Verify compilation** ```bash cd tools/go-analyzer && go build ./... 2>&1 || echo "Expected — other files not yet written" ``` **Step 3: Commit** ```bash git add tools/go-analyzer/types.go git commit -m "feat(go-analyzer): add data model types" ``` --- ### Task 3: Go AST Analyzer — analyzer.go (AST parsing + call graph) **Files:** - Create: `tools/go-analyzer/analyzer.go` **Step 1: Write analyzer.go** This is the core analysis engine. It walks Go source files using `go/ast` and `go/parser` to extract functions, methods, structs, and imports. It uses `golang.org/x/tools/go/packages` for type-checked call graph analysis. ```go package main import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "sort" "strings" ) // Analyzer parses Go source code and extracts structural information. type Analyzer struct { sourceDir string fset *token.FileSet } // NewAnalyzer creates a new Analyzer for the given source directory. func NewAnalyzer(sourceDir string) *Analyzer { return &Analyzer{ sourceDir: sourceDir, fset: token.NewFileSet(), } } // Analyze runs the full analysis pipeline. func (a *Analyzer) Analyze() (*AnalysisResult, error) { serverDir := filepath.Join(a.sourceDir, "server") // 1. Discover all Go files grouped by directory fileGroups, err := a.discoverFiles(serverDir) if err != nil { return nil, fmt.Errorf("discovering files: %w", err) } // 2. Parse each group into modules result := &AnalysisResult{} allImports := make(map[string]*ImportInfo) for dir, files := range fileGroups { module, imports, err := a.parseModule(dir, files) if err != nil { return nil, fmt.Errorf("parsing module %s: %w", dir, err) } result.Modules = append(result.Modules, *module) for _, imp := range imports { if existing, ok := allImports[imp.ImportPath]; ok { existing.UsedInFiles = append(existing.UsedInFiles, imp.UsedInFiles...) } else { allImports[imp.ImportPath] = &imp } } } // 3. Build module-level dependencies from import analysis result.Dependencies = a.buildDependencies(result.Modules) // 4. Collect imports for _, imp := range allImports { result.Imports = append(result.Imports, *imp) } sort.Slice(result.Imports, func(i, j int) bool { return result.Imports[i].ImportPath < result.Imports[j].ImportPath }) // Sort modules by name sort.Slice(result.Modules, func(i, j int) bool { return result.Modules[i].Name < result.Modules[j].Name }) return result, nil } // discoverFiles walks the source tree and groups .go files by directory. func (a *Analyzer) discoverFiles(root string) (map[string][]string, error) { groups := make(map[string][]string) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip non-Go files, configs directories if info.IsDir() { if info.Name() == "configs" || info.Name() == "testdata" { return filepath.SkipDir } return nil } if !strings.HasSuffix(info.Name(), ".go") { return nil } dir := filepath.Dir(path) groups[dir] = append(groups[dir], path) return nil }) return groups, err } // parseModule parses all Go files in a directory into a Module. func (a *Analyzer) parseModule(dir string, files []string) (*Module, []ImportInfo, error) { moduleName := a.moduleNameFromDir(dir) module := &Module{ Name: moduleName, GoPackage: moduleName, GoFile: dir, } var sourceFiles []string var testFiles []string for _, f := range files { if strings.HasSuffix(f, "_test.go") { testFiles = append(testFiles, f) } else { sourceFiles = append(sourceFiles, f) } } var allImports []ImportInfo totalLines := 0 // Parse source files for _, f := range sourceFiles { features, imports, lines, err := a.parseSourceFile(f) if err != nil { fmt.Fprintf(os.Stderr, "Warning: skipping %s: %v\n", f, err) continue } module.Features = append(module.Features, features...) allImports = append(allImports, imports...) totalLines += lines } // Parse test files for _, f := range testFiles { tests, _, lines, err := a.parseTestFile(f) if err != nil { fmt.Fprintf(os.Stderr, "Warning: skipping test %s: %v\n", f, err) continue } module.Tests = append(module.Tests, tests...) totalLines += lines } module.GoLineCount = totalLines return module, allImports, nil } // parseSourceFile extracts functions, methods, and imports from a Go source file. func (a *Analyzer) parseSourceFile(filePath string) ([]Feature, []ImportInfo, int, error) { src, err := os.ReadFile(filePath) if err != nil { return nil, nil, 0, err } file, err := parser.ParseFile(a.fset, filePath, src, parser.ParseComments) if err != nil { return nil, nil, 0, err } lines := strings.Count(string(src), "\n") + 1 relPath := a.relPath(filePath) var features []Feature var imports []ImportInfo // Extract imports for _, imp := range file.Imports { path := strings.Trim(imp.Path.Value, "\"") imports = append(imports, ImportInfo{ ImportPath: path, IsStdlib: isStdlib(path), UsedInFiles: []string{relPath}, }) } // Extract functions and methods for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } feature := Feature{ Name: fn.Name.Name, GoFile: relPath, GoMethod: fn.Name.Name, GoLineNumber: a.fset.Position(fn.Pos()).Line, } // Calculate line count for this function startLine := a.fset.Position(fn.Pos()).Line endLine := a.fset.Position(fn.End()).Line feature.GoLineCount = endLine - startLine + 1 // If it's a method, extract receiver type if fn.Recv != nil && len(fn.Recv.List) > 0 { feature.GoClass = a.receiverTypeName(fn.Recv.List[0].Type) feature.Name = feature.GoClass + "." + fn.Name.Name } // Build description from doc comment if fn.Doc != nil { feature.Description = strings.TrimSpace(fn.Doc.Text()) } features = append(features, feature) } return features, imports, lines, nil } // parseTestFile extracts test functions from a Go test file. func (a *Analyzer) parseTestFile(filePath string) ([]TestFunc, []ImportInfo, int, error) { src, err := os.ReadFile(filePath) if err != nil { return nil, nil, 0, err } file, err := parser.ParseFile(a.fset, filePath, src, parser.ParseComments) if err != nil { return nil, nil, 0, err } lines := strings.Count(string(src), "\n") + 1 relPath := a.relPath(filePath) var tests []TestFunc var imports []ImportInfo for _, imp := range file.Imports { path := strings.Trim(imp.Path.Value, "\"") imports = append(imports, ImportInfo{ ImportPath: path, IsStdlib: isStdlib(path), UsedInFiles: []string{relPath}, }) } for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } name := fn.Name.Name if !strings.HasPrefix(name, "Test") && !strings.HasPrefix(name, "Benchmark") { continue } startLine := a.fset.Position(fn.Pos()).Line endLine := a.fset.Position(fn.End()).Line test := TestFunc{ Name: name, GoFile: relPath, GoMethod: name, GoLineNumber: startLine, GoLineCount: endLine - startLine + 1, } if fn.Doc != nil { test.Description = strings.TrimSpace(fn.Doc.Text()) } // Try to link to a feature by naming convention: // TestFoo -> Foo, TestServer_Foo -> Server.Foo test.FeatureName = a.inferFeatureName(name) tests = append(tests, test) } return tests, imports, lines, nil } // buildDependencies creates module-level dependencies based on cross-package imports. func (a *Analyzer) buildDependencies(modules []Module) []Dependency { // Map package names to module names pkgToModule := make(map[string]string) for _, m := range modules { pkgToModule[m.GoPackage] = m.Name } // For now, build module-level dependencies based on directory structure. // Cross-file function calls within the same package are tracked at feature level. var deps []Dependency // Subdirectory packages are dependencies of the main server package for _, m := range modules { if m.Name != "server" && m.GoPackage != "server" { deps = append(deps, Dependency{ SourceModule: "server", TargetModule: m.Name, DependencyKind: "calls", }) } } return deps } // moduleNameFromDir converts a directory path to a module name. func (a *Analyzer) moduleNameFromDir(dir string) string { // If it's the server root directory, use "server" base := filepath.Base(dir) if base == "server" { return "server" } // For subdirectories, use the subdirectory name return base } // relPath returns a path relative to the analyzer's source directory. func (a *Analyzer) relPath(absPath string) string { rel, err := filepath.Rel(a.sourceDir, absPath) if err != nil { return absPath } return rel } // receiverTypeName extracts the type name from a method receiver. func (a *Analyzer) receiverTypeName(expr ast.Expr) string { switch t := expr.(type) { case *ast.StarExpr: return a.receiverTypeName(t.X) case *ast.Ident: return t.Name default: return "" } } // inferFeatureName attempts to derive a feature name from a test name. // TestFoo -> Foo, TestServer_Foo -> Server.Foo, TestFoo_Bar -> Foo_Bar func (a *Analyzer) inferFeatureName(testName string) string { name := testName for _, prefix := range []string{"Test", "Benchmark"} { if strings.HasPrefix(name, prefix) { name = strings.TrimPrefix(name, prefix) break } } if name == "" { return "" } // Replace first underscore with dot for struct.Method convention if idx := strings.Index(name, "_"); idx > 0 { name = name[:idx] + "." + name[idx+1:] } return name } // isStdlib checks if an import path is a Go standard library package. func isStdlib(importPath string) bool { // Stdlib packages don't contain dots in first path element firstSlash := strings.Index(importPath, "/") var first string if firstSlash < 0 { first = importPath } else { first = importPath[:firstSlash] } return !strings.Contains(first, ".") } ``` **Step 2: Add Go dependencies** ```bash cd tools/go-analyzer go mod tidy ``` **Step 3: Verify compilation** ```bash cd tools/go-analyzer && go build ./... 2>&1 || echo "Expected — sqlite.go not yet written" ``` **Step 4: Commit** ```bash git add tools/go-analyzer/analyzer.go git commit -m "feat(go-analyzer): add AST parsing and analysis engine" ``` --- ### Task 4: Go AST Analyzer — grouper.go (file-to-module grouping) **Files:** - Create: `tools/go-analyzer/grouper.go` **Step 1: Write grouper.go** Handles the logic for grouping related Go files into logical modules (e.g., `jetstream.go` + `jetstream_api.go` + `jetstream_cluster.go` -> "jetstream"). ```go package main import ( "path/filepath" "sort" "strings" ) // ModuleGrouper groups Go source files into logical modules. type ModuleGrouper struct { // Prefixes maps a file prefix to a module name. // Files starting with this prefix are grouped together. Prefixes map[string]string } // DefaultGrouper creates a grouper with default prefix rules for nats-server. func DefaultGrouper() *ModuleGrouper { return &ModuleGrouper{ Prefixes: map[string]string{ "jetstream": "jetstream", "consumer": "jetstream", "stream": "jetstream", "store": "jetstream", "filestore": "jetstream", "memstore": "jetstream", "raft": "raft", "gateway": "gateway", "leafnode": "leafnode", "route": "route", "client": "client", "client_proxyproto": "client", "server": "core", "service": "core", "signal": "core", "reload": "core", "opts": "config", "auth": "auth", "auth_callout": "auth", "jwt": "auth", "nkey": "auth", "accounts": "accounts", "ocsp": "tls", "ocsp_peer": "tls", "ocsp_responsecache": "tls", "ciphersuites": "tls", "parser": "protocol", "proto": "protocol", "sublist": "subscriptions", "subject_transform": "subscriptions", "monitor": "monitoring", "monitor_sort_opts": "monitoring", "mqtt": "mqtt", "websocket": "websocket", "events": "events", "msgtrace": "events", "log": "logging", "errors": "errors", "errors_gen": "errors", "const": "core", "util": "core", "ring": "core", "sendq": "core", "ipqueue": "core", "rate_counter": "core", "scheduler": "core", "sdm": "core", "dirstore": "core", "disk_avail": "core", "elastic": "core", }, } } // GroupFiles takes a flat list of Go files and returns them grouped by module name. func (g *ModuleGrouper) GroupFiles(files []string) map[string][]string { groups := make(map[string][]string) for _, f := range files { base := filepath.Base(f) base = strings.TrimSuffix(base, ".go") base = strings.TrimSuffix(base, "_test") // Remove platform suffixes for _, suffix := range []string{"_windows", "_linux", "_darwin", "_bsd", "_solaris", "_wasm", "_netbsd", "_openbsd", "_dragonfly", "_zos", "_other"} { base = strings.TrimSuffix(base, suffix) } module := g.classify(base) groups[module] = append(groups[module], f) } // Sort files within each group for k := range groups { sort.Strings(groups[k]) } return groups } // classify determines which module a file belongs to based on its base name. func (g *ModuleGrouper) classify(baseName string) string { // Exact match first if module, ok := g.Prefixes[baseName]; ok { return module } // Prefix match (longest prefix wins) bestMatch := "" bestModule := "core" // default for prefix, module := range g.Prefixes { if strings.HasPrefix(baseName, prefix) && len(prefix) > len(bestMatch) { bestMatch = prefix bestModule = module } } return bestModule } ``` **Step 2: Commit** ```bash git add tools/go-analyzer/grouper.go git commit -m "feat(go-analyzer): add file-to-module grouping logic" ``` --- ### Task 5: Go AST Analyzer — sqlite.go (database writer) **Files:** - Create: `tools/go-analyzer/sqlite.go` **Step 1: Write sqlite.go** Handles opening the database, applying the schema, and writing analysis results. ```go package main import ( "database/sql" "fmt" "os" _ "github.com/mattn/go-sqlite3" ) // OpenDB opens or creates the SQLite database and applies the schema. func OpenDB(dbPath, schemaPath string) (*sql.DB, error) { db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=ON") if err != nil { return nil, fmt.Errorf("opening database: %w", err) } schema, err := os.ReadFile(schemaPath) if err != nil { return nil, fmt.Errorf("reading schema: %w", err) } if _, err := db.Exec(string(schema)); err != nil { return nil, fmt.Errorf("applying schema: %w", err) } return db, nil } // DBWriter writes analysis results to the SQLite database. type DBWriter struct { db *sql.DB } // NewDBWriter creates a new DBWriter. func NewDBWriter(db *sql.DB) *DBWriter { return &DBWriter{db: db} } // WriteAll writes all analysis results to the database in a single transaction. func (w *DBWriter) WriteAll(result *AnalysisResult) error { tx, err := w.db.Begin() if err != nil { return fmt.Errorf("beginning transaction: %w", err) } defer tx.Rollback() // Track module name -> DB id for dependency resolution moduleIDs := make(map[string]int64) // Track "module:feature" -> DB id for test linking featureIDs := make(map[string]int64) // 1. Insert modules and their features/tests for _, mod := range result.Modules { modID, err := w.insertModule(tx, &mod) if err != nil { return fmt.Errorf("inserting module %s: %w", mod.Name, err) } moduleIDs[mod.Name] = modID for _, feat := range mod.Features { featID, err := w.insertFeature(tx, modID, &feat) if err != nil { return fmt.Errorf("inserting feature %s: %w", feat.Name, err) } featureIDs[mod.Name+":"+feat.Name] = featID } for _, test := range mod.Tests { var featureID *int64 if test.FeatureName != "" { if fid, ok := featureIDs[mod.Name+":"+test.FeatureName]; ok { featureID = &fid } } if err := w.insertTest(tx, modID, featureID, &test); err != nil { return fmt.Errorf("inserting test %s: %w", test.Name, err) } } } // 2. Insert dependencies for _, dep := range result.Dependencies { sourceID, ok := moduleIDs[dep.SourceModule] if !ok { continue } targetID, ok := moduleIDs[dep.TargetModule] if !ok { continue } if err := w.insertDependency(tx, "module", sourceID, "module", targetID, dep.DependencyKind); err != nil { return fmt.Errorf("inserting dependency %s->%s: %w", dep.SourceModule, dep.TargetModule, err) } } // 3. Insert library mappings (Go-side only) for _, imp := range result.Imports { if imp.IsStdlib { continue // Skip stdlib for now; they can be added manually } if err := w.insertLibrary(tx, &imp); err != nil { return fmt.Errorf("inserting library %s: %w", imp.ImportPath, err) } } return tx.Commit() } func (w *DBWriter) insertModule(tx *sql.Tx, mod *Module) (int64, error) { res, err := tx.Exec( `INSERT INTO modules (name, description, go_package, go_file, go_line_count, status) VALUES (?, ?, ?, ?, ?, 'not_started')`, mod.Name, mod.Description, mod.GoPackage, mod.GoFile, mod.GoLineCount, ) if err != nil { return 0, err } return res.LastInsertId() } func (w *DBWriter) insertFeature(tx *sql.Tx, moduleID int64, feat *Feature) (int64, error) { res, err := tx.Exec( `INSERT INTO features (module_id, name, description, go_file, go_class, go_method, go_line_number, go_line_count, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'not_started')`, moduleID, feat.Name, feat.Description, feat.GoFile, feat.GoClass, feat.GoMethod, feat.GoLineNumber, feat.GoLineCount, ) if err != nil { return 0, err } return res.LastInsertId() } func (w *DBWriter) insertTest(tx *sql.Tx, moduleID int64, featureID *int64, test *TestFunc) error { _, err := tx.Exec( `INSERT INTO unit_tests (module_id, feature_id, name, description, go_file, go_class, go_method, go_line_number, go_line_count, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'not_started')`, moduleID, featureID, test.Name, test.Description, test.GoFile, test.GoClass, test.GoMethod, test.GoLineNumber, test.GoLineCount, ) return err } func (w *DBWriter) insertDependency(tx *sql.Tx, srcType string, srcID int64, tgtType string, tgtID int64, kind string) error { _, err := tx.Exec( `INSERT OR IGNORE INTO dependencies (source_type, source_id, target_type, target_id, dependency_kind) VALUES (?, ?, ?, ?, ?)`, srcType, srcID, tgtType, tgtID, kind, ) return err } func (w *DBWriter) insertLibrary(tx *sql.Tx, imp *ImportInfo) error { _, err := tx.Exec( `INSERT OR IGNORE INTO library_mappings (go_import_path, go_library_name, status) VALUES (?, ?, 'not_mapped')`, imp.ImportPath, imp.ImportPath, ) return err } ``` **Step 2: Add sqlite3 dependency** ```bash cd tools/go-analyzer go get github.com/mattn/go-sqlite3 go mod tidy ``` **Step 3: Build the full analyzer** ```bash cd tools/go-analyzer && go build -o go-analyzer . ``` Expected: successful build. **Step 4: Run against the nats-server source** ```bash cd tools/go-analyzer ./go-analyzer --source ../../golang/nats-server --db ../../porting.db --schema ../../porting-schema.sql ``` Expected output: counts of modules, features, tests, dependencies, imports. **Step 5: Verify data in SQLite** ```bash sqlite3 ../../porting.db "SELECT name, go_line_count FROM modules ORDER BY go_line_count DESC LIMIT 10;" sqlite3 ../../porting.db "SELECT COUNT(*) FROM features;" sqlite3 ../../porting.db "SELECT COUNT(*) FROM unit_tests;" sqlite3 ../../porting.db "SELECT COUNT(*) FROM library_mappings;" ``` **Step 6: Commit** ```bash git add tools/go-analyzer/sqlite.go tools/go-analyzer/go.mod tools/go-analyzer/go.sum git commit -m "feat(go-analyzer): add SQLite writer, complete analyzer pipeline" ``` --- ### Task 6: .NET PortTracker — Project setup and DB access layer **Files:** - Create: `tools/NatsNet.PortTracker/Data/Database.cs` - Create: `tools/NatsNet.PortTracker/Data/Schema.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write Database.cs — connection management** ```csharp using Microsoft.Data.Sqlite; namespace NatsNet.PortTracker.Data; public sealed class Database : IDisposable { private readonly SqliteConnection _connection; public Database(string dbPath) { var connectionString = new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = SqliteOpenMode.ReadWriteCreate, ForeignKeys = true }.ToString(); _connection = new SqliteConnection(connectionString); _connection.Open(); // Enable WAL mode using var cmd = _connection.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode=WAL;"; cmd.ExecuteNonQuery(); } public SqliteConnection Connection => _connection; public SqliteCommand CreateCommand(string sql) { var cmd = _connection.CreateCommand(); cmd.CommandText = sql; return cmd; } public int Execute(string sql, params (string name, object? value)[] parameters) { using var cmd = CreateCommand(sql); foreach (var (name, value) in parameters) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); return cmd.ExecuteNonQuery(); } public T? ExecuteScalar(string sql, params (string name, object? value)[] parameters) { using var cmd = CreateCommand(sql); foreach (var (name, value) in parameters) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); var result = cmd.ExecuteScalar(); if (result is null or DBNull) return default; return (T)Convert.ChangeType(result, typeof(T)); } public List> Query(string sql, params (string name, object? value)[] parameters) { using var cmd = CreateCommand(sql); foreach (var (name, value) in parameters) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); var results = new List>(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var row = new Dictionary(); for (int i = 0; i < reader.FieldCount; i++) { row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); } results.Add(row); } return results; } public void Dispose() { _connection.Dispose(); } } ``` **Step 2: Write Schema.cs — schema initialization from embedded SQL** ```csharp namespace NatsNet.PortTracker.Data; public static class Schema { public static void Initialize(Database db, string schemaPath) { var sql = File.ReadAllText(schemaPath); db.Execute(sql); } } ``` **Step 3: Write minimal Program.cs with init command** ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; var dbOption = new Option( "--db", getDefaultValue: () => Path.Combine(Directory.GetCurrentDirectory(), "porting.db"), description: "Path to the SQLite database file"); var schemaOption = new Option( "--schema", getDefaultValue: () => Path.Combine(Directory.GetCurrentDirectory(), "porting-schema.sql"), description: "Path to the SQL schema file"); var rootCommand = new RootCommand("NATS .NET Porting Tracker"); rootCommand.AddGlobalOption(dbOption); rootCommand.AddGlobalOption(schemaOption); // init command var initCommand = new Command("init", "Create or reset the database schema"); initCommand.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); Schema.Initialize(db, schemaPath); Console.WriteLine($"Database initialized at {dbPath}"); }, dbOption, schemaOption); rootCommand.AddCommand(initCommand); return await rootCommand.InvokeAsync(args); ``` **Step 4: Build and test init command** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- init --db ../../porting.db --schema ../../porting-schema.sql ``` Expected: "Database initialized at ../../porting.db" **Step 5: Commit** ```bash git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add project scaffolding, DB layer, and init command" ``` --- ### Task 7: .NET PortTracker — Module commands (list, show, update, map, set-na) **Files:** - Create: `tools/NatsNet.PortTracker/Commands/ModuleCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write ModuleCommands.cs** ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class ModuleCommands { public static Command Create(Option dbOption, Option schemaOption) { var moduleCommand = new Command("module", "Manage modules"); // list var listStatus = new Option("--status", "Filter by status"); var listCmd = new Command("list", "List modules"); listCmd.AddOption(listStatus); listCmd.SetHandler((string dbPath, string schemaPath, string? status) => { using var db = new Database(dbPath); var sql = "SELECT id, name, status, go_package, go_line_count, dotnet_project, dotnet_class FROM modules"; var parameters = new List<(string, object?)>(); if (status is not null) { sql += " WHERE status = @status"; parameters.Add(("@status", status)); } sql += " ORDER BY name"; var rows = db.Query(sql, parameters.ToArray()); Console.WriteLine($"{"ID",-5} {"Name",-25} {"Status",-15} {"Go Pkg",-15} {"LOC",-8} {"DotNet Project",-25} {"DotNet Class",-20}"); Console.WriteLine(new string('-', 113)); foreach (var row in rows) { Console.WriteLine($"{row["id"],-5} {row["name"],-25} {row["status"],-15} {row["go_package"],-15} {row["go_line_count"],-8} {row["dotnet_project"] ?? "",-25} {row["dotnet_class"] ?? "",-20}"); } Console.WriteLine($"\nTotal: {rows.Count} modules"); }, dbOption, schemaOption, listStatus); // show var showId = new Argument("id", "Module ID"); var showCmd = new Command("show", "Show module details"); showCmd.AddArgument(showId); showCmd.SetHandler((string dbPath, string schemaPath, int id) => { using var db = new Database(dbPath); var modules = db.Query("SELECT * FROM modules WHERE id = @id", ("@id", id)); if (modules.Count == 0) { Console.WriteLine($"Module {id} not found."); return; } var mod = modules[0]; Console.WriteLine($"Module #{mod["id"]}: {mod["name"]}"); Console.WriteLine($" Status: {mod["status"]}"); Console.WriteLine($" Go Package: {mod["go_package"]}"); Console.WriteLine($" Go File: {mod["go_file"]}"); Console.WriteLine($" Go LOC: {mod["go_line_count"]}"); Console.WriteLine($" .NET: {mod["dotnet_project"]} / {mod["dotnet_namespace"]} / {mod["dotnet_class"]}"); Console.WriteLine($" Notes: {mod["notes"]}"); var features = db.Query( "SELECT id, name, status, go_method, dotnet_method FROM features WHERE module_id = @id ORDER BY name", ("@id", id)); Console.WriteLine($"\n Features ({features.Count}):"); foreach (var f in features) Console.WriteLine($" #{f["id"],-5} {f["name"],-35} {f["status"],-15} {f["dotnet_method"] ?? ""}"); var tests = db.Query( "SELECT id, name, status, dotnet_method FROM unit_tests WHERE module_id = @id ORDER BY name", ("@id", id)); Console.WriteLine($"\n Tests ({tests.Count}):"); foreach (var t in tests) Console.WriteLine($" #{t["id"],-5} {t["name"],-35} {t["status"],-15} {t["dotnet_method"] ?? ""}"); var deps = db.Query( "SELECT d.target_type, d.target_id, d.dependency_kind, m.name as target_name FROM dependencies d LEFT JOIN modules m ON d.target_type = 'module' AND d.target_id = m.id WHERE d.source_type = 'module' AND d.source_id = @id", ("@id", id)); Console.WriteLine($"\n Dependencies ({deps.Count}):"); foreach (var d in deps) Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} ({d["target_name"]}) [{d["dependency_kind"]}]"); }, dbOption, schemaOption, showId); // update var updateId = new Argument("id", "Module ID"); var updateStatus = new Option("--status", "New status") { IsRequired = true }; var updateCmd = new Command("update", "Update module status"); updateCmd.AddArgument(updateId); updateCmd.AddOption(updateStatus); updateCmd.SetHandler((string dbPath, string schemaPath, int id, string status) => { using var db = new Database(dbPath); var affected = db.Execute("UPDATE modules SET status = @status WHERE id = @id", ("@status", status), ("@id", id)); Console.WriteLine(affected > 0 ? $"Module {id} updated to '{status}'." : $"Module {id} not found."); }, dbOption, schemaOption, updateId, updateStatus); // map var mapId = new Argument("id", "Module ID"); var mapProject = new Option("--project", "Target .NET project") { IsRequired = true }; var mapNamespace = new Option("--namespace", "Target namespace"); var mapClass = new Option("--class", "Target class"); var mapCmd = new Command("map", "Map module to .NET project"); mapCmd.AddArgument(mapId); mapCmd.AddOption(mapProject); mapCmd.AddOption(mapNamespace); mapCmd.AddOption(mapClass); mapCmd.SetHandler((string dbPath, string schemaPath, int id, string project, string? ns, string? cls) => { using var db = new Database(dbPath); var affected = db.Execute( "UPDATE modules SET dotnet_project = @project, dotnet_namespace = @ns, dotnet_class = @cls WHERE id = @id", ("@project", project), ("@ns", ns), ("@cls", cls), ("@id", id)); Console.WriteLine(affected > 0 ? $"Module {id} mapped to {project}." : $"Module {id} not found."); }, dbOption, schemaOption, mapId, mapProject, mapNamespace, mapClass); // set-na var naId = new Argument("id", "Module ID"); var naReason = new Option("--reason", "Reason for N/A") { IsRequired = true }; var naCmd = new Command("set-na", "Mark module as N/A"); naCmd.AddArgument(naId); naCmd.AddOption(naReason); naCmd.SetHandler((string dbPath, string schemaPath, int id, string reason) => { using var db = new Database(dbPath); var affected = db.Execute( "UPDATE modules SET status = 'n_a', notes = @reason WHERE id = @id", ("@reason", reason), ("@id", id)); Console.WriteLine(affected > 0 ? $"Module {id} set to N/A: {reason}" : $"Module {id} not found."); }, dbOption, schemaOption, naId, naReason); moduleCommand.AddCommand(listCmd); moduleCommand.AddCommand(showCmd); moduleCommand.AddCommand(updateCmd); moduleCommand.AddCommand(mapCmd); moduleCommand.AddCommand(naCmd); return moduleCommand; } } ``` **Step 2: Register in Program.cs** Add after the init command registration: ```csharp rootCommand.AddCommand(ModuleCommands.Create(dbOption, schemaOption)); ``` **Step 3: Build and test** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- module list --db ../../porting.db dotnet run -- module show 1 --db ../../porting.db ``` **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add module commands (list, show, update, map, set-na)" ``` --- ### Task 8: .NET PortTracker — Feature commands **Files:** - Create: `tools/NatsNet.PortTracker/Commands/FeatureCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write FeatureCommands.cs** Same pattern as ModuleCommands but for the `features` table. Includes `--module` filter on list, and `--all-in-module` batch update. ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class FeatureCommands { public static Command Create(Option dbOption, Option schemaOption) { var featureCommand = new Command("feature", "Manage features"); // list var listModule = new Option("--module", "Filter by module ID"); var listStatus = new Option("--status", "Filter by status"); var listCmd = new Command("list", "List features"); listCmd.AddOption(listModule); listCmd.AddOption(listStatus); listCmd.SetHandler((string dbPath, string schemaPath, int? moduleId, string? status) => { using var db = new Database(dbPath); var sql = "SELECT f.id, f.name, f.status, f.go_file, f.go_method, f.go_line_count, f.dotnet_class, f.dotnet_method, m.name as module_name FROM features f JOIN modules m ON f.module_id = m.id WHERE 1=1"; var parameters = new List<(string, object?)>(); if (moduleId is not null) { sql += " AND f.module_id = @moduleId"; parameters.Add(("@moduleId", moduleId)); } if (status is not null) { sql += " AND f.status = @status"; parameters.Add(("@status", status)); } sql += " ORDER BY m.name, f.name"; var rows = db.Query(sql, parameters.ToArray()); Console.WriteLine($"{"ID",-6} {"Module",-18} {"Name",-35} {"Status",-13} {"Go LOC",-8} {"DotNet",-30}"); Console.WriteLine(new string('-', 110)); foreach (var row in rows) { var dotnet = row["dotnet_class"] is not null ? $"{row["dotnet_class"]}.{row["dotnet_method"]}" : ""; Console.WriteLine($"{row["id"],-6} {row["module_name"],-18} {Truncate(row["name"]?.ToString(), 35),-35} {row["status"],-13} {row["go_line_count"],-8} {Truncate(dotnet, 30),-30}"); } Console.WriteLine($"\nTotal: {rows.Count} features"); }, dbOption, schemaOption, listModule, listStatus); // show var showId = new Argument("id", "Feature ID"); var showCmd = new Command("show", "Show feature details"); showCmd.AddArgument(showId); showCmd.SetHandler((string dbPath, string schemaPath, int id) => { using var db = new Database(dbPath); var rows = db.Query("SELECT f.*, m.name as module_name FROM features f JOIN modules m ON f.module_id = m.id WHERE f.id = @id", ("@id", id)); if (rows.Count == 0) { Console.WriteLine($"Feature {id} not found."); return; } var f = rows[0]; Console.WriteLine($"Feature #{f["id"]}: {f["name"]}"); Console.WriteLine($" Module: {f["module_name"]} (#{f["module_id"]})"); Console.WriteLine($" Status: {f["status"]}"); Console.WriteLine($" Go: {f["go_file"]}:{f["go_line_number"]} ({f["go_class"]}.{f["go_method"]}, {f["go_line_count"]} lines)"); Console.WriteLine($" .NET: {f["dotnet_project"]} / {f["dotnet_class"]}.{f["dotnet_method"]}"); Console.WriteLine($" Notes: {f["notes"]}"); }, dbOption, schemaOption, showId); // update var updateId = new Argument("id", "Feature ID"); var updateStatus = new Option("--status", "New status") { IsRequired = true }; var updateAllInModule = new Option("--all-in-module", "Update all features in this module"); var updateCmd = new Command("update", "Update feature status"); updateCmd.AddArgument(updateId); updateCmd.AddOption(updateStatus); updateCmd.AddOption(updateAllInModule); updateCmd.SetHandler((string dbPath, string schemaPath, int id, string status, int? allInModule) => { using var db = new Database(dbPath); if (allInModule is not null) { var affected = db.Execute("UPDATE features SET status = @status WHERE module_id = @mid", ("@status", status), ("@mid", allInModule)); Console.WriteLine($"Updated {affected} features in module {allInModule} to '{status}'."); } else { var affected = db.Execute("UPDATE features SET status = @status WHERE id = @id", ("@status", status), ("@id", id)); Console.WriteLine(affected > 0 ? $"Feature {id} updated to '{status}'." : $"Feature {id} not found."); } }, dbOption, schemaOption, updateId, updateStatus, updateAllInModule); // map var mapId = new Argument("id", "Feature ID"); var mapProject = new Option("--project", "Target .NET project") { IsRequired = true }; var mapClass = new Option("--class", "Target class") { IsRequired = true }; var mapMethod = new Option("--method", "Target method"); var mapCmd = new Command("map", "Map feature to .NET class/method"); mapCmd.AddArgument(mapId); mapCmd.AddOption(mapProject); mapCmd.AddOption(mapClass); mapCmd.AddOption(mapMethod); mapCmd.SetHandler((string dbPath, string schemaPath, int id, string project, string cls, string? method) => { using var db = new Database(dbPath); var affected = db.Execute( "UPDATE features SET dotnet_project = @project, dotnet_class = @cls, dotnet_method = @method WHERE id = @id", ("@project", project), ("@cls", cls), ("@method", method), ("@id", id)); Console.WriteLine(affected > 0 ? $"Feature {id} mapped to {cls}.{method}." : $"Feature {id} not found."); }, dbOption, schemaOption, mapId, mapProject, mapClass, mapMethod); // set-na var naId = new Argument("id", "Feature ID"); var naReason = new Option("--reason", "Reason for N/A") { IsRequired = true }; var naCmd = new Command("set-na", "Mark feature as N/A"); naCmd.AddArgument(naId); naCmd.AddOption(naReason); naCmd.SetHandler((string dbPath, string schemaPath, int id, string reason) => { using var db = new Database(dbPath); var affected = db.Execute("UPDATE features SET status = 'n_a', notes = @reason WHERE id = @id", ("@reason", reason), ("@id", id)); Console.WriteLine(affected > 0 ? $"Feature {id} set to N/A: {reason}" : $"Feature {id} not found."); }, dbOption, schemaOption, naId, naReason); featureCommand.AddCommand(listCmd); featureCommand.AddCommand(showCmd); featureCommand.AddCommand(updateCmd); featureCommand.AddCommand(mapCmd); featureCommand.AddCommand(naCmd); return featureCommand; } private static string Truncate(string? s, int maxLen) => s is null ? "" : s.Length <= maxLen ? s : s[..(maxLen - 3)] + "..."; } ``` **Step 2: Register in Program.cs** ```csharp rootCommand.AddCommand(FeatureCommands.Create(dbOption, schemaOption)); ``` **Step 3: Build and test** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- feature list --db ../../porting.db ``` **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add feature commands (list, show, update, map, set-na)" ``` --- ### Task 9: .NET PortTracker — Test commands **Files:** - Create: `tools/NatsNet.PortTracker/Commands/TestCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` Same pattern as FeatureCommands but for `unit_tests` table. Includes `list`, `show`, `update`, `map`. No `set-na` for tests (they follow their feature's status). Follow the exact same structure as FeatureCommands, replacing table/column references for `unit_tests`. Register with `rootCommand.AddCommand(TestCommands.Create(dbOption, schemaOption));`. **Step 1: Write TestCommands.cs (same pattern as FeatureCommands)** **Step 2: Register in Program.cs** **Step 3: Build and test** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- test list --db ../../porting.db ``` **Step 4: Commit** ```bash git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add test commands (list, show, update, map)" ``` --- ### Task 10: .NET PortTracker — Library commands **Files:** - Create: `tools/NatsNet.PortTracker/Commands/LibraryCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write LibraryCommands.cs** Commands: `list`, `map`, `suggest`. The `suggest` command shows all unmapped libraries. ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class LibraryCommands { public static Command Create(Option dbOption, Option schemaOption) { var libCommand = new Command("library", "Manage library mappings"); // list var listStatus = new Option("--status", "Filter by status"); var listCmd = new Command("list", "List library mappings"); listCmd.AddOption(listStatus); listCmd.SetHandler((string dbPath, string schemaPath, string? status) => { using var db = new Database(dbPath); var sql = "SELECT id, go_import_path, go_library_name, dotnet_package, dotnet_namespace, status FROM library_mappings"; var parameters = new List<(string, object?)>(); if (status is not null) { sql += " WHERE status = @status"; parameters.Add(("@status", status)); } sql += " ORDER BY go_import_path"; var rows = db.Query(sql, parameters.ToArray()); Console.WriteLine($"{"ID",-5} {"Go Import",-45} {"Status",-12} {"DotNet Package",-30} {"DotNet Namespace",-25}"); Console.WriteLine(new string('-', 117)); foreach (var row in rows) Console.WriteLine($"{row["id"],-5} {row["go_import_path"],-45} {row["status"],-12} {row["dotnet_package"] ?? "",-30} {row["dotnet_namespace"] ?? "",-25}"); Console.WriteLine($"\nTotal: {rows.Count} libraries"); }, dbOption, schemaOption, listStatus); // map var mapId = new Argument("id", "Library mapping ID"); var mapPackage = new Option("--package", "NuGet package or BCL") { IsRequired = true }; var mapNamespace = new Option("--namespace", "Target namespace"); var mapNotes = new Option("--notes", "Usage notes"); var mapCmd = new Command("map", "Map Go library to .NET equivalent"); mapCmd.AddArgument(mapId); mapCmd.AddOption(mapPackage); mapCmd.AddOption(mapNamespace); mapCmd.AddOption(mapNotes); mapCmd.SetHandler((string dbPath, string schemaPath, int id, string package_, string? ns, string? notes) => { using var db = new Database(dbPath); var affected = db.Execute( "UPDATE library_mappings SET dotnet_package = @pkg, dotnet_namespace = @ns, dotnet_usage_notes = @notes, status = 'mapped' WHERE id = @id", ("@pkg", package_), ("@ns", ns), ("@notes", notes), ("@id", id)); Console.WriteLine(affected > 0 ? $"Library {id} mapped to {package_}." : $"Library {id} not found."); }, dbOption, schemaOption, mapId, mapPackage, mapNamespace, mapNotes); // suggest var suggestCmd = new Command("suggest", "Show unmapped libraries"); suggestCmd.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); var rows = db.Query("SELECT id, go_import_path, go_library_name FROM library_mappings WHERE status = 'not_mapped' ORDER BY go_import_path"); if (rows.Count == 0) { Console.WriteLine("All libraries are mapped!"); return; } Console.WriteLine($"Unmapped libraries ({rows.Count}):\n"); foreach (var row in rows) Console.WriteLine($" #{row["id"],-5} {row["go_import_path"]}"); }, dbOption, schemaOption); libCommand.AddCommand(listCmd); libCommand.AddCommand(mapCmd); libCommand.AddCommand(suggestCmd); return libCommand; } } ``` **Step 2: Register in Program.cs** **Step 3: Build, test, commit** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- library list --db ../../porting.db dotnet run -- library suggest --db ../../porting.db git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add library commands (list, map, suggest)" ``` --- ### Task 11: .NET PortTracker — Dependency commands **Files:** - Create: `tools/NatsNet.PortTracker/Commands/DependencyCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write DependencyCommands.cs** Commands: `show `, `blocked`, `ready`. The `ready` command is the most important — it queries for items whose dependencies are ALL at status `complete`, `verified`, or `n_a`, meaning they can be worked on. The `blocked` command shows items that have at least one dependency still at `not_started` or `stub`. ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class DependencyCommands { public static Command Create(Option dbOption, Option schemaOption) { var depCommand = new Command("dependency", "Manage dependencies"); // show var showType = new Argument("type", "Item type: module, feature, or unit_test"); var showId = new Argument("id", "Item ID"); var showCmd = new Command("show", "Show dependencies for an item"); showCmd.AddArgument(showType); showCmd.AddArgument(showId); showCmd.SetHandler((string dbPath, string schemaPath, string type, int id) => { using var db = new Database(dbPath); // Items this depends on var deps = db.Query( "SELECT target_type, target_id, dependency_kind FROM dependencies WHERE source_type = @type AND source_id = @id", ("@type", type), ("@id", id)); Console.WriteLine($"Dependencies of {type} #{id} ({deps.Count}):"); foreach (var d in deps) { var targetName = ResolveItemName(db, d["target_type"]!.ToString()!, Convert.ToInt32(d["target_id"])); Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} ({targetName}) [{d["dependency_kind"]}]"); } // Items that depend on this var rdeps = db.Query( "SELECT source_type, source_id, dependency_kind FROM dependencies WHERE target_type = @type AND target_id = @id", ("@type", type), ("@id", id)); Console.WriteLine($"\nDepended on by ({rdeps.Count}):"); foreach (var d in rdeps) { var srcName = ResolveItemName(db, d["source_type"]!.ToString()!, Convert.ToInt32(d["source_id"])); Console.WriteLine($" <- {d["source_type"]} #{d["source_id"]} ({srcName}) [{d["dependency_kind"]}]"); } }, dbOption, schemaOption, showType, showId); // blocked var blockedCmd = new Command("blocked", "Show items with unported dependencies"); blockedCmd.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); // Modules that have at least one dependency not yet complete/verified/n_a var sql = @" SELECT DISTINCT d.source_type, d.source_id, CASE d.source_type WHEN 'module' THEN (SELECT name FROM modules WHERE id = d.source_id) WHEN 'feature' THEN (SELECT name FROM features WHERE id = d.source_id) WHEN 'unit_test' THEN (SELECT name FROM unit_tests WHERE id = d.source_id) END as name, COUNT(*) as blocking_count FROM dependencies d WHERE EXISTS ( SELECT 1 FROM ( SELECT 'module' as type, id, status FROM modules UNION ALL SELECT 'feature', id, status FROM features UNION ALL SELECT 'unit_test', id, status FROM unit_tests ) items WHERE items.type = d.target_type AND items.id = d.target_id AND items.status NOT IN ('complete', 'verified', 'n_a') ) GROUP BY d.source_type, d.source_id ORDER BY d.source_type, blocking_count DESC"; var rows = db.Query(sql); Console.WriteLine($"Blocked items ({rows.Count}):\n"); Console.WriteLine($"{"Type",-12} {"ID",-6} {"Name",-35} {"Blocking Deps",-15}"); Console.WriteLine(new string('-', 68)); foreach (var row in rows) Console.WriteLine($"{row["source_type"],-12} {row["source_id"],-6} {row["name"],-35} {row["blocking_count"],-15}"); }, dbOption, schemaOption); // ready var readyCmd = new Command("ready", "Show items ready to port (all deps satisfied)"); readyCmd.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); // Items with status not_started or stub whose ALL dependencies are complete/verified/n_a // (or items with no dependencies at all) var sql = @" SELECT 'module' as type, id, name, status FROM modules WHERE status IN ('not_started', 'stub') AND NOT EXISTS ( SELECT 1 FROM dependencies d JOIN ( SELECT 'module' as type, id, status FROM modules UNION ALL SELECT 'feature', id, status FROM features UNION ALL SELECT 'unit_test', id, status FROM unit_tests ) items ON items.type = d.target_type AND items.id = d.target_id WHERE d.source_type = 'module' AND d.source_id = modules.id AND items.status NOT IN ('complete', 'verified', 'n_a') ) UNION ALL SELECT 'feature', id, name, status FROM features WHERE status IN ('not_started', 'stub') AND NOT EXISTS ( SELECT 1 FROM dependencies d JOIN ( SELECT 'module' as type, id, status FROM modules UNION ALL SELECT 'feature', id, status FROM features UNION ALL SELECT 'unit_test', id, status FROM unit_tests ) items ON items.type = d.target_type AND items.id = d.target_id WHERE d.source_type = 'feature' AND d.source_id = features.id AND items.status NOT IN ('complete', 'verified', 'n_a') ) ORDER BY type, name"; var rows = db.Query(sql); Console.WriteLine($"Items ready to port ({rows.Count}):\n"); Console.WriteLine($"{"Type",-12} {"ID",-6} {"Name",-40} {"Status",-15}"); Console.WriteLine(new string('-', 73)); foreach (var row in rows) Console.WriteLine($"{row["type"],-12} {row["id"],-6} {row["name"],-40} {row["status"],-15}"); }, dbOption, schemaOption); depCommand.AddCommand(showCmd); depCommand.AddCommand(blockedCmd); depCommand.AddCommand(readyCmd); return depCommand; } private static string ResolveItemName(Database db, string type, int id) { var table = type switch { "module" => "modules", "feature" => "features", "unit_test" => "unit_tests", _ => "modules" }; return db.ExecuteScalar($"SELECT name FROM {table} WHERE id = @id", ("@id", id)) ?? "?"; } } ``` **Step 2: Register in Program.cs** **Step 3: Build, test, commit** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- dependency ready --db ../../porting.db dotnet run -- dependency blocked --db ../../porting.db git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add dependency commands (show, blocked, ready)" ``` --- ### Task 12: .NET PortTracker — Report commands **Files:** - Create: `tools/NatsNet.PortTracker/Reporting/ReportGenerator.cs` - Create: `tools/NatsNet.PortTracker/Commands/ReportCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write ReportGenerator.cs** ```csharp using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Reporting; public class ReportGenerator { private readonly Database _db; public ReportGenerator(Database db) => _db = db; public void PrintSummary() { PrintTableSummary("Modules", "modules"); PrintTableSummary("Features", "features"); PrintTableSummary("Unit Tests", "unit_tests"); PrintTableSummary("Libraries", "library_mappings"); } private void PrintTableSummary(string label, string table) { var statusCol = table == "library_mappings" ? "status" : "status"; var rows = _db.Query($"SELECT {statusCol} as status, COUNT(*) as count FROM {table} GROUP BY {statusCol} ORDER BY {statusCol}"); var total = rows.Sum(r => Convert.ToInt32(r["count"])); Console.WriteLine($"\n{label} ({total} total):"); foreach (var row in rows) { var count = Convert.ToInt32(row["count"]); var pct = total > 0 ? (count * 100.0 / total).ToString("F1") : "0.0"; Console.WriteLine($" {row["status"],-15} {count,6} ({pct}%)"); } } public string ExportMarkdown() { var sb = new System.Text.StringBuilder(); sb.AppendLine("# Porting Status Report"); sb.AppendLine($"\nGenerated: {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC\n"); AppendMarkdownSection(sb, "Modules", "modules"); AppendMarkdownSection(sb, "Features", "features"); AppendMarkdownSection(sb, "Unit Tests", "unit_tests"); AppendMarkdownSection(sb, "Library Mappings", "library_mappings"); // Per-module breakdown sb.AppendLine("\n## Per-Module Breakdown\n"); sb.AppendLine("| Module | Features | Tests | Status |"); sb.AppendLine("|--------|----------|-------|--------|"); var modules = _db.Query("SELECT id, name, status FROM modules ORDER BY name"); foreach (var mod in modules) { var fCount = _db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE module_id = @id", ("@id", mod["id"]!)); var tCount = _db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE module_id = @id", ("@id", mod["id"]!)); sb.AppendLine($"| {mod["name"]} | {fCount} | {tCount} | {mod["status"]} |"); } return sb.ToString(); } private void AppendMarkdownSection(System.Text.StringBuilder sb, string label, string table) { sb.AppendLine($"\n## {label}\n"); sb.AppendLine("| Status | Count | % |"); sb.AppendLine("|--------|-------|---|"); var rows = _db.Query($"SELECT status, COUNT(*) as count FROM {table} GROUP BY status ORDER BY status"); var total = rows.Sum(r => Convert.ToInt32(r["count"])); foreach (var row in rows) { var count = Convert.ToInt32(row["count"]); var pct = total > 0 ? (count * 100.0 / total).ToString("F1") : "0.0"; sb.AppendLine($"| {row["status"]} | {count} | {pct}% |"); } sb.AppendLine($"| **Total** | **{total}** | |"); } } ``` **Step 2: Write ReportCommands.cs** ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; using NatsNet.PortTracker.Reporting; namespace NatsNet.PortTracker.Commands; public static class ReportCommands { public static Command Create(Option dbOption, Option schemaOption) { var reportCommand = new Command("report", "Generate reports"); // summary var summaryCmd = new Command("summary", "Show overall progress"); summaryCmd.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); var report = new ReportGenerator(db); report.PrintSummary(); }, dbOption, schemaOption); // export var exportFormat = new Option("--format", getDefaultValue: () => "md", "Export format (md)"); var exportOutput = new Option("--output", "Output file path (stdout if not set)"); var exportCmd = new Command("export", "Export status report"); exportCmd.AddOption(exportFormat); exportCmd.AddOption(exportOutput); exportCmd.SetHandler((string dbPath, string schemaPath, string format, string? output) => { using var db = new Database(dbPath); var report = new ReportGenerator(db); var md = report.ExportMarkdown(); if (output is not null) { File.WriteAllText(output, md); Console.WriteLine($"Report exported to {output}"); } else { Console.Write(md); } }, dbOption, schemaOption, exportFormat, exportOutput); reportCommand.AddCommand(summaryCmd); reportCommand.AddCommand(exportCmd); return reportCommand; } } ``` **Step 3: Register in Program.cs, build, test, commit** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- report summary --db ../../porting.db git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add report commands (summary, export)" ``` --- ### Task 13: .NET PortTracker — Phase commands **Files:** - Create: `tools/NatsNet.PortTracker/Commands/PhaseCommands.cs` - Modify: `tools/NatsNet.PortTracker/Program.cs` **Step 1: Write PhaseCommands.cs** The `list` command shows all 7 phases with a calculated status. The `check` command runs verification queries for a specific phase. ```csharp using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class PhaseCommands { private static readonly (int Number, string Name, string Description)[] Phases = [ (1, "Decomposition", "Break down Go codebase into modules, features, tests, dependencies"), (2, "Verification", "Verify all items captured and dependencies populated"), (3, "Library Mapping", "Map Go libraries to .NET equivalents"), (4, ".NET Design", "Design .NET solution and map items"), (5, "Mapping Verification", "Verify all Go items mapped to .NET"), (6, "Porting", "Port Go code to .NET"), (7, "Port Verification", "Verify ported code via targeted testing"), ]; public static Command Create(Option dbOption, Option schemaOption) { var phaseCommand = new Command("phase", "Manage phases"); // list var listCmd = new Command("list", "Show all phases and their status"); listCmd.SetHandler((string dbPath, string schemaPath) => { using var db = new Database(dbPath); Console.WriteLine($"{"#",-4} {"Phase",-25} {"Status",-15} {"Description"}"); Console.WriteLine(new string('-', 85)); foreach (var (num, name, desc) in Phases) { var status = GetPhaseStatus(db, num); Console.WriteLine($"{num,-4} {name,-25} {status,-15} {desc}"); } }, dbOption, schemaOption); // check var checkNum = new Argument("phase", "Phase number (1-7)"); var checkCmd = new Command("check", "Run verification for a phase"); checkCmd.AddArgument(checkNum); checkCmd.SetHandler((string dbPath, string schemaPath, int phase) => { using var db = new Database(dbPath); RunPhaseCheck(db, phase); }, dbOption, schemaOption, checkNum); phaseCommand.AddCommand(listCmd); phaseCommand.AddCommand(checkCmd); return phaseCommand; } private static string GetPhaseStatus(Database db, int phase) { return phase switch { 1 => db.ExecuteScalar("SELECT COUNT(*) FROM modules") > 0 ? "done" : "pending", 2 => db.ExecuteScalar("SELECT COUNT(*) FROM modules") > 0 ? "ready" : "blocked", 3 => db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings WHERE status = 'not_mapped'") == 0 && db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings") > 0 ? "done" : "pending", 4 => db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE dotnet_project IS NULL AND status != 'n_a'") == 0 && db.ExecuteScalar("SELECT COUNT(*) FROM modules") > 0 ? "done" : "pending", 5 => db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE dotnet_class IS NULL AND status != 'n_a'") == 0 && db.ExecuteScalar("SELECT COUNT(*) FROM features") > 0 ? "done" : "pending", 6 => db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')") == 0 && db.ExecuteScalar("SELECT COUNT(*) FROM features") > 0 ? "done" : "pending", 7 => db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status != 'verified' AND status != 'n_a'") == 0 && db.ExecuteScalar("SELECT COUNT(*) FROM features") > 0 ? "done" : "pending", _ => "unknown" }; } private static void RunPhaseCheck(Database db, int phase) { Console.WriteLine($"Phase {phase} verification:\n"); switch (phase) { case 1: var mods = db.ExecuteScalar("SELECT COUNT(*) FROM modules"); var feats = db.ExecuteScalar("SELECT COUNT(*) FROM features"); var tests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests"); var deps = db.ExecuteScalar("SELECT COUNT(*) FROM dependencies"); Console.WriteLine($" Modules: {mods}"); Console.WriteLine($" Features: {feats}"); Console.WriteLine($" Unit Tests: {tests}"); Console.WriteLine($" Dependencies: {deps}"); Console.WriteLine(mods > 0 && feats > 0 ? "\n PASS: Data populated" : "\n FAIL: Missing data"); break; case 2: var orphanFeatures = db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE module_id NOT IN (SELECT id FROM modules)"); var orphanTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE module_id NOT IN (SELECT id FROM modules)"); Console.WriteLine($" Orphaned features: {orphanFeatures}"); Console.WriteLine($" Orphaned tests: {orphanTests}"); Console.WriteLine(orphanFeatures == 0 && orphanTests == 0 ? "\n PASS: No orphans" : "\n FAIL: Orphaned items found"); break; case 3: var unmapped = db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings WHERE status = 'not_mapped'"); var total = db.ExecuteScalar("SELECT COUNT(*) FROM library_mappings"); Console.WriteLine($" Total libraries: {total}"); Console.WriteLine($" Unmapped: {unmapped}"); Console.WriteLine(unmapped == 0 ? "\n PASS: All libraries mapped" : $"\n FAIL: {unmapped} libraries unmapped"); break; case 4: case 5: var unmappedMods = db.ExecuteScalar("SELECT COUNT(*) FROM modules WHERE dotnet_project IS NULL AND status != 'n_a'"); var unmappedFeats = db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE dotnet_class IS NULL AND status != 'n_a'"); var unmappedTests = db.ExecuteScalar("SELECT COUNT(*) FROM unit_tests WHERE dotnet_class IS NULL AND status != 'n_a'"); Console.WriteLine($" Unmapped modules: {unmappedMods}"); Console.WriteLine($" Unmapped features: {unmappedFeats}"); Console.WriteLine($" Unmapped tests: {unmappedTests}"); var allMapped = unmappedMods == 0 && unmappedFeats == 0 && unmappedTests == 0; Console.WriteLine(allMapped ? "\n PASS: All items mapped" : "\n FAIL: Unmapped items remain"); break; case 6: var notPortedF = db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status NOT IN ('complete', 'verified', 'n_a')"); var totalF = db.ExecuteScalar("SELECT COUNT(*) FROM features"); Console.WriteLine($" Features not ported: {notPortedF} / {totalF}"); Console.WriteLine(notPortedF == 0 ? "\n PASS: All features ported" : $"\n FAIL: {notPortedF} features remaining"); break; case 7: var notVerified = db.ExecuteScalar("SELECT COUNT(*) FROM features WHERE status NOT IN ('verified', 'n_a')"); var totalV = db.ExecuteScalar("SELECT COUNT(*) FROM features"); Console.WriteLine($" Features not verified: {notVerified} / {totalV}"); Console.WriteLine(notVerified == 0 ? "\n PASS: All features verified" : $"\n FAIL: {notVerified} features not verified"); break; default: Console.WriteLine($" Unknown phase: {phase}"); break; } } } ``` **Step 2: Register in Program.cs, build, test, commit** ```bash cd tools/NatsNet.PortTracker && dotnet build dotnet run -- phase list --db ../../porting.db dotnet run -- phase check 1 --db ../../porting.db git add tools/NatsNet.PortTracker/ git commit -m "feat(porttracker): add phase commands (list, check)" ``` --- ### Task 14: Write Phase 1-3 instruction documents **Files:** - Create: `docs/plans/phases/phase-1-decomposition.md` - Create: `docs/plans/phases/phase-2-verification.md` - Create: `docs/plans/phases/phase-3-library-mapping.md` **Step 1: Write phase-1-decomposition.md** Full step-by-step guide for breaking down the Go codebase. Includes exact commands to run, expected output patterns, and troubleshooting. Key sections: - Prerequisites (Go toolchain, SQLite, built tools) - Step-by-step: init DB, run analyzer, review modules, spot-check features, verify tests, review deps - Verification: `porttracker phase check 1` - Troubleshooting: common issues with Go parsing **Step 2: Write phase-2-verification.md** Cross-checking captured data against baselines. Includes exact shell commands for counting functions/files in Go source to compare against DB counts. **Step 3: Write phase-3-library-mapping.md** Guide for mapping each Go import to .NET. Includes a reference table of common Go stdlib -> .NET BCL mappings as a starting point. **Step 4: Commit** ```bash git add docs/plans/phases/ git commit -m "docs: add phase 1-3 instruction guides" ``` --- ### Task 15: Write Phase 4-7 instruction documents **Files:** - Create: `docs/plans/phases/phase-4-dotnet-design.md` - Create: `docs/plans/phases/phase-5-mapping-verification.md` - Create: `docs/plans/phases/phase-6-porting.md` - Create: `docs/plans/phases/phase-7-porting-verification.md` **Step 1: Write phase-4-dotnet-design.md** Guide for designing the .NET solution structure, mapping modules/features/tests. References `documentation_rules.md` for naming conventions. **Step 2: Write phase-5-mapping-verification.md** Verification checklist: all items mapped or N/A with justification, naming validated, no collisions. **Step 3: Write phase-6-porting.md** Detailed porting workflow: dependency-ordered work, DB update discipline, commit patterns. Includes exact `porttracker` commands for the workflow loop. **Step 4: Write phase-7-porting-verification.md** Targeted testing guide: per-module test execution using `dotnet test --filter`, status update flow, behavioral comparison approach. **Step 5: Commit** ```bash git add docs/plans/phases/ git commit -m "docs: add phase 4-7 instruction guides" ``` --- ### Task 16: Final integration test and cleanup **Files:** - Modify: `tools/NatsNet.PortTracker/Program.cs` (ensure all commands registered) **Step 1: Clean build of both tools** ```bash cd tools/go-analyzer && go build -o go-analyzer . cd ../../tools/NatsNet.PortTracker && dotnet build ``` **Step 2: End-to-end test** ```bash # Delete any existing DB rm -f porting.db # Init cd tools/NatsNet.PortTracker dotnet run -- init --db ../../porting.db --schema ../../porting-schema.sql # Run Go analyzer cd ../go-analyzer ./go-analyzer --source ../../golang/nats-server --db ../../porting.db --schema ../../porting-schema.sql # Test all porttracker commands cd ../NatsNet.PortTracker dotnet run -- module list --db ../../porting.db dotnet run -- feature list --db ../../porting.db | head -20 dotnet run -- test list --db ../../porting.db | head -20 dotnet run -- library suggest --db ../../porting.db dotnet run -- dependency ready --db ../../porting.db dotnet run -- report summary --db ../../porting.db dotnet run -- phase list --db ../../porting.db dotnet run -- phase check 1 --db ../../porting.db dotnet run -- report export --format md --output ../../porting-status.md --db ../../porting.db ``` **Step 3: Verify DB contents via sqlite3** ```bash sqlite3 ../../porting.db ".tables" sqlite3 ../../porting.db "SELECT COUNT(*) FROM modules;" sqlite3 ../../porting.db "SELECT COUNT(*) FROM features;" sqlite3 ../../porting.db "SELECT COUNT(*) FROM unit_tests;" ``` **Step 4: Final commit** ```bash git add -A git commit -m "feat: complete porting tracker tooling — Go analyzer, .NET CLI, phase guides" ```