feat: Add metadata.json version tracking validation to bd doctor (bd-u4sb)

Add comprehensive validation of metadata.json version tracking to bd doctor:

Checks added:
- metadata.json exists and is valid JSON
- LastBdVersion field is present and non-empty
- LastBdVersion is valid semver format (e.g., 0.24.2)
- Warns if LastBdVersion is very old (> 10 minor versions behind)
- Provides helpful fix messages for each validation failure

Implementation:
- New checkMetadataVersionTracking() function
- Helper functions: isValidSemver(), parseVersionParts()
- Comprehensive test coverage for all validation scenarios

Tests:
- TestCheckMetadataVersionTracking: 7 test cases covering all scenarios
- TestIsValidSemver: Version format validation
- TestParseVersionParts: Version parsing logic

This helps ensure version tracking (bd-loka) is working correctly and
alerts users if they've missed important upgrade notifications.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-23 17:28:25 -08:00
parent 238ce34b52
commit d8f3eb0c25
4 changed files with 308 additions and 1 deletions

View File

@@ -70,6 +70,7 @@ This command checks:
- Circular dependencies
- Git hooks (pre-commit, post-merge, pre-push)
- .beads/.gitignore up to date
- Metadata.json version tracking (LastBdVersion field)
Performance Mode (--perf):
Run performance diagnostics on your database:
@@ -342,6 +343,11 @@ func runDiagnostics(path string) doctorResult {
result.Checks = append(result.Checks, mergeDriverCheck)
// Don't fail overall check for merge driver, just warn
// Check 16: Metadata.json version tracking (bd-u4sb)
metadataCheck := checkMetadataVersionTracking(path)
result.Checks = append(result.Checks, metadataCheck)
// Don't fail overall check for metadata, just warn
return result
}
@@ -1586,6 +1592,138 @@ func checkMergeDriver(path string) doctorCheck {
}
}
func checkMetadataVersionTracking(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Load metadata.json
cfg, err := configfile.Load(beadsDir)
if err != nil {
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusError,
Message: "Unable to read metadata.json",
Detail: err.Error(),
Fix: "Ensure metadata.json exists and is valid JSON. Run 'bd init' if needed.",
}
}
// 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",
}
}
// Check if LastBdVersion field is present
if cfg.LastBdVersion == "" {
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusWarning,
Message: "LastBdVersion field is empty (first run)",
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) {
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusWarning,
Message: fmt.Sprintf("LastBdVersion has invalid format: %q", cfg.LastBdVersion),
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(Version, cfg.LastBdVersion)
if versionDiff > 0 {
// Current version is newer - check how far behind
currentParts := parseVersionParts(Version)
lastParts := parseVersionParts(cfg.LastBdVersion)
// Simple heuristic: warn if minor version is 10+ behind or major version differs by 1+
majorDiff := currentParts[0] - lastParts[0]
minorDiff := currentParts[1] - lastParts[1]
if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) {
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusWarning,
Message: fmt.Sprintf("LastBdVersion is very old: %s (current: %s)", cfg.LastBdVersion, Version),
Detail: "You may have missed important upgrade notifications",
Fix: "Run 'bd upgrade review' to see recent changes",
}
}
// Version is behind but not too old
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusOK,
Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", cfg.LastBdVersion, Version),
}
}
// Version is current or ahead (shouldn't happen, but handle it)
return doctorCheck{
Name: "Metadata Version Tracking",
Status: statusOK,
Message: fmt.Sprintf("Version tracking active (version: %s)", cfg.LastBdVersion),
}
}
// isValidSemver checks if a version string is valid semver-like format (X.Y.Z)
func isValidSemver(version string) bool {
if version == "" {
return false
}
// Split by dots and ensure all parts are numeric
versionParts := strings.Split(version, ".")
if len(versionParts) < 1 {
return false
}
// Parse each part to ensure it's a valid number
for _, part := range versionParts {
if part == "" {
return false
}
var num int
if _, err := fmt.Sscanf(part, "%d", &num); err != nil {
return false
}
if num < 0 {
return false
}
}
return true
}
// parseVersionParts parses version string into numeric parts
// Returns [major, minor, patch, ...] or empty slice on error
func parseVersionParts(version string) []int {
parts := strings.Split(version, ".")
result := make([]int, 0, len(parts))
for _, part := range parts {
var num int
if _, err := fmt.Sscanf(part, "%d", &num); err != nil {
return result
}
result = append(result, num)
}
return result
}
func init() {
rootCmd.AddCommand(doctorCmd)
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")