Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
645 lines
19 KiB
Go
645 lines
19 KiB
Go
package doctor
|
|
|
|
import (
|
|
"bufio"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/ncruces/go-sqlite3/driver"
|
|
_ "github.com/ncruces/go-sqlite3/embed"
|
|
"github.com/steveyegge/beads/cmd/bd/doctor/fix"
|
|
"github.com/steveyegge/beads/internal/beads"
|
|
"github.com/steveyegge/beads/internal/configfile"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// localConfig represents the config.yaml structure for no-db mode detection
|
|
type localConfig struct {
|
|
SyncBranch string `yaml:"sync-branch"`
|
|
NoDb bool `yaml:"no-db"`
|
|
}
|
|
|
|
// CheckDatabaseVersion checks the database version and migration status
|
|
func CheckDatabaseVersion(path string, cliVersion string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Check metadata.json first for custom database name
|
|
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)
|
|
}
|
|
|
|
// Check if database file exists
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
// Check if JSONL exists
|
|
// Check canonical (issues.jsonl) first, then legacy (beads.jsonl)
|
|
issuesJSONL := filepath.Join(beadsDir, "issues.jsonl")
|
|
beadsJSONL := filepath.Join(beadsDir, "beads.jsonl")
|
|
|
|
var jsonlPath string
|
|
if _, err := os.Stat(issuesJSONL); err == nil {
|
|
jsonlPath = issuesJSONL
|
|
} else if _, err := os.Stat(beadsJSONL); err == nil {
|
|
jsonlPath = beadsJSONL
|
|
}
|
|
|
|
if jsonlPath != "" {
|
|
// JSONL exists but no database - check if this is no-db mode or fresh clone
|
|
// Use proper YAML parsing to detect no-db mode (bd-r6k2)
|
|
if isNoDbModeConfigured(beadsDir) {
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusOK,
|
|
Message: "JSONL-only mode",
|
|
Detail: "Using issues.jsonl (no SQLite database)",
|
|
}
|
|
}
|
|
|
|
// This is a fresh clone - JSONL exists but no database and not no-db mode
|
|
// Count issues and detect prefix for helpful suggestion
|
|
issueCount := countIssuesInJSONLFile(jsonlPath)
|
|
prefix := detectPrefixFromJSONL(jsonlPath)
|
|
|
|
message := "Fresh clone detected (no database)"
|
|
detail := fmt.Sprintf("Found %d issue(s) in JSONL that need to be imported", issueCount)
|
|
fix := "Run 'bd init' to hydrate the database from JSONL"
|
|
if prefix != "" {
|
|
fix = fmt.Sprintf("Run 'bd init' to hydrate the database (detected prefix: %s)", prefix)
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusWarning,
|
|
Message: message,
|
|
Detail: detail,
|
|
Fix: fix,
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusError,
|
|
Message: "No beads.db found",
|
|
Fix: "Run 'bd init' to create database",
|
|
}
|
|
}
|
|
|
|
// Get database version
|
|
dbVersion := getDatabaseVersionFromPath(dbPath)
|
|
|
|
if dbVersion == "unknown" {
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusError,
|
|
Message: "Unable to read database version",
|
|
Detail: "Storage: SQLite",
|
|
Fix: "Database may be corrupted. Try 'bd migrate'",
|
|
}
|
|
}
|
|
|
|
if dbVersion == "pre-0.17.5" {
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("version %s (very old)", dbVersion),
|
|
Detail: "Storage: SQLite",
|
|
Fix: "Run 'bd migrate' to upgrade database schema",
|
|
}
|
|
}
|
|
|
|
if dbVersion != cliVersion {
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("version %s (CLI: %s)", dbVersion, cliVersion),
|
|
Detail: "Storage: SQLite",
|
|
Fix: "Run 'bd migrate' to sync database with CLI version",
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Database",
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("version %s", dbVersion),
|
|
Detail: "Storage: SQLite",
|
|
}
|
|
}
|
|
|
|
// CheckSchemaCompatibility checks if all required tables and columns are present
|
|
func CheckSchemaCompatibility(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Check metadata.json first for custom database name
|
|
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 no database, skip this check
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
return DoctorCheck{
|
|
Name: "Schema Compatibility",
|
|
Status: StatusOK,
|
|
Message: "N/A (no database)",
|
|
}
|
|
}
|
|
|
|
// Open database (bd-ckvw: This will run migrations and schema probe)
|
|
// Note: We can't use the global 'store' because doctor can check arbitrary paths
|
|
db, err := sql.Open("sqlite3", "file:"+dbPath+"?_pragma=foreign_keys(ON)&_pragma=busy_timeout(30000)")
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Schema Compatibility",
|
|
Status: StatusError,
|
|
Message: "Failed to open database",
|
|
Detail: err.Error(),
|
|
Fix: "Database may be corrupted. Try 'bd migrate' or restore from backup",
|
|
}
|
|
}
|
|
defer db.Close()
|
|
|
|
// Run schema probe (defined in internal/storage/sqlite/schema_probe.go)
|
|
// This is a simplified version since we can't import the internal package directly
|
|
// Check all critical tables and columns
|
|
criticalChecks := map[string][]string{
|
|
"issues": {"id", "title", "content_hash", "external_ref", "compacted_at", "close_reason", "pinned", "sender", "ephemeral"},
|
|
"dependencies": {"issue_id", "depends_on_id", "type", "metadata", "thread_id"},
|
|
"child_counters": {"parent_id", "last_child"},
|
|
"export_hashes": {"issue_id", "content_hash"},
|
|
}
|
|
|
|
var missingElements []string
|
|
for table, columns := range criticalChecks {
|
|
// Try to query all columns
|
|
query := fmt.Sprintf(
|
|
"SELECT %s FROM %s LIMIT 0",
|
|
strings.Join(columns, ", "),
|
|
table,
|
|
) // #nosec G201 -- table/column names sourced from hardcoded map
|
|
_, err := db.Exec(query)
|
|
|
|
if err != nil {
|
|
errMsg := err.Error()
|
|
if strings.Contains(errMsg, "no such table") {
|
|
missingElements = append(missingElements, fmt.Sprintf("table:%s", table))
|
|
} else if strings.Contains(errMsg, "no such column") {
|
|
// Find which columns are missing
|
|
for _, col := range columns {
|
|
colQuery := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", col, table) // #nosec G201 -- names come from static schema definition
|
|
if _, colErr := db.Exec(colQuery); colErr != nil && strings.Contains(colErr.Error(), "no such column") {
|
|
missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(missingElements) > 0 {
|
|
return DoctorCheck{
|
|
Name: "Schema Compatibility",
|
|
Status: StatusError,
|
|
Message: "Database schema is incomplete or incompatible",
|
|
Detail: fmt.Sprintf("Missing: %s", strings.Join(missingElements, ", ")),
|
|
Fix: "Run 'bd migrate' to upgrade schema, or if daemon is running an old version, run 'bd daemons killall' to restart",
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Schema Compatibility",
|
|
Status: StatusOK,
|
|
Message: "All required tables and columns present",
|
|
}
|
|
}
|
|
|
|
// CheckDatabaseIntegrity runs SQLite's PRAGMA integrity_check (bd-2au)
|
|
func CheckDatabaseIntegrity(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Get database path (same logic as CheckSchemaCompatibility)
|
|
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 no database, skip this check
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
return DoctorCheck{
|
|
Name: "Database Integrity",
|
|
Status: StatusOK,
|
|
Message: "N/A (no database)",
|
|
}
|
|
}
|
|
|
|
// Open database in read-only mode for integrity check
|
|
db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro&_pragma=busy_timeout(30000)")
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Database Integrity",
|
|
Status: StatusError,
|
|
Message: "Failed to open database for integrity check",
|
|
Detail: err.Error(),
|
|
Fix: "Run 'bd doctor --fix' to back up the corrupt DB and rebuild from JSONL (if available), or restore from backup",
|
|
}
|
|
}
|
|
defer db.Close()
|
|
|
|
// Run PRAGMA integrity_check
|
|
// This checks the entire database for corruption
|
|
rows, err := db.Query("PRAGMA integrity_check")
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Database Integrity",
|
|
Status: StatusError,
|
|
Message: "Failed to run integrity check",
|
|
Detail: err.Error(),
|
|
Fix: "Run 'bd doctor --fix' to back up the corrupt DB and rebuild from JSONL (if available), or restore from backup",
|
|
}
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []string
|
|
for rows.Next() {
|
|
var result string
|
|
if err := rows.Scan(&result); err != nil {
|
|
continue
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
|
|
// "ok" means no corruption detected
|
|
if len(results) == 1 && results[0] == "ok" {
|
|
return DoctorCheck{
|
|
Name: "Database Integrity",
|
|
Status: StatusOK,
|
|
Message: "No corruption detected",
|
|
}
|
|
}
|
|
|
|
// Any other result indicates corruption
|
|
return DoctorCheck{
|
|
Name: "Database Integrity",
|
|
Status: StatusError,
|
|
Message: "Database corruption detected",
|
|
Detail: strings.Join(results, "; "),
|
|
Fix: "Run 'bd doctor --fix' to back up the corrupt DB and rebuild from JSONL (if available), or restore from backup",
|
|
}
|
|
}
|
|
|
|
// CheckDatabaseJSONLSync checks if database and JSONL are in sync
|
|
func CheckDatabaseJSONLSync(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Resolve database path (respects metadata.json override).
|
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
|
dbPath = cfg.DatabasePath(beadsDir)
|
|
}
|
|
|
|
// 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 no database, skip this check
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusOK,
|
|
Message: "N/A (no database)",
|
|
}
|
|
}
|
|
|
|
// If no JSONL, skip this check
|
|
if jsonlPath == "" {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusOK,
|
|
Message: "N/A (no JSONL file)",
|
|
}
|
|
}
|
|
|
|
// Try to read JSONL first (doesn't depend on database)
|
|
jsonlCount, jsonlPrefixes, jsonlErr := CountJSONLIssues(jsonlPath)
|
|
|
|
// Single database open for all queries (instead of 3 separate opens)
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
// Database can't be opened. If JSONL has issues, suggest recovery.
|
|
if jsonlErr == nil && jsonlCount > 0 {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("Database cannot be opened but JSONL contains %d issues", jsonlCount),
|
|
Detail: err.Error(),
|
|
Fix: fmt.Sprintf("Run 'bd import -i %s --rename-on-import' to recover issues from JSONL", filepath.Base(jsonlPath)),
|
|
}
|
|
}
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to open database",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
defer db.Close()
|
|
|
|
// Get database count
|
|
var dbCount int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&dbCount)
|
|
if err != nil {
|
|
// Database opened but can't query. If JSONL has issues, suggest recovery.
|
|
if jsonlErr == nil && jsonlCount > 0 {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("Database cannot be queried but JSONL contains %d issues", jsonlCount),
|
|
Detail: err.Error(),
|
|
Fix: fmt.Sprintf("Run 'bd import -i %s --rename-on-import' to recover issues from JSONL", filepath.Base(jsonlPath)),
|
|
}
|
|
}
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to query database",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
|
|
// Get database prefix
|
|
var dbPrefix string
|
|
err = db.QueryRow("SELECT value FROM config WHERE key = ?", "issue_prefix").Scan(&dbPrefix)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to read database prefix",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
|
|
// Use JSONL error if we got it earlier
|
|
if jsonlErr != nil {
|
|
fixMsg := "Run 'bd doctor --fix' to attempt recovery"
|
|
if strings.Contains(jsonlErr.Error(), "malformed") {
|
|
fixMsg = "Run 'bd doctor --fix' to back up and regenerate the JSONL from the database"
|
|
}
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to read JSONL file",
|
|
Detail: jsonlErr.Error(),
|
|
Fix: fixMsg,
|
|
}
|
|
}
|
|
|
|
// Check for issues
|
|
var issues []string
|
|
|
|
// Count mismatch
|
|
if dbCount != jsonlCount {
|
|
issues = append(issues, fmt.Sprintf("Count mismatch: database has %d issues, JSONL has %d", dbCount, jsonlCount))
|
|
}
|
|
|
|
// Prefix mismatch (only check most common prefix in JSONL)
|
|
if dbPrefix != "" && len(jsonlPrefixes) > 0 {
|
|
var mostCommonPrefix string
|
|
maxCount := 0
|
|
for prefix, count := range jsonlPrefixes {
|
|
if count > maxCount {
|
|
maxCount = count
|
|
mostCommonPrefix = prefix
|
|
}
|
|
}
|
|
|
|
// Only warn if majority of issues have wrong prefix
|
|
if mostCommonPrefix != dbPrefix && maxCount > jsonlCount/2 {
|
|
issues = append(issues, fmt.Sprintf("Prefix mismatch: database uses %q but most JSONL issues use %q", dbPrefix, mostCommonPrefix))
|
|
}
|
|
}
|
|
|
|
// If we found issues, report them
|
|
if len(issues) > 0 {
|
|
// Provide direction-specific guidance
|
|
var fixMsg string
|
|
if dbCount > jsonlCount {
|
|
fixMsg = "Run 'bd doctor --fix' to automatically export DB to JSONL, or manually run 'bd export'"
|
|
} else if jsonlCount > dbCount {
|
|
fixMsg = "Run 'bd doctor --fix' to automatically import JSONL to DB, or manually run 'bd sync --import-only'"
|
|
} else {
|
|
// Equal counts but other issues (like prefix mismatch)
|
|
fixMsg = "Run 'bd doctor --fix' to fix automatically, or manually run 'bd sync --import-only' or 'bd export' depending on which has newer data"
|
|
}
|
|
if strings.Contains(strings.Join(issues, " "), "Prefix mismatch") {
|
|
fixMsg = "Run 'bd import -i " + filepath.Base(jsonlPath) + " --rename-on-import' to fix prefixes"
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: strings.Join(issues, "; "),
|
|
Fix: fixMsg,
|
|
}
|
|
}
|
|
|
|
// Check modification times (only if counts match)
|
|
dbInfo, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to check database file",
|
|
}
|
|
}
|
|
|
|
jsonlInfo, err := os.Stat(jsonlPath)
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "Unable to check JSONL file",
|
|
}
|
|
}
|
|
|
|
if jsonlInfo.ModTime().After(dbInfo.ModTime()) {
|
|
timeDiff := jsonlInfo.ModTime().Sub(dbInfo.ModTime())
|
|
if timeDiff > 30*time.Second {
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusWarning,
|
|
Message: "JSONL is newer than database",
|
|
Fix: "Run 'bd sync --import-only' to import JSONL updates",
|
|
}
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "DB-JSONL Sync",
|
|
Status: StatusOK,
|
|
Message: "Database and JSONL are in sync",
|
|
}
|
|
}
|
|
|
|
// Fix functions
|
|
|
|
// FixDatabaseConfig auto-detects and fixes metadata.json database/JSONL config mismatches
|
|
func FixDatabaseConfig(path string) error {
|
|
return fix.DatabaseConfig(path)
|
|
}
|
|
|
|
// FixDBJSONLSync fixes database-JSONL sync issues by running bd sync --import-only
|
|
func FixDBJSONLSync(path string) error {
|
|
return fix.DBJSONLSync(path)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// getDatabaseVersionFromPath reads the database version from the given path
|
|
func getDatabaseVersionFromPath(dbPath string) string {
|
|
db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro")
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
defer db.Close()
|
|
|
|
// Try to read version from metadata table
|
|
var version string
|
|
err = db.QueryRow("SELECT value FROM metadata WHERE key = 'bd_version'").Scan(&version)
|
|
if err == nil {
|
|
return version
|
|
}
|
|
|
|
// Check if metadata table exists
|
|
var tableName string
|
|
err = db.QueryRow(`
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='metadata'
|
|
`).Scan(&tableName)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return "pre-0.17.5"
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
// CountJSONLIssues counts issues in the JSONL file and returns the count, prefixes, and any error
|
|
func CountJSONLIssues(jsonlPath string) (int, map[string]int, error) {
|
|
// jsonlPath is safe: constructed from filepath.Join(beadsDir, hardcoded name)
|
|
file, err := os.Open(jsonlPath) //nolint:gosec
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed to open JSONL file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
count := 0
|
|
prefixes := make(map[string]int)
|
|
errorCount := 0
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Bytes()
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Parse JSON to get the ID
|
|
var issue map[string]interface{}
|
|
if err := json.Unmarshal(line, &issue); err != nil {
|
|
errorCount++
|
|
continue
|
|
}
|
|
|
|
if id, ok := issue["id"].(string); ok && id != "" {
|
|
count++
|
|
// Extract prefix (everything before the last dash)
|
|
lastDash := strings.LastIndex(id, "-")
|
|
if lastDash != -1 {
|
|
prefixes[id[:lastDash]]++
|
|
} else {
|
|
prefixes[id]++
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return count, prefixes, fmt.Errorf("failed to read JSONL file: %w", err)
|
|
}
|
|
|
|
if errorCount > 0 {
|
|
return count, prefixes, fmt.Errorf("skipped %d malformed lines in JSONL", errorCount)
|
|
}
|
|
|
|
return count, prefixes, nil
|
|
}
|
|
|
|
// countIssuesInJSONLFile counts the number of valid issues in a JSONL file
|
|
// This is a wrapper around CountJSONLIssues that returns only the count
|
|
func countIssuesInJSONLFile(jsonlPath string) int {
|
|
count, _, _ := CountJSONLIssues(jsonlPath)
|
|
return count
|
|
}
|
|
|
|
// detectPrefixFromJSONL detects the most common prefix in a JSONL file
|
|
func detectPrefixFromJSONL(jsonlPath string) string {
|
|
_, prefixes, _ := CountJSONLIssues(jsonlPath)
|
|
if len(prefixes) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Find the most common prefix
|
|
var mostCommonPrefix string
|
|
maxCount := 0
|
|
for prefix, count := range prefixes {
|
|
if count > maxCount {
|
|
maxCount = count
|
|
mostCommonPrefix = prefix
|
|
}
|
|
}
|
|
return mostCommonPrefix
|
|
}
|
|
|
|
// isNoDbModeConfigured checks if no-db: true is set in config.yaml
|
|
// Uses proper YAML parsing to avoid false matches in comments or nested keys
|
|
func isNoDbModeConfigured(beadsDir string) bool {
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
data, err := os.ReadFile(configPath) // #nosec G304 - config file path from beadsDir
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
var cfg localConfig
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return false
|
|
}
|
|
|
|
return cfg.NoDb
|
|
}
|