Skip to content

Conversation

@connerohnesorge
Copy link
Owner

@connerohnesorge connerohnesorge commented Feb 3, 2026

Summary by CodeRabbit

  • New Features

    • Discovery now uses a per-directory cache for faster, more consistent results and caches discovery errors to avoid repeated work.
    • New helpers expose single-root checks and multi-root detection for CLI workflows.
  • Refactor

    • Discovery logic reorganized with dedicated Git-root detection and smarter skipping of large/hidden directories; traversal depth reduced for efficiency.
  • Documentation

    • Added an internal guidance document describing project layout and workflows.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

Walkthrough

Refactors Spectr root discovery by extracting downward traversal, Git-root detection, and directory-skip heuristics into new modules; reduces maxDiscoveryDepth from 10 to 5; adds a mutex-protected per-working-directory cache and helpers in cmd/discovery.go; updates tests and adds CLAUDE.md.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md
New internal guidance for AI-assisted development: commands, architecture, project layout, workflows, testing, and packaging.
Discovery refactor — new modules
internal/discovery/downward.go, internal/discovery/git_root.go, internal/discovery/skip_dirs.go
Add downward traversal logic, cached Git-root discovery, and a skip-dir set/heuristic for discovery traversal.
Core discovery
internal/discovery/roots.go
Removed in-file downward discovery helpers (moved to new modules) and reduced maxDiscoveryDepth from 10 to 5; retains upward discovery, deduplication and sorting.
Public-facing discovery API & cache
cmd/discovery.go
Added per-working-directory cache with mutex protection, error caching, ResetDiscoveryCache, GetSingleRoot, and HasMultipleRoots.
Tests
internal/discovery/roots_test.go
Updated depth-limit tests and path setups to reflect new maxDiscoveryDepth = 5.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Caller
participant CmdCache as "cmd/discovery cache"
participant Finder as "discovery.FindSpectrRoots"
participant Downward as "downward discovery"
participant GitCache as "git_root cache"
participant FS as "Filesystem/.git"

Caller->>CmdCache: GetDiscoveredRoots(cwd)
alt cache hit
CmdCache-->>Caller: cached roots/error
else cache miss
CmdCache->>Finder: FindSpectrRoots(cwd)
Finder->>Downward: traverse dirs (respect skip set, maxDepth=5)
Downward->>GitCache: findGitRoot(startPath)
GitCache->>FS: check .git up the tree (uncached if needed)
FS-->>GitCache: .git location or none
GitCache-->>Downward: git root result
Downward-->>Finder: collected roots
Finder->>CmdCache: store roots/error
CmdCache-->>Caller: roots/error
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'fix select/archive' does not align with the changeset, which primarily refactors discovery caching, Git root detection, and depth limits—not select/archive functionality. Update the title to reflect the actual changes, such as 'Refactor discovery caching and Git root detection' or 'Optimize root discovery with caching and reduced depth limit'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf-fix

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/discovery/roots.go (1)

237-238: ⚠️ Potential issue | 🔴 Critical

Bug: filepath.SplitList splits PATH-style strings, not path components.

filepath.SplitList is designed for splitting environment variables like PATH (colon/semicolon separated), not for counting path depth. This will return incorrect results—typically 1 for any relative path—causing incorrect sorting.

Use strings.Count with filepath.Separator or iterate with filepath.Split instead.

Proposed fix
 import (
 	"fmt"
 	"os"
 	"path/filepath"
 	"sort"
+	"strings"
 )
-		// Compare by number of path separators (shorter = closer)
-		// "." has 0 separators (closest)
-		// ".." has 1 separator
-		// "../.." has 2 separators, etc.
-		depthI := len(filepath.SplitList(relI))
-		depthJ := len(filepath.SplitList(relJ))
+		// Compare by number of path components (fewer = closer)
+		// "." has 1 component (closest)
+		// ".." has 1 component
+		// "../.." has 2 components, etc.
+		depthI := strings.Count(relI, string(filepath.Separator)) + 1
+		depthJ := strings.Count(relJ, string(filepath.Separator)) + 1
🤖 Fix all issues with AI agents
In `@internal/discovery/downward.go`:
- Around line 96-98: The boolean expression "info.IsDir() || !info.IsDir()" is
always true, so in the return inside downward.go replace the redundant test with
a simple existence check: return err == nil; in other words, remove the
"(info.IsDir() || !info.IsDir())" part and rely on the error from os.Stat (the
variable err) to determine whether ".git" exists (using the existing info
variable).
- Around line 46-49: The code incorrectly uses filepath.SplitList to count
directory components; replace that with splitting the cleaned relative path on
the OS path separator: after computing relPath with filepath.Rel (and handling
relErr), call filepath.Clean(relPath) and then split on string(os.PathSeparator)
(or use strings.Count+1 semantics) to compute the component count; update the
code references around relPath/relErr and add imports for strings and os if
needed.

