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:
Dustin Brown
2026-01-20 17:34:00 -08:00
committed by GitHub
parent c1ac69da3e
commit d3ccd5cfba
31 changed files with 1071 additions and 305 deletions

19
cmd/bd/doctor/backend.go Normal file
View 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
}

View File

@@ -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) {

View File

@@ -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 := `{

View File

@@ -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",

View File

@@ -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

View File

@@ -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 != "" {

View File

@@ -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",

View File

@@ -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,
}
}