Fix Dolt backend init/daemon/doctor; prevent accidental SQLite artifacts; add integration tests; clean up lint (#1218)
* /{cmd,internal}: get dolt backend init working and allow issue creation
* /{website,internal,docs,cmd}: integration tests and more split backend fixes
* /{cmd,internal}: fix lint issues
* /cmd/bd/doctor/integrity.go: fix unable to query issues bug with dolt backend
* /cmd/bd/daemon.go: remove debug logging
This commit is contained in:
19
cmd/bd/doctor/backend.go
Normal file
19
cmd/bd/doctor/backend.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
)
|
||||
|
||||
// getBackendAndBeadsDir resolves the effective .beads directory (following redirects)
|
||||
// and returns the configured storage backend ("sqlite" by default, or "dolt").
|
||||
func getBackendAndBeadsDir(repoPath string) (backend string, beadsDir string) {
|
||||
beadsDir = resolveBeadsDir(filepath.Join(repoPath, ".beads"))
|
||||
|
||||
cfg, err := configfile.Load(beadsDir)
|
||||
if err != nil || cfg == nil {
|
||||
return configfile.BackendSQLite, beadsDir
|
||||
}
|
||||
return cfg.GetBackend(), beadsDir
|
||||
}
|
||||
@@ -309,8 +309,19 @@ func checkMetadataConfigValues(repoPath string) []string {
|
||||
if strings.Contains(cfg.Database, string(os.PathSeparator)) || strings.Contains(cfg.Database, "/") {
|
||||
issues = append(issues, fmt.Sprintf("metadata.json database: %q should be a filename, not a path", cfg.Database))
|
||||
}
|
||||
if !strings.HasSuffix(cfg.Database, ".db") && !strings.HasSuffix(cfg.Database, ".sqlite") && !strings.HasSuffix(cfg.Database, ".sqlite3") {
|
||||
issues = append(issues, fmt.Sprintf("metadata.json database: %q has unusual extension (expected .db, .sqlite, or .sqlite3)", cfg.Database))
|
||||
backend := cfg.GetBackend()
|
||||
if backend == configfile.BackendSQLite {
|
||||
if !strings.HasSuffix(cfg.Database, ".db") && !strings.HasSuffix(cfg.Database, ".sqlite") && !strings.HasSuffix(cfg.Database, ".sqlite3") {
|
||||
issues = append(issues, fmt.Sprintf("metadata.json database: %q has unusual extension (expected .db, .sqlite, or .sqlite3)", cfg.Database))
|
||||
}
|
||||
} else if backend == configfile.BackendDolt {
|
||||
// Dolt is directory-backed; `database` should point to a directory (typically "dolt").
|
||||
if strings.HasSuffix(cfg.Database, ".db") || strings.HasSuffix(cfg.Database, ".sqlite") || strings.HasSuffix(cfg.Database, ".sqlite3") {
|
||||
issues = append(issues, fmt.Sprintf("metadata.json database: %q looks like a SQLite file, but backend is dolt (expected a directory like %q)", cfg.Database, "dolt"))
|
||||
}
|
||||
if cfg.Database == beads.CanonicalDatabaseName {
|
||||
issues = append(issues, fmt.Sprintf("metadata.json database: %q is misleading for dolt backend (expected %q)", cfg.Database, "dolt"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,11 +356,16 @@ func checkDatabaseConfigValues(repoPath string) []string {
|
||||
return issues // No .beads directory, nothing to check
|
||||
}
|
||||
|
||||
// Get database path
|
||||
// Get database path (backend-aware)
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
// Check metadata.json for custom database name
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
|
||||
// For Dolt, cfg.DatabasePath() is a directory and sqlite checks are not applicable.
|
||||
if cfg.GetBackend() == configfile.BackendDolt {
|
||||
return issues
|
||||
}
|
||||
if cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
|
||||
@@ -182,6 +182,22 @@ func TestCheckMetadataConfigValues(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid dolt metadata", func(t *testing.T) {
|
||||
metadataContent := `{
|
||||
"database": "dolt",
|
||||
"jsonl_export": "issues.jsonl",
|
||||
"backend": "dolt"
|
||||
}`
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte(metadataContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write metadata.json: %v", err)
|
||||
}
|
||||
|
||||
issues := checkMetadataConfigValues(tmpDir)
|
||||
if len(issues) > 0 {
|
||||
t.Errorf("expected no issues, got: %v", issues)
|
||||
}
|
||||
})
|
||||
|
||||
// Test with path in database field
|
||||
t.Run("path in database field", func(t *testing.T) {
|
||||
metadataContent := `{
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/steveyegge/beads/internal/daemon"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/storage/factory"
|
||||
"github.com/steveyegge/beads/internal/syncbranch"
|
||||
)
|
||||
|
||||
@@ -167,7 +167,7 @@ func CheckGitSyncSetup(path string) DoctorCheck {
|
||||
// CheckDaemonAutoSync checks if daemon has auto-commit/auto-push enabled when
|
||||
// sync-branch is configured. Missing auto-sync slows down agent workflows.
|
||||
func CheckDaemonAutoSync(path string) DoctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
_, beadsDir := getBackendAndBeadsDir(path)
|
||||
socketPath := filepath.Join(beadsDir, "bd.sock")
|
||||
|
||||
// Check if daemon is running
|
||||
@@ -181,8 +181,7 @@ func CheckDaemonAutoSync(path string) DoctorCheck {
|
||||
|
||||
// Check if sync-branch is configured
|
||||
ctx := context.Background()
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
store, err := factory.NewFromConfigWithOptions(ctx, beadsDir, factory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Daemon Auto-Sync",
|
||||
@@ -249,11 +248,10 @@ func CheckDaemonAutoSync(path string) DoctorCheck {
|
||||
// CheckLegacyDaemonConfig checks for deprecated daemon config options and
|
||||
// encourages migration to the unified daemon.auto-sync setting.
|
||||
func CheckLegacyDaemonConfig(path string) DoctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
_, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
ctx := context.Background()
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
store, err := factory.NewFromConfigWithOptions(ctx, beadsDir, factory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Daemon Config",
|
||||
|
||||
@@ -2,6 +2,7 @@ package doctor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/steveyegge/beads/cmd/bd/doctor/fix"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
storagefactory "github.com/steveyegge/beads/internal/storage/factory"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -27,8 +29,85 @@ type localConfig struct {
|
||||
|
||||
// CheckDatabaseVersion checks the database version and migration status
|
||||
func CheckDatabaseVersion(path string, cliVersion string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Dolt backend: directory-backed store; version lives in metadata table.
|
||||
if backend == configfile.BackendDolt {
|
||||
doltPath := filepath.Join(beadsDir, "dolt")
|
||||
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
|
||||
// If JSONL exists, treat as fresh clone / needs init.
|
||||
issuesJSONL := filepath.Join(beadsDir, "issues.jsonl")
|
||||
beadsJSONL := filepath.Join(beadsDir, "beads.jsonl")
|
||||
_, issuesErr := os.Stat(issuesJSONL)
|
||||
_, beadsErr := os.Stat(beadsJSONL)
|
||||
if issuesErr == nil || beadsErr == nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusWarning,
|
||||
Message: "Fresh clone detected (no dolt database)",
|
||||
Detail: "Storage: Dolt",
|
||||
Fix: "Run 'bd init --backend dolt' to create and hydrate the dolt database",
|
||||
}
|
||||
}
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusError,
|
||||
Message: "No dolt database found",
|
||||
Detail: "Storage: Dolt",
|
||||
Fix: "Run 'bd init --backend dolt' to create database",
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusError,
|
||||
Message: "Unable to open database",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
Fix: "Run 'bd init --backend dolt' (or remove and re-init .beads/dolt if corrupted)",
|
||||
}
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
|
||||
dbVersion, err := store.GetMetadata(ctx, "bd_version")
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusError,
|
||||
Message: "Unable to read database version",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
Fix: "Database may be corrupted. Try re-initializing the dolt database with 'bd init --backend dolt'",
|
||||
}
|
||||
}
|
||||
if dbVersion == "" {
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusWarning,
|
||||
Message: "Database missing version metadata",
|
||||
Detail: "Storage: Dolt",
|
||||
Fix: "Run 'bd migrate' or re-run 'bd init --backend dolt' to set version metadata",
|
||||
}
|
||||
}
|
||||
|
||||
if dbVersion != cliVersion {
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("version %s (CLI: %s)", dbVersion, cliVersion),
|
||||
Detail: "Storage: Dolt",
|
||||
Fix: "Update bd CLI and re-run (dolt metadata will be updated automatically by the daemon)",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Database",
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("version %s", dbVersion),
|
||||
Detail: "Storage: Dolt",
|
||||
}
|
||||
}
|
||||
|
||||
// Check metadata.json first for custom database name
|
||||
var dbPath string
|
||||
@@ -137,8 +216,48 @@ func CheckDatabaseVersion(path string, cliVersion string) DoctorCheck {
|
||||
|
||||
// CheckSchemaCompatibility checks if all required tables and columns are present
|
||||
func CheckSchemaCompatibility(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Dolt backend: no SQLite schema probe. Instead, run a lightweight query sanity check.
|
||||
if backend == configfile.BackendDolt {
|
||||
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err != nil || !info.IsDir() {
|
||||
return DoctorCheck{
|
||||
Name: "Schema Compatibility",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no database)",
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Schema Compatibility",
|
||||
Status: StatusError,
|
||||
Message: "Failed to open database",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
}
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
|
||||
// Exercise core tables/views.
|
||||
if _, err := store.GetStatistics(ctx); err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Schema Compatibility",
|
||||
Status: StatusError,
|
||||
Message: "Database schema is incomplete or incompatible",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
Fix: "Re-run 'bd init --backend dolt' or remove and re-initialize .beads/dolt if corrupted",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Schema Compatibility",
|
||||
Status: StatusOK,
|
||||
Message: "Basic queries succeeded",
|
||||
Detail: "Storage: Dolt",
|
||||
}
|
||||
}
|
||||
|
||||
// Check metadata.json first for custom database name
|
||||
var dbPath string
|
||||
@@ -227,8 +346,57 @@ func CheckSchemaCompatibility(path string) DoctorCheck {
|
||||
|
||||
// CheckDatabaseIntegrity runs SQLite's PRAGMA integrity_check
|
||||
func CheckDatabaseIntegrity(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Dolt backend: SQLite PRAGMA integrity_check doesn't apply.
|
||||
// We do a lightweight read-only sanity check instead.
|
||||
if backend == configfile.BackendDolt {
|
||||
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err != nil || !info.IsDir() {
|
||||
return DoctorCheck{
|
||||
Name: "Database Integrity",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no database)",
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database Integrity",
|
||||
Status: StatusError,
|
||||
Message: "Failed to open database",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
Fix: "Re-run 'bd init --backend dolt' or remove and re-initialize .beads/dolt if corrupted",
|
||||
}
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
|
||||
// Minimal checks: metadata + statistics. If these work, the store is at least readable.
|
||||
if _, err := store.GetMetadata(ctx, "bd_version"); err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database Integrity",
|
||||
Status: StatusError,
|
||||
Message: "Basic query failed",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
}
|
||||
}
|
||||
if _, err := store.GetStatistics(ctx); err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Database Integrity",
|
||||
Status: StatusError,
|
||||
Message: "Basic query failed",
|
||||
Detail: fmt.Sprintf("Storage: Dolt\n\nError: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Database Integrity",
|
||||
Status: StatusOK,
|
||||
Message: "Basic query check passed",
|
||||
Detail: "Storage: Dolt (no SQLite integrity_check equivalent)",
|
||||
}
|
||||
}
|
||||
|
||||
// Get database path (same logic as CheckSchemaCompatibility)
|
||||
var dbPath string
|
||||
@@ -340,8 +508,46 @@ func CheckDatabaseIntegrity(path string) DoctorCheck {
|
||||
|
||||
// CheckDatabaseJSONLSync checks if database and JSONL are in sync
|
||||
func CheckDatabaseJSONLSync(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Dolt backend: JSONL is a derived compatibility artifact (export-only today).
|
||||
// The SQLite-style import/export divergence checks don't apply.
|
||||
if backend == configfile.BackendDolt {
|
||||
// Find JSONL file (respects metadata.json override when set).
|
||||
jsonlPath := ""
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
|
||||
if cfg.JSONLExport != "" && !isSystemJSONLFilename(cfg.JSONLExport) {
|
||||
p := cfg.JSONLPath(beadsDir)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
jsonlPath = p
|
||||
}
|
||||
}
|
||||
}
|
||||
if jsonlPath == "" {
|
||||
for _, name := range []string{"issues.jsonl", "beads.jsonl"} {
|
||||
testPath := filepath.Join(beadsDir, name)
|
||||
if _, err := os.Stat(testPath); err == nil {
|
||||
jsonlPath = testPath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if jsonlPath == "" {
|
||||
return DoctorCheck{
|
||||
Name: "DB-JSONL Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no JSONL file)",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "DB-JSONL Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (dolt backend)",
|
||||
Detail: "JSONL is derived from Dolt (export-only); import-only sync checks do not apply",
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve database path (respects metadata.json override).
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
@@ -795,8 +1001,7 @@ func isNoDbModeConfigured(beadsDir string) bool {
|
||||
// irreversible. The user must make an explicit decision to delete their
|
||||
// closed issue history. We only provide guidance, never action.
|
||||
func CheckDatabaseSize(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
_, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Get database path
|
||||
var dbPath string
|
||||
|
||||
@@ -2,6 +2,7 @@ package doctor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -16,23 +17,20 @@ import (
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
"github.com/steveyegge/beads/internal/git"
|
||||
storagefactory "github.com/steveyegge/beads/internal/storage/factory"
|
||||
)
|
||||
|
||||
// CheckIDFormat checks whether issues use hash-based or sequential IDs
|
||||
func CheckIDFormat(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory (bd-tvus fix)
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Check metadata.json first for custom database name
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
// Determine the on-disk location (file for SQLite, directory for Dolt).
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
} else {
|
||||
// Fall back to canonical database name
|
||||
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
}
|
||||
|
||||
// Check if using JSONL-only mode
|
||||
// Check if using JSONL-only mode (or uninitialized DB).
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// Check if JSONL exists (--no-db mode)
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
@@ -51,24 +49,29 @@ func CheckIDFormat(path string) DoctorCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// Open database
|
||||
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
||||
// Open the configured backend in read-only mode.
|
||||
// This must work for both SQLite and Dolt.
|
||||
ctx := context.Background()
|
||||
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: StatusError,
|
||||
Message: "Unable to open database",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
defer func() { _ = db.Close() }() // Intentionally ignore close error
|
||||
defer func() { _ = store.Close() }() // Intentionally ignore close error
|
||||
db := store.UnderlyingDB()
|
||||
|
||||
// Get sample of issues to check ID format (up to 10 for pattern analysis)
|
||||
rows, err := db.Query("SELECT id FROM issues ORDER BY created_at LIMIT 10")
|
||||
rows, err := db.QueryContext(ctx, "SELECT id FROM issues ORDER BY created_at LIMIT 10")
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: StatusError,
|
||||
Message: "Unable to query issues",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -99,6 +102,13 @@ func CheckIDFormat(path string) DoctorCheck {
|
||||
}
|
||||
|
||||
// Sequential IDs - recommend migration
|
||||
if backend == configfile.BackendDolt {
|
||||
return DoctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: StatusOK,
|
||||
Message: "hash-based ✓",
|
||||
}
|
||||
}
|
||||
return DoctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: StatusWarning,
|
||||
@@ -404,9 +414,98 @@ func CheckDeletionsManifest(path string) DoctorCheck {
|
||||
// This detects when a .beads directory was copied from another repo or when
|
||||
// the git remote URL changed. A mismatch can cause data loss during sync.
|
||||
func CheckRepoFingerprint(path string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory (bd-tvus fix)
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(path)
|
||||
|
||||
// Backend-aware existence check
|
||||
switch backend {
|
||||
case configfile.BackendDolt:
|
||||
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err != nil || !info.IsDir() {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no database)",
|
||||
}
|
||||
}
|
||||
default:
|
||||
// SQLite backend: needs a .db file
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
} else {
|
||||
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
}
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no database)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For Dolt, read fingerprint from storage metadata (no sqlite assumptions).
|
||||
if backend == configfile.BackendDolt {
|
||||
ctx := context.Background()
|
||||
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusWarning,
|
||||
Message: "Unable to open database",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
defer func() { _ = store.Close() }()
|
||||
|
||||
storedRepoID, err := store.GetMetadata(ctx, "repo_id")
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusWarning,
|
||||
Message: "Unable to read repo fingerprint",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// If missing, warn (not the legacy sqlite messaging).
|
||||
if storedRepoID == "" {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusWarning,
|
||||
Message: "Missing repo fingerprint metadata",
|
||||
Detail: "Storage: Dolt",
|
||||
Fix: "Run 'bd migrate --update-repo-id' to add fingerprint metadata",
|
||||
}
|
||||
}
|
||||
|
||||
currentRepoID, err := beads.ComputeRepoID()
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusWarning,
|
||||
Message: "Unable to compute current repo ID",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if storedRepoID != currentRepoID {
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusError,
|
||||
Message: "Database belongs to different repository",
|
||||
Detail: fmt.Sprintf("stored: %s, current: %s", storedRepoID[:8], currentRepoID[:8]),
|
||||
Fix: "Run 'bd migrate --update-repo-id' if URL changed, or 'rm -rf .beads && bd init --backend dolt' if wrong database",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Repo Fingerprint",
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("Verified (%s)", currentRepoID[:8]),
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite path (existing behavior)
|
||||
// Get database path
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
|
||||
@@ -371,8 +371,7 @@ func CheckDatabaseConfig(repoPath string) DoctorCheck {
|
||||
// CheckFreshClone detects if this is a fresh clone that needs 'bd init'.
|
||||
// A fresh clone has JSONL with issues but no database file.
|
||||
func CheckFreshClone(repoPath string) DoctorCheck {
|
||||
// Follow redirect to resolve actual beads directory
|
||||
beadsDir := resolveBeadsDir(filepath.Join(repoPath, ".beads"))
|
||||
backend, beadsDir := getBackendAndBeadsDir(repoPath)
|
||||
|
||||
// Check if .beads/ exists
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
@@ -404,21 +403,32 @@ func CheckFreshClone(repoPath string) DoctorCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if database exists
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
} else {
|
||||
// Fall back to canonical database name
|
||||
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
}
|
||||
|
||||
// If database exists, not a fresh clone
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
return DoctorCheck{
|
||||
Name: "Fresh Clone",
|
||||
Status: "ok",
|
||||
Message: "Database exists",
|
||||
// Check if database exists (backend-aware)
|
||||
switch backend {
|
||||
case configfile.BackendDolt:
|
||||
// Dolt is directory-backed: treat .beads/dolt as the DB existence signal.
|
||||
if info, err := os.Stat(filepath.Join(beadsDir, "dolt")); err == nil && info.IsDir() {
|
||||
return DoctorCheck{
|
||||
Name: "Fresh Clone",
|
||||
Status: "ok",
|
||||
Message: "Database exists",
|
||||
}
|
||||
}
|
||||
default:
|
||||
// SQLite (default): check configured .db file path.
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
} else {
|
||||
// Fall back to canonical database name
|
||||
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
}
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
return DoctorCheck{
|
||||
Name: "Fresh Clone",
|
||||
Status: "ok",
|
||||
Message: "Database exists",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +447,12 @@ func CheckFreshClone(repoPath string) DoctorCheck {
|
||||
if prefix != "" {
|
||||
fixCmd = fmt.Sprintf("bd init --prefix %s", prefix)
|
||||
}
|
||||
if backend == configfile.BackendDolt {
|
||||
fixCmd = "bd init --backend dolt"
|
||||
if prefix != "" {
|
||||
fixCmd = fmt.Sprintf("bd init --backend dolt --prefix %s", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Fresh Clone",
|
||||
|
||||
@@ -54,6 +54,11 @@ func CheckSyncDivergence(path string) DoctorCheck {
|
||||
}
|
||||
}
|
||||
|
||||
backend := configfile.BackendSQLite
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
|
||||
backend = cfg.GetBackend()
|
||||
}
|
||||
|
||||
var issues []SyncDivergenceIssue
|
||||
|
||||
// Check 1: JSONL differs from git HEAD
|
||||
@@ -62,10 +67,13 @@ func CheckSyncDivergence(path string) DoctorCheck {
|
||||
issues = append(issues, *jsonlIssue)
|
||||
}
|
||||
|
||||
// Check 2: SQLite last_import_time vs JSONL mtime
|
||||
mtimeIssue := checkSQLiteMtimeDivergence(path, beadsDir)
|
||||
if mtimeIssue != nil {
|
||||
issues = append(issues, *mtimeIssue)
|
||||
// Check 2: SQLite last_import_time vs JSONL mtime (SQLite only).
|
||||
// Dolt backend does not maintain SQLite metadata and does not support import-only sync.
|
||||
if backend == configfile.BackendSQLite {
|
||||
mtimeIssue := checkSQLiteMtimeDivergence(path, beadsDir)
|
||||
if mtimeIssue != nil {
|
||||
issues = append(issues, *mtimeIssue)
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: Uncommitted .beads/ changes
|
||||
@@ -75,10 +83,14 @@ func CheckSyncDivergence(path string) DoctorCheck {
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
msg := "JSONL, SQLite, and git are in sync"
|
||||
if backend == configfile.BackendDolt {
|
||||
msg = "JSONL, Dolt, and git are in sync"
|
||||
}
|
||||
return DoctorCheck{
|
||||
Name: "Sync Divergence",
|
||||
Status: StatusOK,
|
||||
Message: "JSONL, SQLite, and git are in sync",
|
||||
Message: msg,
|
||||
Category: CategoryData,
|
||||
}
|
||||
}
|
||||
@@ -256,10 +268,16 @@ func checkUncommittedBeadsChanges(path, beadsDir string) *SyncDivergenceIssue {
|
||||
}
|
||||
}
|
||||
|
||||
fixCmd := "bd sync"
|
||||
// For dolt backend, bd sync/import-only workflows don't apply; recommend a plain git commit.
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.GetBackend() == configfile.BackendDolt {
|
||||
fixCmd = "git add .beads/ && git commit -m 'sync beads'"
|
||||
}
|
||||
|
||||
return &SyncDivergenceIssue{
|
||||
Type: "uncommitted_beads",
|
||||
Description: fmt.Sprintf("Uncommitted .beads/ changes (%d file(s))", fileCount),
|
||||
FixCommand: "bd sync",
|
||||
FixCommand: fixCmd,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user