Files
gastown/internal/doctor/rig_routes_jsonl_check.go
Julian Knutsen 043a6abc59 fix(beads): prevent routes.jsonl corruption and add doctor check for rig-level routes.jsonl (#377)
* fix(beads): prevent routes.jsonl corruption from bd auto-export

When issues.jsonl doesn't exist, bd's auto-export mechanism writes
issue data to routes.jsonl, corrupting the routing configuration.

Changes:
- install.go: Create issues.jsonl before routes.jsonl at town level
- manager.go: Create issues.jsonl in rig beads; don't create routes.jsonl
  (rig-level routes.jsonl breaks bd's walk-up routing to town routes)
- Add integration tests for routes.jsonl corruption prevention

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(doctor): add check to detect and fix rig-level routes.jsonl

Add RigRoutesJSONLCheck to detect routes.jsonl files in rig .beads
directories. These files break bd's walk-up routing to town-level
routes.jsonl, causing cross-rig routing failures.

The fix unconditionally deletes rig-level routes.jsonl files since
bd will auto-export to issues.jsonl on next run.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(rig): add verification that routes.jsonl does NOT exist in rig .beads

Add explicit test assertion and detailed comment explaining why rig-level
routes.jsonl files must not exist (breaks bd walk-up routing to town routes).

Also verify that issues.jsonl DOES exist (prevents bd auto-export corruption).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(doctor): ensure town root route exists in routes.jsonl

The RoutesCheck now detects and fixes missing town root routes (hq- -> .).
This can happen when routes.jsonl is corrupted or was created without the
town route during initialization.

Changes:
- Detect missing hq- route in Run()
- Add hq- route in Fix() when missing
- Handle case where routes.jsonl is corrupted (regenerate with town route)
- Add comprehensive unit tests for route detection and fixing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(beads): fix routing integration test for routes.jsonl corruption

The TestBeadsRoutingFromTownRoot test was failing because bd's auto-export
mechanism writes issue data to routes.jsonl when issues.jsonl doesn't exist.
This corrupts the routing configuration.

Fix: Create empty issues.jsonl after bd init to prevent corruption.
This mirrors what gt install does to prevent the same bug.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 01:45:26 -08:00

181 lines
5.2 KiB
Go

package doctor
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
)
// RigRoutesJSONLCheck detects and fixes routes.jsonl files in rig .beads directories.
//
// Rig-level routes.jsonl files are problematic because:
// 1. bd's routing walks up to find town root (via mayor/town.json) and uses town-level routes.jsonl
// 2. If a rig has its own routes.jsonl, bd uses it and never finds town routes, breaking cross-rig routing
// 3. These files often exist due to a bug where bd's auto-export wrote issue data to routes.jsonl
//
// Fix: Delete routes.jsonl unconditionally. The SQLite database (beads.db) is the source
// of truth, and bd will auto-export to issues.jsonl on next run.
type RigRoutesJSONLCheck struct {
FixableCheck
// affectedRigs tracks which rigs have routes.jsonl
affectedRigs []rigRoutesInfo
}
type rigRoutesInfo struct {
rigName string
routesPath string
}
// NewRigRoutesJSONLCheck creates a new check for rig-level routes.jsonl files.
func NewRigRoutesJSONLCheck() *RigRoutesJSONLCheck {
return &RigRoutesJSONLCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "rig-routes-jsonl",
CheckDescription: "Check for routes.jsonl in rig .beads directories",
CheckCategory: CategoryConfig,
},
},
}
}
// Run checks for routes.jsonl files in rig .beads directories.
func (c *RigRoutesJSONLCheck) Run(ctx *CheckContext) *CheckResult {
c.affectedRigs = nil // Reset
// Get list of rigs from multiple sources
rigDirs := c.findRigDirectories(ctx.TownRoot)
if len(rigDirs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs to check",
Category: c.Category(),
}
}
var problems []string
for _, rigDir := range rigDirs {
rigName := filepath.Base(rigDir)
beadsDir := filepath.Join(rigDir, ".beads")
routesPath := filepath.Join(beadsDir, beads.RoutesFileName)
// Check if routes.jsonl exists in this rig's .beads directory
if _, err := os.Stat(routesPath); os.IsNotExist(err) {
continue // Good - no rig-level routes.jsonl
}
// routes.jsonl exists - it should be deleted
problems = append(problems, fmt.Sprintf("%s: has routes.jsonl (will delete - breaks cross-rig routing)", rigName))
c.affectedRigs = append(c.affectedRigs, rigRoutesInfo{
rigName: rigName,
routesPath: routesPath,
})
}
if len(c.affectedRigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("No rig-level routes.jsonl files (%d rigs checked)", len(rigDirs)),
Category: c.Category(),
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d rig(s) have routes.jsonl (breaks routing)", len(c.affectedRigs)),
Details: problems,
FixHint: "Run 'gt doctor --fix' to delete these files",
Category: c.Category(),
}
}
// Fix deletes routes.jsonl files in rig .beads directories.
// The SQLite database (beads.db) is the source of truth - bd will auto-export
// to issues.jsonl on next run.
func (c *RigRoutesJSONLCheck) Fix(ctx *CheckContext) error {
// Re-run check to populate affectedRigs if needed
if len(c.affectedRigs) == 0 {
result := c.Run(ctx)
if result.Status == StatusOK {
return nil // Nothing to fix
}
}
for _, info := range c.affectedRigs {
if err := os.Remove(info.routesPath); err != nil {
return fmt.Errorf("deleting %s: %w", info.routesPath, err)
}
}
return nil
}
// findRigDirectories finds all rig directories in the town.
func (c *RigRoutesJSONLCheck) findRigDirectories(townRoot string) []string {
var rigDirs []string
seen := make(map[string]bool)
// Source 1: rigs.json registry
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
if rigsConfig, err := config.LoadRigsConfig(rigsPath); err == nil {
for rigName := range rigsConfig.Rigs {
rigPath := filepath.Join(townRoot, rigName)
if _, err := os.Stat(rigPath); err == nil && !seen[rigPath] {
rigDirs = append(rigDirs, rigPath)
seen[rigPath] = true
}
}
}
// Source 2: routes.jsonl (for rigs that may not be in registry)
townBeadsDir := filepath.Join(townRoot, ".beads")
if routes, err := beads.LoadRoutes(townBeadsDir); err == nil {
for _, route := range routes {
if route.Path == "." || route.Path == "" {
continue // Skip town root
}
// Extract rig name (first path component)
parts := strings.Split(route.Path, "/")
if len(parts) > 0 && parts[0] != "" {
rigPath := filepath.Join(townRoot, parts[0])
if _, err := os.Stat(rigPath); err == nil && !seen[rigPath] {
rigDirs = append(rigDirs, rigPath)
seen[rigPath] = true
}
}
}
}
// Source 3: Look for directories with .beads subdirs (for unregistered rigs)
entries, err := os.ReadDir(townRoot)
if err == nil {
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// Skip known non-rig directories
if entry.Name() == "mayor" || entry.Name() == ".beads" || entry.Name() == ".git" {
continue
}
rigPath := filepath.Join(townRoot, entry.Name())
beadsDir := filepath.Join(rigPath, ".beads")
if _, err := os.Stat(beadsDir); err == nil && !seen[rigPath] {
rigDirs = append(rigDirs, rigPath)
seen[rigPath] = true
}
}
}
return rigDirs
}