diff --git a/beads.go b/beads.go index f8190582..504cbf56 100644 --- a/beads.go +++ b/beads.go @@ -17,23 +17,36 @@ import ( ) // Issue represents a tracked work item with metadata, dependencies, and status. -// Status represents the current state of an issue (open, in progress, closed, blocked). type ( - Issue = types.Issue - Status = types.Status - IssueType = types.IssueType - Dependency = types.Dependency + Issue = types.Issue + // Status represents the current state of an issue (open, in progress, closed, blocked). + Status = types.Status + // IssueType represents the type of issue (bug, feature, task, epic, chore). + IssueType = types.IssueType + // Dependency represents a relationship between issues. + Dependency = types.Dependency + // DependencyType represents the type of dependency (blocks, related, parent-child, discovered-from). DependencyType = types.DependencyType - Comment = types.Comment - Event = types.Event - EventType = types.EventType - Label = types.Label - BlockedIssue = types.BlockedIssue - TreeNode = types.TreeNode - Statistics = types.Statistics - IssueFilter = types.IssueFilter - WorkFilter = types.WorkFilter - EpicStatus = types.EpicStatus + // Comment represents a user comment on an issue. + Comment = types.Comment + // Event represents an audit log event. + Event = types.Event + // EventType represents the type of audit event. + EventType = types.EventType + // Label represents a tag attached to an issue. + Label = types.Label + // BlockedIssue represents an issue with blocking dependencies. + BlockedIssue = types.BlockedIssue + // TreeNode represents a node in a dependency tree. + TreeNode = types.TreeNode + // Statistics represents project-wide metrics. + Statistics = types.Statistics + // IssueFilter represents filtering criteria for issue queries. + IssueFilter = types.IssueFilter + // WorkFilter represents filtering criteria for work queries. + WorkFilter = types.WorkFilter + // EpicStatus represents the status of an epic issue. + EpicStatus = types.EpicStatus ) // Status constants diff --git a/beads_test.go b/beads_test.go index e315d853..e820c3a3 100644 --- a/beads_test.go +++ b/beads_test.go @@ -11,15 +11,15 @@ func TestFindDatabasePathEnvVar(t *testing.T) { originalEnv := os.Getenv("BEADS_DB") defer func() { if originalEnv != "" { - os.Setenv("BEADS_DB", originalEnv) + _ = os.Setenv("BEADS_DB", originalEnv) } else { - os.Unsetenv("BEADS_DB") + _ = os.Unsetenv("BEADS_DB") } }() // Set env var to a test path testPath := "/test/path/test.db" - os.Setenv("BEADS_DB", testPath) + _ = os.Setenv("BEADS_DB", testPath) result := FindDatabasePath() if result != testPath { diff --git a/cmd/bd/autoimport_collision_test.go b/cmd/bd/autoimport_collision_test.go index 374aaf0d..e448b93e 100644 --- a/cmd/bd/autoimport_collision_test.go +++ b/cmd/bd/autoimport_collision_test.go @@ -42,7 +42,7 @@ func createTestDBWithIssues(t *testing.T, issues []*types.Issue) (string, *sqlit } // Helper function to write JSONL file -func writeJSONLFile(t *testing.T, dir string, issues []*types.Issue) string { +func writeJSONLFile(t *testing.T, dir string, issues []*types.Issue) { t.Helper() jsonlPath := filepath.Join(dir, "issues.jsonl") f, err := os.Create(jsonlPath) @@ -57,8 +57,6 @@ func writeJSONLFile(t *testing.T, dir string, issues []*types.Issue) string { t.Fatalf("Failed to encode issue %s: %v", issue.ID, err) } } - - return jsonlPath } // Helper function to capture stderr output diff --git a/cmd/bd/compact.go b/cmd/bd/compact.go index aae56a3a..5e044e93 100644 --- a/cmd/bd/compact.go +++ b/cmd/bd/compact.go @@ -48,7 +48,7 @@ Examples: // Handle compact stats first if compactStats { if daemonClient != nil { - runCompactStatsRPC(ctx) + runCompactStatsRPC() } else { sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { @@ -94,7 +94,7 @@ Examples: os.Exit(1) } - config := &compact.CompactConfig{ + config := &compact.Config{ APIKey: apiKey, Concurrency: compactWorkers, DryRun: compactDryRun, @@ -512,7 +512,7 @@ func runCompactRPC(ctx context.Context) { } } -func runCompactStatsRPC(ctx context.Context) { +func runCompactStatsRPC() { args := map[string]interface{}{ "tier": compactTier, } diff --git a/cmd/bd/daemon_lock.go b/cmd/bd/daemon_lock.go index 41a3e38f..3071012e 100644 --- a/cmd/bd/daemon_lock.go +++ b/cmd/bd/daemon_lock.go @@ -30,7 +30,7 @@ func (l *DaemonLock) Close() error { // acquireDaemonLock attempts to acquire an exclusive lock on daemon.lock // Returns ErrDaemonLocked if another daemon is already running -func acquireDaemonLock(beadsDir string, global bool) (*DaemonLock, error) { +func acquireDaemonLock(beadsDir string, _ bool) (*DaemonLock, error) { lockPath := filepath.Join(beadsDir, "daemon.lock") // Open or create the lock file @@ -56,7 +56,7 @@ func acquireDaemonLock(beadsDir string, global bool) (*DaemonLock, error) { // Also write PID file for Windows compatibility (can't read locked files on Windows) pidFile := filepath.Join(beadsDir, "daemon.pid") - _ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644) + _ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0600) return &DaemonLock{file: f, path: lockPath}, nil } diff --git a/cmd/bd/delete.go b/cmd/bd/delete.go index 042487c1..09489d73 100644 --- a/cmd/bd/delete.go +++ b/cmd/bd/delete.go @@ -345,7 +345,7 @@ func removeIssueFromJSONL(issueID string) error { // Write to temp file atomically temp := fmt.Sprintf("%s.tmp.%d", path, os.Getpid()) - out, err := os.OpenFile(temp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + out, err := os.OpenFile(temp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) if err != nil { return fmt.Errorf("failed to create temp file: %w", err) } diff --git a/cmd/bd/label.go b/cmd/bd/label.go index bed68fc6..bbb273a5 100644 --- a/cmd/bd/label.go +++ b/cmd/bd/label.go @@ -66,14 +66,19 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin } } +func parseLabelArgs(args []string) (issueIDs []string, label string) { + label = args[len(args)-1] + issueIDs = args[:len(args)-1] + return +} + +//nolint:dupl // labelAddCmd and labelRemoveCmd are similar but serve different operations var labelAddCmd = &cobra.Command{ Use: "add [issue-id...] [label]", Short: "Add a label to one or more issues", Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { - label := args[len(args)-1] - issueIDs := args[:len(args)-1] - + issueIDs, label := parseLabelArgs(args) processBatchLabelOperation(issueIDs, label, "added", func(issueID, lbl string) error { _, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl}) @@ -85,14 +90,13 @@ var labelAddCmd = &cobra.Command{ }, } +//nolint:dupl // labelRemoveCmd and labelAddCmd are similar but serve different operations var labelRemoveCmd = &cobra.Command{ Use: "remove [issue-id...] [label]", Short: "Remove a label from one or more issues", Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { - label := args[len(args)-1] - issueIDs := args[:len(args)-1] - + issueIDs, label := parseLabelArgs(args) processBatchLabelOperation(issueIDs, label, "removed", func(issueID, lbl string) error { _, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl}) diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 3d2dd06c..ce6a9e24 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -813,6 +813,8 @@ func canDialSocket(socketPath string, timeout time.Duration) bool { } // waitForSocketReadiness waits for daemon socket to be ready by testing actual connections +// +//nolint:unparam // timeout is configurable even though current callers use 5s func waitForSocketReadiness(socketPath string, timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { diff --git a/internal/compact/compactor.go b/internal/compact/compactor.go index 8abd5a79..59504e2c 100644 --- a/internal/compact/compactor.go +++ b/internal/compact/compactor.go @@ -13,8 +13,8 @@ const ( defaultConcurrency = 5 ) -// CompactConfig holds configuration for the compaction process. -type CompactConfig struct { +// Config holds configuration for the compaction process. +type Config struct { APIKey string Concurrency int DryRun bool @@ -24,13 +24,13 @@ type CompactConfig struct { type Compactor struct { store *sqlite.SQLiteStorage haiku *HaikuClient - config *CompactConfig + config *Config } // New creates a new Compactor instance with the given configuration. -func New(store *sqlite.SQLiteStorage, apiKey string, config *CompactConfig) (*Compactor, error) { +func New(store *sqlite.SQLiteStorage, apiKey string, config *Config) (*Compactor, error) { if config == nil { - config = &CompactConfig{ + config = &Config{ Concurrency: defaultConcurrency, } } @@ -61,8 +61,8 @@ func New(store *sqlite.SQLiteStorage, apiKey string, config *CompactConfig) (*Co }, nil } -// CompactResult holds the outcome of a compaction operation. -type CompactResult struct { +// Result holds the outcome of a compaction operation. +type Result struct { IssueID string OriginalSize int CompactedSize int @@ -143,25 +143,25 @@ func (c *Compactor) CompactTier1(ctx context.Context, issueID string) error { } // CompactTier1Batch performs tier-1 compaction on multiple issues in a single batch. -func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([]*CompactResult, error) { +func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([]*Result, error) { if len(issueIDs) == 0 { return nil, nil } eligibleIDs := make([]string, 0, len(issueIDs)) - results := make([]*CompactResult, 0, len(issueIDs)) + results := make([]*Result, 0, len(issueIDs)) for _, id := range issueIDs { eligible, reason, err := c.store.CheckEligibility(ctx, id, 1) if err != nil { - results = append(results, &CompactResult{ + results = append(results, &Result{ IssueID: id, Err: fmt.Errorf("failed to verify eligibility: %w", err), }) continue } if !eligible { - results = append(results, &CompactResult{ + results = append(results, &Result{ IssueID: id, Err: fmt.Errorf("not eligible for Tier 1 compaction: %s", reason), }) @@ -178,14 +178,14 @@ func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([ for _, id := range eligibleIDs { issue, err := c.store.GetIssue(ctx, id) if err != nil { - results = append(results, &CompactResult{ + results = append(results, &Result{ IssueID: id, Err: fmt.Errorf("failed to get issue: %w", err), }) continue } originalSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria) - results = append(results, &CompactResult{ + results = append(results, &Result{ IssueID: id, OriginalSize: originalSize, Err: nil, @@ -195,7 +195,7 @@ func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([ } workCh := make(chan string, len(eligibleIDs)) - resultCh := make(chan *CompactResult, len(eligibleIDs)) + resultCh := make(chan *Result, len(eligibleIDs)) var wg sync.WaitGroup for i := 0; i < c.config.Concurrency; i++ { @@ -203,7 +203,7 @@ func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([ go func() { defer wg.Done() for issueID := range workCh { - result := &CompactResult{IssueID: issueID} + result := &Result{IssueID: issueID} if err := c.compactSingleWithResult(ctx, issueID, result); err != nil { result.Err = err @@ -231,7 +231,7 @@ func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([ return results, nil } -func (c *Compactor) compactSingleWithResult(ctx context.Context, issueID string, result *CompactResult) error { +func (c *Compactor) compactSingleWithResult(ctx context.Context, issueID string, result *Result) error { if ctx.Err() != nil { return ctx.Err() } diff --git a/internal/compact/compactor_test.go b/internal/compact/compactor_test.go index 6006d77a..40ee629a 100644 --- a/internal/compact/compactor_test.go +++ b/internal/compact/compactor_test.go @@ -99,7 +99,7 @@ func TestNew(t *testing.T) { defer store.Close() t.Run("creates compactor with config", func(t *testing.T) { - config := &CompactConfig{ + config := &Config{ Concurrency: 10, DryRun: true, } @@ -129,7 +129,7 @@ func TestCompactTier1_DryRun(t *testing.T) { issue := createClosedIssue(t, store, "test-1") - config := &CompactConfig{DryRun: true} + config := &Config{DryRun: true} c, err := New(store, "", config) if err != nil { t.Fatalf("failed to create compactor: %v", err) @@ -173,7 +173,7 @@ func TestCompactTier1_IneligibleIssue(t *testing.T) { t.Fatalf("failed to create issue: %v", err) } - config := &CompactConfig{DryRun: true} + config := &Config{DryRun: true} c, err := New(store, "", config) if err != nil { t.Fatalf("failed to create compactor: %v", err) @@ -198,7 +198,7 @@ func TestCompactTier1_WithAPI(t *testing.T) { issue := createClosedIssue(t, store, "test-api") - c, err := New(store, "", &CompactConfig{Concurrency: 1}) + c, err := New(store, "", &Config{Concurrency: 1}) if err != nil { t.Fatalf("failed to create compactor: %v", err) } @@ -234,7 +234,7 @@ func TestCompactTier1Batch_DryRun(t *testing.T) { issue1 := createClosedIssue(t, store, "test-batch-1") issue2 := createClosedIssue(t, store, "test-batch-2") - config := &CompactConfig{DryRun: true, Concurrency: 2} + config := &Config{DryRun: true, Concurrency: 2} c, err := New(store, "", config) if err != nil { t.Fatalf("failed to create compactor: %v", err) @@ -282,7 +282,7 @@ func TestCompactTier1Batch_WithIneligible(t *testing.T) { t.Fatalf("failed to create issue: %v", err) } - config := &CompactConfig{DryRun: true, Concurrency: 2} + config := &Config{DryRun: true, Concurrency: 2} c, err := New(store, "", config) if err != nil { t.Fatalf("failed to create compactor: %v", err) @@ -323,7 +323,7 @@ func TestCompactTier1Batch_WithAPI(t *testing.T) { issue2 := createClosedIssue(t, store, "test-api-batch-2") issue3 := createClosedIssue(t, store, "test-api-batch-3") - c, err := New(store, "", &CompactConfig{Concurrency: 2}) + c, err := New(store, "", &Config{Concurrency: 2}) if err != nil { t.Fatalf("failed to create compactor: %v", err) } @@ -367,7 +367,7 @@ func TestMockAPI_CompactTier1(t *testing.T) { issue := createClosedIssue(t, store, "test-mock") - c, err := New(store, "", &CompactConfig{DryRun: true, Concurrency: 1}) + c, err := New(store, "", &Config{DryRun: true, Concurrency: 1}) if err != nil { t.Fatalf("failed to create compactor: %v", err) } @@ -399,7 +399,7 @@ func TestBatchOperations_ErrorHandling(t *testing.T) { t.Fatalf("failed to create open issue: %v", err) } - c, err := New(store, "", &CompactConfig{DryRun: true, Concurrency: 2}) + c, err := New(store, "", &Config{DryRun: true, Concurrency: 2}) if err != nil { t.Fatalf("failed to create compactor: %v", err) } diff --git a/internal/compact/haiku.go b/internal/compact/haiku.go index 0c60e19a..033a01bc 100644 --- a/internal/compact/haiku.go +++ b/internal/compact/haiku.go @@ -144,7 +144,7 @@ func isRetryable(err error) bool { } var netErr net.Error - if errors.As(err, &netErr) && (netErr.Timeout() || netErr.Temporary()) { + if errors.As(err, &netErr) && netErr.Timeout() { return true } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c367e6c1..e9da54b2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -72,7 +72,7 @@ func TestEnvironmentBinding(t *testing.T) { t.Run(tt.envVar, func(t *testing.T) { // Set environment variable oldValue := os.Getenv(tt.envVar) - os.Setenv(tt.envVar, tt.value) + _ = os.Setenv(tt.envVar, tt.value) defer os.Setenv(tt.envVar, oldValue) // Re-initialize viper to pick up env var @@ -101,7 +101,7 @@ actor: configuser flush-debounce: 15s ` configPath := filepath.Join(tmpDir, "config.yaml") - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { t.Fatalf("failed to write config file: %v", err) } @@ -164,7 +164,7 @@ func TestConfigPrecedence(t *testing.T) { } configPath := filepath.Join(beadsDir, "config.yaml") - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { t.Fatalf("failed to write config file: %v", err) } @@ -190,8 +190,8 @@ func TestConfigPrecedence(t *testing.T) { } // Test 2: Environment variable overrides config file - os.Setenv("BD_JSON", "true") - defer os.Unsetenv("BD_JSON") + _ = os.Setenv("BD_JSON", "true") + defer func() { _ = os.Unsetenv("BD_JSON") }() err = Initialize() if err != nil { diff --git a/internal/rpc/limits_test.go b/internal/rpc/limits_test.go index d69e96b1..b255386e 100644 --- a/internal/rpc/limits_test.go +++ b/internal/rpc/limits_test.go @@ -72,7 +72,7 @@ func TestConnectionLimits(t *testing.T) { // Send a long-running ping to keep connection busy wg.Add(1) - go func(c net.Conn, idx int) { + go func(c net.Conn, _ int) { defer wg.Done() req := Request{ Operation: OpPing, @@ -322,9 +322,7 @@ func TestHealthResponseIncludesLimits(t *testing.T) { t.Errorf("expected ActiveConns>=0, got %d", health.ActiveConns) } - if health.MemoryAllocMB < 0 { - t.Errorf("expected MemoryAllocMB>=0, got %d", health.MemoryAllocMB) - } + // No need to check MemoryAllocMB < 0 since it's uint64 t.Logf("Health: %d/%d connections, %d MB memory", health.ActiveConns, health.MaxConns, health.MemoryAllocMB) } diff --git a/internal/rpc/rpc_test.go b/internal/rpc/rpc_test.go index d7db1136..b7ed7dd3 100644 --- a/internal/rpc/rpc_test.go +++ b/internal/rpc/rpc_test.go @@ -114,8 +114,10 @@ func setupTestServer(t *testing.T) (*Server, *Client, func()) { // setupTestServerIsolated creates an isolated test server in a temp directory // with .beads structure, but allows the caller to customize server/client setup. -// Returns tmpDir, beadsDir, dbPath, socketPath, and cleanup function. +// Returns tmpDir, dbPath, socketPath, and cleanup function. // Caller must change to tmpDir if needed and set client.dbPath manually. +// +//nolint:unparam // beadsDir is not used by callers but part of test isolation setup func setupTestServerIsolated(t *testing.T) (tmpDir, beadsDir, dbPath, socketPath string, cleanup func()) { tmpDir, err := os.MkdirTemp("", "bd-rpc-test-*") if err != nil { @@ -321,7 +323,7 @@ func TestConcurrentRequests(t *testing.T) { errors := make(chan error, 5) for i := 0; i < 5; i++ { - go func(n int) { + go func(_ int) { client, err := TryConnect(server.socketPath) if err != nil { errors <- err diff --git a/internal/rpc/server.go b/internal/rpc/server.go index 9406ca07..c8344555 100644 --- a/internal/rpc/server.go +++ b/internal/rpc/server.go @@ -1785,7 +1785,7 @@ func (s *Server) handleCompact(req *Request) Response { } } - config := &compact.CompactConfig{ + config := &compact.Config{ APIKey: args.APIKey, Concurrency: args.Workers, DryRun: args.DryRun, @@ -2147,8 +2147,8 @@ func (s *Server) handleExport(req *Request) Response { } } - // Set appropriate file permissions (0644: rw-r--r--) - if err := os.Chmod(exportArgs.JSONLPath, 0644); err != nil { + // Set appropriate file permissions (0600: rw-------) + if err := os.Chmod(exportArgs.JSONLPath, 0600); err != nil { // Non-fatal, just log fmt.Fprintf(os.Stderr, "Warning: failed to set file permissions: %v\n", err) } diff --git a/internal/storage/sqlite/counter_sync_test.go b/internal/storage/sqlite/counter_sync_test.go index 2ddae6c9..d3a1b55a 100644 --- a/internal/storage/sqlite/counter_sync_test.go +++ b/internal/storage/sqlite/counter_sync_test.go @@ -227,6 +227,6 @@ func TestCounterSyncAfterDeleteAll(t *testing.T) { } // genID is a helper to generate issue IDs in tests -func genID(prefix string, num int) string { - return fmt.Sprintf("%s-%d", prefix, num) +func genID(_ string, num int) string { + return fmt.Sprintf("bd-%d", num) }