In `@internal/discovery/skip_dirs.go`:
- Line 25: The "*.egg-info" key in the skipDirs map is ineffective because
literal map lookup won't match directories like "mypkg.egg-info"; either remove
the "*.egg-info" entry from the skipDirs map or update shouldSkipDirectory to
check suffixes (e.g., strings.HasSuffix(name, ".egg-info")) in addition to
existing exact and glob checks so directories ending with ".egg-info" are
correctly skipped; reference the skipDirs map entry "*.egg-info" and the
shouldSkipDirectory function when making the change.
🧹 Nitpick comments (3)
GEMINI.md (1)

39-39: Minor: Add language specifier to fenced code block.

The static analysis tool flagged this code block as missing a language specifier. Consider adding a language identifier for proper syntax highlighting.

-```
+```text
 cmd/                    # CLI commands (thin layer using Kong framework)
cmd/discovery.go (1)

44-47: Consider edge case: nil slice vs empty slice.

If FindSpectrRoots returns a nil slice (rather than an empty slice []SpectrRoot{}), the cache check cachedRoots != nil will be false, causing repeated discovery calls. Depending on FindSpectrRoots's behavior, consider explicitly marking cache validity:

+	cacheValid     bool

However, reviewing FindSpectrRoots, it appears to always return a non-nil slice on success, so this is likely fine in practice.

internal/discovery/git_root.go (1)

52-60: Consider simplifying the .git existence check.

The two conditions on lines 53-55 and 58-60 can be combined since both return current when err == nil. The current structure is correct but verbose.

Proposed simplification
 func findGitRootUncached(startPath string) string {
 	current := startPath
 	for {
 		gitDir := filepath.Join(current, gitDirName)
-		info, err := os.Stat(gitDir)
-		if err == nil && info.IsDir() {
-			return current
-		}
-
-		// Also check for git worktree files (where .git is a file, not dir)
-		if err == nil && !info.IsDir() {
+		// Check for .git directory or worktree file
+		if _, err := os.Stat(gitDir); err == nil {
 			return current
 		}

 		parent := filepath.Dir(current)

Comment on lines +46 to +49
relPath, relErr := filepath.Rel(absStartPath, path)
if relErr == nil {
return len(filepath.SplitList(relPath))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: filepath.SplitList splits PATH-style strings, not path components.

Same issue as in roots.go: filepath.SplitList is for environment variables like PATH, not for counting directory depth. This will produce incorrect depth calculations.

Proposed fix
+import "strings"
 	// Fallback: calculate depth from path segments
 	relPath, relErr := filepath.Rel(absStartPath, path)
 	if relErr == nil {
-		return len(filepath.SplitList(relPath))
+		if relPath == "." {
+			return 0
+		}
+		return strings.Count(relPath, string(filepath.Separator)) + 1
 	}
🤖 Prompt for AI Agents
In `@internal/discovery/downward.go` around lines 46 - 49, The code incorrectly
uses filepath.SplitList to count directory components; replace that with
splitting the cleaned relative path on the OS path separator: after computing
relPath with filepath.Rel (and handling relErr), call filepath.Clean(relPath)
and then split on string(os.PathSeparator) (or use strings.Count+1 semantics) to
compute the component count; update the code references around relPath/relErr
and add imports for strings and os if needed.

".tox": {},
".nox": {},
".eggs": {},
"*.egg-info": {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Glob pattern *.egg-info won't match actual directories.

The map uses literal string keys, so "*.egg-info" will only match a directory literally named *.egg-info, not directories like mypackage.egg-info. Consider removing this entry or implementing suffix matching in shouldSkipDirectory.

Proposed fix

Option 1: Remove the ineffective entry:

-	"*.egg-info":     {},

Option 2: Add suffix check in shouldSkipDirectory:

 func shouldSkipDirectory(dirName string) bool {
 	// Fast path: check the pre-computed set
 	if _, skip := skipDirsSet[dirName]; skip {
 		return true
 	}

+	// Check for .egg-info suffix (Python package metadata)
+	if len(dirName) > 9 && dirName[len(dirName)-9:] == ".egg-info" {
+		return true
+	}
+
 	// Skip hidden directories (except .git which is handled separately)
 	if len(dirName) > 1 && dirName[0] == '.' && dirName != gitDirName {
 		return true
 	}

 	return false
 }
🤖 Prompt for AI Agents
In `@internal/discovery/skip_dirs.go` at line 25, The "*.egg-info" key in the
skipDirs map is ineffective because literal map lookup won't match directories
like "mypkg.egg-info"; either remove the "*.egg-info" entry from the skipDirs
map or update shouldSkipDirectory to check suffixes (e.g.,
strings.HasSuffix(name, ".egg-info")) in addition to existing exact and glob
checks so directories ending with ".egg-info" are correctly skipped; reference
the skipDirs map entry "*.egg-info" and the shouldSkipDirectory function when
making the change.

@connerohnesorge connerohnesorge merged commit ba210e4 into main Feb 4, 2026
12 checks passed
@connerohnesorge connerohnesorge deleted the perf-fix branch February 4, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant