From 5b2a516acafbc758116822cb339449cf24b7c697 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 20 Dec 2025 14:41:13 -0800 Subject: [PATCH] fix(doctor): check .local_version instead of deprecated LastBdVersion (fixes #662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doctor's version tracking check was looking at metadata.json:LastBdVersion, but version tracking was moved to .local_version file (gitignored). This caused the "LastBdVersion field is empty" warning to persist even after running bd commands, because those commands update .local_version but not metadata.json. - Update CheckMetadataVersionTracking to read from .local_version - Rename check from "Metadata Version Tracking" to "Version Tracking" - Update tests to use .local_version instead of metadata.json - Remove unused configfile import 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/doctor/version.go | 86 ++++++++++++++++++++++------------------ cmd/bd/doctor_test.go | 68 +++++++++---------------------- 2 files changed, 66 insertions(+), 88 deletions(-) diff --git a/cmd/bd/doctor/version.go b/cmd/bd/doctor/version.go index fe0044fe..7c24695a 100644 --- a/cmd/bd/doctor/version.go +++ b/cmd/bd/doctor/version.go @@ -5,11 +5,10 @@ import ( "fmt" "io" "net/http" + "os" "path/filepath" "strings" "time" - - "github.com/steveyegge/beads/internal/configfile" ) // CheckCLIVersion checks if the CLI version is up to date. @@ -53,62 +52,73 @@ func CheckCLIVersion(cliVersion string) DoctorCheck { } } -// CheckMetadataVersionTracking checks if metadata.json has proper version tracking. +// localVersionFile is the gitignored file that stores the last bd version used locally. +// Must match the constant in version_tracking.go. +const localVersionFile = ".local_version" + +// CheckMetadataVersionTracking checks if version tracking is properly configured. +// Version tracking uses .local_version file (gitignored) to track the last bd version used. +// +// GH#662: This was updated to check .local_version instead of metadata.json:LastBdVersion, +// which is now deprecated. func CheckMetadataVersionTracking(path string, currentVersion string) DoctorCheck { beadsDir := filepath.Join(path, ".beads") + localVersionPath := filepath.Join(beadsDir, localVersionFile) - // Load metadata.json - cfg, err := configfile.Load(beadsDir) + // Read .local_version file + // #nosec G304 - path is constructed from controlled beadsDir + constant + data, err := os.ReadFile(localVersionPath) if err != nil { + if os.IsNotExist(err) { + // File doesn't exist yet - will be created on next bd command + return DoctorCheck{ + Name: "Version Tracking", + Status: StatusWarning, + Message: "Version tracking not initialized", + Detail: "The .local_version file will be created on next bd command", + Fix: "Run any bd command (e.g., 'bd ready') to initialize version tracking", + } + } + // Other error reading file return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusError, - Message: "Unable to read metadata.json", + Message: "Unable to read .local_version file", Detail: err.Error(), - Fix: "Ensure metadata.json exists and is valid JSON. Run 'bd init' if needed.", + Fix: "Check file permissions on .beads/.local_version", } } - // Check if metadata.json exists - if cfg == nil { - return DoctorCheck{ - Name: "Metadata Version Tracking", - Status: StatusWarning, - Message: "metadata.json not found", - Fix: "Run any bd command to create metadata.json with version tracking", - } - } + lastVersion := strings.TrimSpace(string(data)) - // Check if LastBdVersion field is present - if cfg.LastBdVersion == "" { + // Check if file is empty + if lastVersion == "" { return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusWarning, - Message: "LastBdVersion field is empty (first run)", + Message: ".local_version file is empty", Detail: "Version tracking will be initialized on next command", Fix: "Run any bd command to initialize version tracking", } } - // Validate that LastBdVersion is a valid semver-like string - // Simple validation: should be X.Y.Z format where X, Y, Z are numbers - if !IsValidSemver(cfg.LastBdVersion) { + // Validate that version is a valid semver-like string + if !IsValidSemver(lastVersion) { return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusWarning, - Message: fmt.Sprintf("LastBdVersion has invalid format: %q", cfg.LastBdVersion), + Message: fmt.Sprintf("Invalid version format in .local_version: %q", lastVersion), Detail: "Expected semver format like '0.24.2'", Fix: "Run any bd command to reset version tracking to current version", } } - // Check if LastBdVersion is very old (> 10 versions behind) - // Calculate version distance - versionDiff := CompareVersions(currentVersion, cfg.LastBdVersion) + // Check if version is very old (> 10 versions behind) + versionDiff := CompareVersions(currentVersion, lastVersion) if versionDiff > 0 { // Current version is newer - check how far behind currentParts := ParseVersionParts(currentVersion) - lastParts := ParseVersionParts(cfg.LastBdVersion) + lastParts := ParseVersionParts(lastVersion) // Simple heuristic: warn if minor version is 10+ behind or major version differs by 1+ majorDiff := currentParts[0] - lastParts[0] @@ -116,27 +126,27 @@ func CheckMetadataVersionTracking(path string, currentVersion string) DoctorChec if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) { return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusWarning, - Message: fmt.Sprintf("LastBdVersion is very old: %s (current: %s)", cfg.LastBdVersion, currentVersion), + Message: fmt.Sprintf("Last recorded version is very old: %s (current: %s)", lastVersion, currentVersion), Detail: "You may have missed important upgrade notifications", Fix: "Run 'bd upgrade review' to see recent changes", } } - // Version is behind but not too old + // Version is behind but not too old - this is normal after upgrade return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusOK, - Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", cfg.LastBdVersion, currentVersion), + Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", lastVersion, currentVersion), } } - // Version is current or ahead (shouldn't happen, but handle it) + // Version is current or ahead return DoctorCheck{ - Name: "Metadata Version Tracking", + Name: "Version Tracking", Status: StatusOK, - Message: fmt.Sprintf("Version tracking active (version: %s)", cfg.LastBdVersion), + Message: fmt.Sprintf("Version tracking active (version: %s)", lastVersion), } } diff --git a/cmd/bd/doctor_test.go b/cmd/bd/doctor_test.go index f17a1e57..f3ea14e7 100644 --- a/cmd/bd/doctor_test.go +++ b/cmd/bd/doctor_test.go @@ -877,89 +877,57 @@ func TestGetClaudePluginVersion(t *testing.T) { } func TestCheckMetadataVersionTracking(t *testing.T) { + // GH#662: Tests updated to use .local_version file instead of metadata.json:LastBdVersion tests := []struct { name string - setupMetadata func(beadsDir string) error + setupVersion func(beadsDir string) error expectedStatus string expectWarning bool }{ { name: "valid current version", - setupMetadata: func(beadsDir string) error { - cfg := map[string]string{ - "database": "beads.db", - "last_bd_version": Version, - } - data, _ := json.Marshal(cfg) - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644) + setupVersion: func(beadsDir string) error { + return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(Version+"\n"), 0644) }, expectedStatus: doctor.StatusOK, expectWarning: false, }, { name: "slightly outdated version", - setupMetadata: func(beadsDir string) error { - cfg := map[string]string{ - "database": "beads.db", - "last_bd_version": "0.24.0", - } - data, _ := json.Marshal(cfg) - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644) + setupVersion: func(beadsDir string) error { + return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.24.0\n"), 0644) }, expectedStatus: doctor.StatusOK, expectWarning: false, }, { name: "very old version", - setupMetadata: func(beadsDir string) error { - cfg := map[string]string{ - "database": "beads.db", - "last_bd_version": "0.14.0", - } - data, _ := json.Marshal(cfg) - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644) + setupVersion: func(beadsDir string) error { + return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.14.0\n"), 0644) }, expectedStatus: doctor.StatusWarning, expectWarning: true, }, { - name: "empty version field", - setupMetadata: func(beadsDir string) error { - cfg := map[string]string{ - "database": "beads.db", - "last_bd_version": "", - } - data, _ := json.Marshal(cfg) - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644) + name: "empty version file", + setupVersion: func(beadsDir string) error { + return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(""), 0644) }, expectedStatus: doctor.StatusWarning, expectWarning: true, }, { name: "invalid version format", - setupMetadata: func(beadsDir string) error { - cfg := map[string]string{ - "database": "beads.db", - "last_bd_version": "invalid-version", - } - data, _ := json.Marshal(cfg) - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644) + setupVersion: func(beadsDir string) error { + return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("invalid-version\n"), 0644) }, expectedStatus: doctor.StatusWarning, expectWarning: true, }, { - name: "corrupted metadata.json", - setupMetadata: func(beadsDir string) error { - return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte("{invalid json}"), 0644) - }, - expectedStatus: doctor.StatusError, - expectWarning: false, - }, - { - name: "missing metadata.json", - setupMetadata: func(beadsDir string) error { - // Don't create metadata.json + name: "missing .local_version file", + setupVersion: func(beadsDir string) error { + // Don't create .local_version return nil }, expectedStatus: doctor.StatusWarning, @@ -975,8 +943,8 @@ func TestCheckMetadataVersionTracking(t *testing.T) { t.Fatal(err) } - // Setup metadata.json - if err := tc.setupMetadata(beadsDir); err != nil { + // Setup .local_version file + if err := tc.setupVersion(beadsDir); err != nil { t.Fatal(err) }