refactor(doctor): split doctor.go into modular package files (#653)
* refactor(doctor): split doctor.go into modular package files Split the 3,171-line doctor.go into logical sub-files within the cmd/bd/doctor/ package, reducing the main file to 834 lines (74% reduction). New package structure: - types.go: DoctorCheck struct and status constants - installation.go: CheckInstallation, CheckMultipleDatabases, CheckPermissions - git.go: CheckGitHooks, CheckMergeDriver, CheckSyncBranch* checks - database.go: CheckDatabaseVersion, CheckSchemaCompatibility, CheckDatabaseJSONLSync - version.go: CheckCLIVersion, CheckMetadataVersionTracking, CompareVersions - integrity.go: CheckIDFormat, CheckDependencyCycles, CheckTombstones - daemon.go: CheckDaemonStatus, CheckVersionMismatch - quick.go: Quick checks for sync-branch and hooks Updated tests to use exported doctor.CheckXxx() functions and doctor.StatusXxx constants. * fix(doctor): suppress gosec G204 false positives Add #nosec G204 comments to exec.Command calls in CheckSyncBranchHealth where variables come from trusted sources (config files or hardcoded values like "main"/"master"/"origin"), not untrusted user input.
This commit is contained in:
608
cmd/bd/doctor/database.go
Normal file
608
cmd/bd/doctor/database.go
Normal file
@@ -0,0 +1,608 @@
|
||||
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"},
|
||||
"dependencies": {"issue_id", "depends_on_id", "type"},
|
||||
"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(),
|
||||
}
|
||||
}
|
||||
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(),
|
||||
}
|
||||
}
|
||||
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: "Database may need recovery. Export with 'bd export' if possible, then restore from backup or reinitialize",
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDatabaseJSONLSync checks if database and JSONL are in sync
|
||||
func CheckDatabaseJSONLSync(path string) DoctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
|
||||
// Find JSONL file
|
||||
var jsonlPath string
|
||||
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 {
|
||||
return DoctorCheck{
|
||||
Name: "DB-JSONL Sync",
|
||||
Status: StatusWarning,
|
||||
Message: "Unable to read JSONL file",
|
||||
Detail: jsonlErr.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return DoctorCheck{
|
||||
Name: "DB-JSONL Sync",
|
||||
Status: StatusWarning,
|
||||
Message: strings.Join(issues, "; "),
|
||||
Fix: "Run 'bd sync --import-only' to import JSONL updates or 'bd import -i issues.jsonl --rename-on-import' to fix prefixes",
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user