bd-6xd: Standardize on issues.jsonl as canonical filename
- Change default JSONL filename from beads.jsonl to issues.jsonl - Add bd doctor check and fix to auto-migrate legacy beads.jsonl configs - Update FindJSONLPath to prefer issues.jsonl over beads.jsonl - Add CheckLegacyJSONLConfig and CheckLegacyJSONLFilename checks - Add LegacyJSONLConfig fix to rename files and update config - Update .gitattributes to reference issues.jsonl - Fix tests to expect new canonical filename - Add bd-6xd to v0.25.1 release notes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ Files removed:
|
||||
- Git merge driver temp files (*.json[0-9], *.jsonl[0-9])
|
||||
|
||||
Files preserved:
|
||||
- beads.jsonl (source of truth)
|
||||
- issues.jsonl (source of truth)
|
||||
- beads.db (SQLite database)
|
||||
- metadata.json
|
||||
- config.yaml
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ var cleanupCmd = &cobra.Command{
|
||||
Short: "Delete closed issues from database to free up space",
|
||||
Long: `Delete closed issues from the database to reduce database size.
|
||||
|
||||
This command permanently removes closed issues from beads.db and beads.jsonl.
|
||||
This command permanently removes closed issues from beads.db and issues.jsonl.
|
||||
It does NOT remove temporary files - use 'bd clean' for that.
|
||||
|
||||
By default, deletes ALL closed issues. Use --older-than to only delete
|
||||
|
||||
+15
-7
@@ -210,6 +210,8 @@ func applyFixes(result doctorResult) {
|
||||
err = fix.SyncBranchConfig(result.Path)
|
||||
case "Database Config":
|
||||
err = fix.DatabaseConfig(result.Path)
|
||||
case "JSONL Config":
|
||||
err = fix.LegacyJSONLConfig(result.Path)
|
||||
case "Deletions Manifest":
|
||||
err = fix.HydrateDeletionsManifest(result.Path)
|
||||
case "Untracked Files":
|
||||
@@ -484,6 +486,11 @@ func runDiagnostics(path string) doctorResult {
|
||||
result.OverallOK = false
|
||||
}
|
||||
|
||||
// Check 6a: Legacy JSONL config (bd-6xd: migrate beads.jsonl to issues.jsonl)
|
||||
legacyConfigCheck := convertDoctorCheck(doctor.CheckLegacyJSONLConfig(path))
|
||||
result.Checks = append(result.Checks, legacyConfigCheck)
|
||||
// Don't fail overall check for legacy config, just warn
|
||||
|
||||
// Check 7: Database/JSONL configuration mismatch
|
||||
configCheck := convertDoctorCheck(doctor.CheckDatabaseConfig(path))
|
||||
result.Checks = append(result.Checks, configCheck)
|
||||
@@ -625,20 +632,20 @@ func checkDatabaseVersion(path string) doctorCheck {
|
||||
// Check if database file exists
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// Check if JSONL exists (--no-db mode)
|
||||
// Check both canonical (beads.jsonl) and legacy (issues.jsonl) names
|
||||
beadsJSONL := filepath.Join(beadsDir, "beads.jsonl")
|
||||
// Check canonical (issues.jsonl) first, then legacy (beads.jsonl)
|
||||
issuesJSONL := filepath.Join(beadsDir, "issues.jsonl")
|
||||
beadsJSONL := filepath.Join(beadsDir, "beads.jsonl")
|
||||
|
||||
if _, err := os.Stat(beadsJSONL); err == nil {
|
||||
if _, err := os.Stat(issuesJSONL); err == nil {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusOK,
|
||||
Message: "JSONL-only mode",
|
||||
Detail: "Using beads.jsonl (no SQLite database)",
|
||||
Detail: "Using issues.jsonl (no SQLite database)",
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(issuesJSONL); err == nil {
|
||||
if _, err := os.Stat(beadsJSONL); err == nil {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusOK,
|
||||
@@ -2095,9 +2102,10 @@ func checkDeletionsManifest(path string) doctorCheck {
|
||||
|
||||
// deletions.jsonl doesn't exist or is empty
|
||||
// Check if there's git history that might have deletions
|
||||
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
// bd-6xd: Check canonical issues.jsonl first, then legacy beads.jsonl
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||
jsonlPath = filepath.Join(beadsDir, "issues.jsonl")
|
||||
jsonlPath = filepath.Join(beadsDir, "beads.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||
return doctorCheck{
|
||||
Name: "Deletions Manifest",
|
||||
|
||||
@@ -73,7 +73,8 @@ func DatabaseConfig(path string) error {
|
||||
}
|
||||
|
||||
// findActualJSONLFile scans .beads/ for the actual JSONL file in use.
|
||||
// Prefers beads.jsonl over issues.jsonl, skips backups and merge artifacts.
|
||||
// Prefers issues.jsonl over beads.jsonl (canonical name), skips backups and merge artifacts.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func findActualJSONLFile(beadsDir string) string {
|
||||
entries, err := os.ReadDir(beadsDir)
|
||||
if err != nil {
|
||||
@@ -109,17 +110,87 @@ func findActualJSONLFile(beadsDir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prefer beads.jsonl over issues.jsonl (canonical name)
|
||||
// bd-6xd: Prefer issues.jsonl over beads.jsonl (canonical name)
|
||||
for _, name := range candidates {
|
||||
if name == "beads.jsonl" {
|
||||
if name == "issues.jsonl" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first candidate
|
||||
// Fall back to first candidate (including beads.jsonl as legacy)
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
// LegacyJSONLConfig migrates from legacy beads.jsonl to canonical issues.jsonl.
|
||||
// This renames the file, updates metadata.json, and updates .gitattributes if present.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func LegacyJSONLConfig(path string) error {
|
||||
if err := validateBeadsWorkspace(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
|
||||
// Load existing config
|
||||
cfg, err := configfile.Load(beadsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("no metadata.json found")
|
||||
}
|
||||
|
||||
legacyPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
canonicalPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
legacyExists := false
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
legacyExists = true
|
||||
}
|
||||
|
||||
canonicalExists := false
|
||||
if _, err := os.Stat(canonicalPath); err == nil {
|
||||
canonicalExists = true
|
||||
}
|
||||
|
||||
// Case 1: Config says beads.jsonl, file exists, issues.jsonl doesn't exist -> rename
|
||||
if cfg.JSONLExport == "beads.jsonl" && legacyExists && !canonicalExists {
|
||||
fmt.Printf(" Renaming beads.jsonl → issues.jsonl\n")
|
||||
if err := os.Rename(legacyPath, canonicalPath); err != nil {
|
||||
return fmt.Errorf("failed to rename file: %w", err)
|
||||
}
|
||||
cfg.JSONLExport = "issues.jsonl"
|
||||
|
||||
// Update .gitattributes if it references beads.jsonl
|
||||
gitattrsPath := filepath.Join(path, ".gitattributes")
|
||||
if content, err := os.ReadFile(gitattrsPath); err == nil {
|
||||
if strings.Contains(string(content), ".beads/beads.jsonl") {
|
||||
newContent := strings.ReplaceAll(string(content), ".beads/beads.jsonl", ".beads/issues.jsonl")
|
||||
// #nosec G306 -- .gitattributes should be world-readable
|
||||
if err := os.WriteFile(gitattrsPath, []byte(newContent), 0644); err != nil {
|
||||
fmt.Printf(" Warning: failed to update .gitattributes: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Updated .gitattributes\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Config says beads.jsonl but issues.jsonl exists -> just update config
|
||||
if cfg.JSONLExport == "beads.jsonl" && canonicalExists {
|
||||
fmt.Printf(" Updating config: beads.jsonl → issues.jsonl\n")
|
||||
cfg.JSONLExport = "issues.jsonl"
|
||||
}
|
||||
|
||||
// Save updated config
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Updated metadata.json\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// findActualDBFile scans .beads/ for the actual database file in use.
|
||||
// Prefers beads.db (canonical name), skips backups and vc.db.
|
||||
func findActualDBFile(beadsDir string) string {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// TestDatabaseConfigFix_JSONLMismatch tests that DatabaseConfig fixes JSONL mismatches.
|
||||
// bd-afd: Verify auto-fix for metadata.json jsonl_export mismatch
|
||||
// bd-6xd: Verify auto-fix for metadata.json jsonl_export mismatch
|
||||
func TestDatabaseConfigFix_JSONLMismatch(t *testing.T) {
|
||||
// Create temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
@@ -18,16 +18,16 @@ func TestDatabaseConfigFix_JSONLMismatch(t *testing.T) {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Create beads.jsonl file (actual JSONL)
|
||||
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
// Create issues.jsonl file (actual JSONL - canonical name)
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-123"}`), 0644); err != nil {
|
||||
t.Fatalf("Failed to create beads.jsonl: %v", err)
|
||||
t.Fatalf("Failed to create issues.jsonl: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json with wrong JSONL filename (issues.jsonl)
|
||||
// Create metadata.json with wrong JSONL filename (beads.jsonl)
|
||||
cfg := &configfile.Config{
|
||||
Database: "beads.db",
|
||||
JSONLExport: "issues.jsonl", // Wrong - should be beads.jsonl
|
||||
JSONLExport: "beads.jsonl", // Wrong - should be issues.jsonl
|
||||
}
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
@@ -44,13 +44,14 @@ func TestDatabaseConfigFix_JSONLMismatch(t *testing.T) {
|
||||
t.Fatalf("Failed to load updated config: %v", err)
|
||||
}
|
||||
|
||||
if updatedCfg.JSONLExport != "beads.jsonl" {
|
||||
t.Errorf("Expected JSONLExport to be 'beads.jsonl', got %q", updatedCfg.JSONLExport)
|
||||
if updatedCfg.JSONLExport != "issues.jsonl" {
|
||||
t.Errorf("Expected JSONLExport to be 'issues.jsonl', got %q", updatedCfg.JSONLExport)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatabaseConfigFix_PrefersBeadsJSONL tests that DatabaseConfig prefers beads.jsonl over issues.jsonl.
|
||||
func TestDatabaseConfigFix_PrefersBeadsJSONL(t *testing.T) {
|
||||
// TestDatabaseConfigFix_PrefersIssuesJSONL tests that DatabaseConfig prefers issues.jsonl over beads.jsonl.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func TestDatabaseConfigFix_PrefersIssuesJSONL(t *testing.T) {
|
||||
// Create temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
@@ -72,7 +73,7 @@ func TestDatabaseConfigFix_PrefersBeadsJSONL(t *testing.T) {
|
||||
// Create metadata.json with wrong JSONL filename (old.jsonl)
|
||||
cfg := &configfile.Config{
|
||||
Database: "beads.db",
|
||||
JSONLExport: "old.jsonl", // Wrong - should prefer beads.jsonl
|
||||
JSONLExport: "old.jsonl", // Wrong - should prefer issues.jsonl
|
||||
}
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
@@ -83,30 +84,31 @@ func TestDatabaseConfigFix_PrefersBeadsJSONL(t *testing.T) {
|
||||
t.Fatalf("DatabaseConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the config was updated to beads.jsonl (not issues.jsonl)
|
||||
// Verify the config was updated to issues.jsonl (canonical name)
|
||||
updatedCfg, err := configfile.Load(beadsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load updated config: %v", err)
|
||||
}
|
||||
|
||||
if updatedCfg.JSONLExport != "beads.jsonl" {
|
||||
t.Errorf("Expected JSONLExport to be 'beads.jsonl', got %q", updatedCfg.JSONLExport)
|
||||
if updatedCfg.JSONLExport != "issues.jsonl" {
|
||||
t.Errorf("Expected JSONLExport to be 'issues.jsonl', got %q", updatedCfg.JSONLExport)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindActualJSONLFile_SkipsBackups tests that backup files are skipped.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func TestFindActualJSONLFile_SkipsBackups(t *testing.T) {
|
||||
// Create temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create beads.jsonl and various backup files
|
||||
// Create issues.jsonl and various backup files
|
||||
files := []string{
|
||||
"beads.jsonl",
|
||||
"beads.jsonl.backup",
|
||||
"backup_beads.jsonl",
|
||||
"beads.jsonl.orig",
|
||||
"beads.jsonl.bak",
|
||||
"beads.jsonl~",
|
||||
"issues.jsonl",
|
||||
"issues.jsonl.backup",
|
||||
"backup_issues.jsonl",
|
||||
"issues.jsonl.orig",
|
||||
"issues.jsonl.bak",
|
||||
"issues.jsonl~",
|
||||
}
|
||||
|
||||
for _, name := range files {
|
||||
@@ -116,9 +118,105 @@ func TestFindActualJSONLFile_SkipsBackups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// findActualJSONLFile should return beads.jsonl (not backups)
|
||||
// findActualJSONLFile should return issues.jsonl (not backups)
|
||||
result := findActualJSONLFile(tmpDir)
|
||||
if result != "beads.jsonl" {
|
||||
t.Errorf("Expected 'beads.jsonl', got %q", result)
|
||||
if result != "issues.jsonl" {
|
||||
t.Errorf("Expected 'issues.jsonl', got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLegacyJSONLConfig_MigratesBeadsToIssues tests migration from beads.jsonl to issues.jsonl.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func TestLegacyJSONLConfig_MigratesBeadsToIssues(t *testing.T) {
|
||||
// Create temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Create beads.jsonl file (legacy name)
|
||||
legacyPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
if err := os.WriteFile(legacyPath, []byte(`{"id":"test-123"}`), 0644); err != nil {
|
||||
t.Fatalf("Failed to create beads.jsonl: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json with legacy filename
|
||||
cfg := &configfile.Config{
|
||||
Database: "beads.db",
|
||||
JSONLExport: "beads.jsonl",
|
||||
}
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Run the fix
|
||||
if err := LegacyJSONLConfig(tmpDir); err != nil {
|
||||
t.Fatalf("LegacyJSONLConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was renamed
|
||||
canonicalPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if _, err := os.Stat(canonicalPath); os.IsNotExist(err) {
|
||||
t.Error("Expected issues.jsonl to exist after migration")
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
t.Error("Expected beads.jsonl to be removed after migration")
|
||||
}
|
||||
|
||||
// Verify the config was updated
|
||||
updatedCfg, err := configfile.Load(beadsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load updated config: %v", err)
|
||||
}
|
||||
|
||||
if updatedCfg.JSONLExport != "issues.jsonl" {
|
||||
t.Errorf("Expected JSONLExport to be 'issues.jsonl', got %q", updatedCfg.JSONLExport)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLegacyJSONLConfig_UpdatesGitattributes tests that .gitattributes is updated during migration.
|
||||
func TestLegacyJSONLConfig_UpdatesGitattributes(t *testing.T) {
|
||||
// Create temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Create beads.jsonl file (legacy name)
|
||||
legacyPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
if err := os.WriteFile(legacyPath, []byte(`{"id":"test-123"}`), 0644); err != nil {
|
||||
t.Fatalf("Failed to create beads.jsonl: %v", err)
|
||||
}
|
||||
|
||||
// Create .gitattributes with legacy reference
|
||||
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
|
||||
if err := os.WriteFile(gitattrsPath, []byte(".beads/beads.jsonl merge=beads\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create .gitattributes: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json with legacy filename
|
||||
cfg := &configfile.Config{
|
||||
Database: "beads.db",
|
||||
JSONLExport: "beads.jsonl",
|
||||
}
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Run the fix
|
||||
if err := LegacyJSONLConfig(tmpDir); err != nil {
|
||||
t.Fatalf("LegacyJSONLConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify .gitattributes was updated
|
||||
content, err := os.ReadFile(gitattrsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read .gitattributes: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != ".beads/issues.jsonl merge=beads\n" {
|
||||
t.Errorf("Expected .gitattributes to reference issues.jsonl, got: %q", string(content))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ func HydrateDeletionsManifest(path string) error {
|
||||
}
|
||||
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
// Also check for legacy issues.jsonl
|
||||
// Also check for legacy beads.jsonl
|
||||
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||
legacyPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
legacyPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
jsonlPath = legacyPath
|
||||
} else {
|
||||
|
||||
@@ -177,6 +177,73 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckLegacyJSONLConfig detects if metadata.json is configured to use the legacy
|
||||
// beads.jsonl filename and recommends migrating to the canonical issues.jsonl.
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
func CheckLegacyJSONLConfig(repoPath string) DoctorCheck {
|
||||
beadsDir := filepath.Join(repoPath, ".beads")
|
||||
|
||||
// Load config
|
||||
cfg, err := configfile.Load(beadsDir)
|
||||
if err != nil || cfg == nil {
|
||||
// No config - using defaults, which are now issues.jsonl
|
||||
return DoctorCheck{
|
||||
Name: "JSONL Config",
|
||||
Status: "ok",
|
||||
Message: "Using default configuration (issues.jsonl)",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if using legacy beads.jsonl
|
||||
if cfg.JSONLExport == "beads.jsonl" {
|
||||
// Check if beads.jsonl actually exists
|
||||
legacyPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
canonicalPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
legacyExists := false
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
legacyExists = true
|
||||
}
|
||||
|
||||
canonicalExists := false
|
||||
if _, err := os.Stat(canonicalPath); err == nil {
|
||||
canonicalExists = true
|
||||
}
|
||||
|
||||
if legacyExists && !canonicalExists {
|
||||
return DoctorCheck{
|
||||
Name: "JSONL Config",
|
||||
Status: "warning",
|
||||
Message: "Using legacy beads.jsonl filename",
|
||||
Detail: "The canonical filename is now issues.jsonl (bd-6xd).\n" +
|
||||
" Legacy beads.jsonl is still supported but should be migrated.",
|
||||
Fix: "Run 'bd doctor --fix' to auto-migrate, or manually:\n" +
|
||||
" 1. git mv .beads/beads.jsonl .beads/issues.jsonl\n" +
|
||||
" 2. Update metadata.json: jsonl_export: \"issues.jsonl\"\n" +
|
||||
" 3. Update .gitattributes if present",
|
||||
}
|
||||
}
|
||||
|
||||
if !legacyExists && canonicalExists {
|
||||
// Config says beads.jsonl but issues.jsonl exists - just update config
|
||||
return DoctorCheck{
|
||||
Name: "JSONL Config",
|
||||
Status: "warning",
|
||||
Message: "Config references beads.jsonl but issues.jsonl exists",
|
||||
Detail: "metadata.json says beads.jsonl but the actual file is issues.jsonl",
|
||||
Fix: "Run 'bd doctor --fix' to update the configuration",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Using issues.jsonl or custom name - all good
|
||||
return DoctorCheck{
|
||||
Name: "JSONL Config",
|
||||
Status: "ok",
|
||||
Message: fmt.Sprintf("Using %s", cfg.JSONLExport),
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDatabaseConfig verifies that the configured database and JSONL paths
|
||||
// match what actually exists on disk.
|
||||
func CheckDatabaseConfig(repoPath string) DoctorCheck {
|
||||
|
||||
@@ -261,3 +261,86 @@ func TestCheckLegacyJSONLFilename(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLegacyJSONLConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configJSONL string // what metadata.json says
|
||||
existingFiles []string // which files actually exist
|
||||
expectedStatus string
|
||||
expectWarning bool
|
||||
}{
|
||||
{
|
||||
name: "no config (defaults)",
|
||||
configJSONL: "",
|
||||
existingFiles: []string{},
|
||||
expectedStatus: "ok",
|
||||
expectWarning: false,
|
||||
},
|
||||
{
|
||||
name: "using canonical issues.jsonl",
|
||||
configJSONL: "issues.jsonl",
|
||||
existingFiles: []string{"issues.jsonl"},
|
||||
expectedStatus: "ok",
|
||||
expectWarning: false,
|
||||
},
|
||||
{
|
||||
name: "using custom name",
|
||||
configJSONL: "my-project.jsonl",
|
||||
existingFiles: []string{"my-project.jsonl"},
|
||||
expectedStatus: "ok",
|
||||
expectWarning: false,
|
||||
},
|
||||
{
|
||||
name: "using legacy beads.jsonl",
|
||||
configJSONL: "beads.jsonl",
|
||||
existingFiles: []string{"beads.jsonl"},
|
||||
expectedStatus: "warning",
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "config says beads.jsonl but issues.jsonl exists",
|
||||
configJSONL: "beads.jsonl",
|
||||
existingFiles: []string{"issues.jsonl"},
|
||||
expectedStatus: "warning",
|
||||
expectWarning: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test files
|
||||
for _, file := range tt.existingFiles {
|
||||
filePath := filepath.Join(beadsDir, file)
|
||||
if err := os.WriteFile(filePath, []byte(`{"id":"test"}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create metadata.json if configJSONL is set
|
||||
if tt.configJSONL != "" {
|
||||
metadataPath := filepath.Join(beadsDir, "metadata.json")
|
||||
content := `{"database":"beads.db","jsonl_export":"` + tt.configJSONL + `"}`
|
||||
if err := os.WriteFile(metadataPath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
check := CheckLegacyJSONLConfig(tmpDir)
|
||||
|
||||
if check.Status != tt.expectedStatus {
|
||||
t.Errorf("Expected status %s, got %s (message: %s)", tt.expectedStatus, check.Status, check.Message)
|
||||
}
|
||||
|
||||
if tt.expectWarning && check.Fix == "" {
|
||||
t.Error("Expected fix message for warning, got empty string")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -43,7 +43,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
fmt.Fprintf(os.Stderr, "Did you mean: bd import -i %s\n\n", args[0])
|
||||
fmt.Fprintf(os.Stderr, "The import command does not accept positional arguments.\n")
|
||||
fmt.Fprintf(os.Stderr, "Use the -i flag to specify an input file:\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/beads.jsonl\n\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/issues.jsonl\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Or pipe data via stdin:\n")
|
||||
fmt.Fprintf(os.Stderr, " cat data.jsonl | bd import\n")
|
||||
os.Exit(1)
|
||||
@@ -89,8 +89,8 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
if input == "" && term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
fmt.Fprintf(os.Stderr, "Error: No input specified.\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/beads.jsonl # Import from file\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/beads.jsonl --dry-run # Preview changes\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/issues.jsonl # Import from file\n")
|
||||
fmt.Fprintf(os.Stderr, " bd import -i .beads/issues.jsonl --dry-run # Preview changes\n")
|
||||
fmt.Fprintf(os.Stderr, " cat data.jsonl | bd import # Import from pipe\n")
|
||||
fmt.Fprintf(os.Stderr, " bd sync --import-only # Import latest JSONL\n\n")
|
||||
fmt.Fprintf(os.Stderr, "For more information, run: bd import --help\n")
|
||||
@@ -144,8 +144,8 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
if err := attemptAutoMerge(input); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: Automatic merge failed: %v\n\n", err)
|
||||
fmt.Fprintf(os.Stderr, "To resolve manually:\n")
|
||||
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/beads.jsonl && bd import -i .beads/beads.jsonl\n")
|
||||
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/beads.jsonl && bd import -i .beads/beads.jsonl\n\n")
|
||||
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n")
|
||||
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n\n")
|
||||
fmt.Fprintf(os.Stderr, "For advanced field-level merging, see: https://github.com/neongreen/mono/tree/main/beads-merge\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
||||
return fmt.Errorf("failed to create .beads in planning repo: %w", err)
|
||||
}
|
||||
|
||||
// Create issues.jsonl
|
||||
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||
// Create issues.jsonl (canonical name, bd-6xd)
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
// #nosec G306 -- planning repo JSONL must be shareable across collaborators
|
||||
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create issues.jsonl: %w", err)
|
||||
@@ -183,8 +183,8 @@ Created by: bd init --contributor
|
||||
fmt.Printf("\n%s %s\n\n", green("✓"), bold("Contributor setup complete!"))
|
||||
|
||||
fmt.Println("Configuration:")
|
||||
fmt.Printf(" Current repo issues: %s\n", cyan(".beads/beads.jsonl"))
|
||||
fmt.Printf(" Planning repo issues: %s\n", cyan(filepath.Join(planningPath, ".beads/beads.jsonl")))
|
||||
fmt.Printf(" Current repo issues: %s\n", cyan(".beads/issues.jsonl"))
|
||||
fmt.Printf(" Planning repo issues: %s\n", cyan(filepath.Join(planningPath, ".beads/issues.jsonl")))
|
||||
fmt.Println()
|
||||
fmt.Println("How it works:")
|
||||
fmt.Println(" • Issues you create will route to the planning repo")
|
||||
|
||||
+2
-2
@@ -177,8 +177,8 @@ func TestAutoFlushJSONLContent(t *testing.T) {
|
||||
|
||||
dbPath = filepath.Join(tmpDir, "test.db")
|
||||
// The actual JSONL path - findJSONLPath() will determine this
|
||||
// but in tests it appears to be issues.jsonl in the same directory as the db
|
||||
expectedJSONLPath := filepath.Join(tmpDir, "beads.jsonl")
|
||||
// bd-6xd: Default is now issues.jsonl (canonical name)
|
||||
expectedJSONLPath := filepath.Join(tmpDir, "issues.jsonl")
|
||||
|
||||
// Create store
|
||||
testStore := newTestStore(t, dbPath)
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ Vendored into bd with permission.`,
|
||||
|
||||
func cleanupMergeArtifacts(outputPath string, debug bool) {
|
||||
// Determine the .beads directory from the output path
|
||||
// outputPath is typically .beads/beads.jsonl
|
||||
// outputPath is typically .beads/issues.jsonl
|
||||
beadsDir := filepath.Dir(outputPath)
|
||||
|
||||
if debug {
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ func ensureDatabaseFresh(ctx context.Context) error {
|
||||
"hasn't been imported yet. This would cause you to see stale/incomplete data.\n\n"+
|
||||
"To fix:\n"+
|
||||
" bd sync --import-only # Import JSONL updates to database\n"+
|
||||
" bd import -i .beads/beads.jsonl # Alternative: specify file explicitly\n\n"+
|
||||
" bd import -i .beads/issues.jsonl # Alternative: specify file explicitly\n\n"+
|
||||
"If in a sandboxed environment (e.g., Codex) where daemon can't be stopped:\n"+
|
||||
" bd --sandbox ready # Use direct mode (no daemon)\n"+
|
||||
" bd ready --allow-stale # Skip staleness check (use with caution)\n\n"+
|
||||
|
||||
+3
-3
@@ -169,7 +169,7 @@ Examples:
|
||||
},
|
||||
}
|
||||
|
||||
// getGitActivity calculates activity stats from git log of beads.jsonl
|
||||
// getGitActivity calculates activity stats from git log of issues.jsonl
|
||||
func getGitActivity(hours int) *RecentActivitySummary {
|
||||
activity := &RecentActivitySummary{
|
||||
HoursTracked: hours,
|
||||
@@ -177,7 +177,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
||||
|
||||
// Run git log to get patches for the last N hours
|
||||
since := fmt.Sprintf("%d hours ago", hours)
|
||||
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -213,7 +213,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
||||
}
|
||||
|
||||
// Get detailed diff to analyze changes
|
||||
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
|
||||
+3
-3
@@ -1045,9 +1045,9 @@ func showSyncStatus(ctx context.Context) error {
|
||||
fmt.Print(string(logOutput))
|
||||
}
|
||||
|
||||
// Show file diff for .beads/beads.jsonl
|
||||
fmt.Println("\nFile differences in .beads/beads.jsonl:")
|
||||
diffCmd := exec.CommandContext(ctx, "git", "diff", currentBranch+"..."+syncBranch, "--", ".beads/beads.jsonl")
|
||||
// Show file diff for .beads/issues.jsonl
|
||||
fmt.Println("\nFile differences in .beads/issues.jsonl:")
|
||||
diffCmd := exec.CommandContext(ctx, "git", "diff", currentBranch+"..."+syncBranch, "--", ".beads/issues.jsonl")
|
||||
diffOutput, err := diffCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// diff returns non-zero when there are differences, which is fine
|
||||
|
||||
@@ -186,10 +186,10 @@ func findSubstring(haystack, needle string) int {
|
||||
}
|
||||
|
||||
// findActualJSONLFile scans .beads/ for the actual JSONL file in use.
|
||||
// Prefers beads.jsonl over issues.jsonl, skips backups and merge artifacts.
|
||||
// Prefers issues.jsonl over beads.jsonl (canonical name), skips backups and merge artifacts.
|
||||
// Returns empty string if no JSONL file is found.
|
||||
//
|
||||
// bd-afd: Auto-detect JSONL file to prevent metadata.json mismatches
|
||||
// bd-6xd: Auto-detect JSONL file to prevent metadata.json mismatches
|
||||
func findActualJSONLFile(beadsDir string) string {
|
||||
entries, err := os.ReadDir(beadsDir)
|
||||
if err != nil {
|
||||
@@ -225,14 +225,14 @@ func findActualJSONLFile(beadsDir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prefer beads.jsonl over issues.jsonl (canonical name)
|
||||
// bd-6xd: Prefer issues.jsonl over beads.jsonl (canonical name)
|
||||
for _, name := range candidates {
|
||||
if name == "beads.jsonl" {
|
||||
if name == "issues.jsonl" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first candidate (including issues.jsonl if present)
|
||||
// Fall back to first candidate (including beads.jsonl as legacy)
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user