Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions cmd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func hookSessionStart(root string) error {

// Show last session context if resuming work
if len(lastSessionEvents) > 0 {
showLastSessionContext(lastSessionEvents)
showLastSessionContext(root, lastSessionEvents)
}

return nil
Expand Down Expand Up @@ -167,7 +167,7 @@ func getLastSessionEvents(root string) []string {
}

// showLastSessionContext displays what was worked on in previous session
func showLastSessionContext(events []string) {
func showLastSessionContext(root string, events []string) {
// Extract unique files from events
files := make(map[string]string) // file -> last operation
for _, line := range events {
Expand All @@ -185,6 +185,16 @@ func showLastSessionContext(events []string) {
return
}

// Fix atomic save artifacts: editors often do write-to-temp + rename,
// which fsnotify sees as REMOVE. If file still exists, it was edited.
for file, op := range files {
if strings.EqualFold(op, "REMOVE") || strings.EqualFold(op, "RENAME") {
if _, err := os.Stat(filepath.Join(root, file)); err == nil {
files[file] = "edited"
}
}
}

fmt.Println()
fmt.Println("🕐 Last session worked on:")
count := 0
Expand Down
179 changes: 45 additions & 134 deletions render/depgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,134 +22,6 @@ func titleCase(s string) string {
return strings.Join(words, " ")
}

// Language compatibility groups
var langGroups = map[string]string{
"python": "python",
"go": "go",
"javascript": "js",
"typescript": "js",
"rust": "rust",
"ruby": "ruby",
"c": "c",
"cpp": "c",
"java": "java",
"swift": "swift",
"bash": "bash",
"kotlin": "kotlin",
"csharp": "csharp",
"php": "php",
"lua": "lua",
"scala": "scala",
"elixir": "elixir",
"solidity": "solidity",
}

// Standard library names to filter out
var stdlibNames = map[string]bool{
// Go stdlib
"errors": true, "fmt": true, "io": true, "os": true, "path": true, "sync": true, "time": true, "context": true, "http": true,
"net": true, "bytes": true, "strings": true, "strconv": true, "sort": true, "flag": true, "log": true, "bufio": true,
"encoding": true, "testing": true, "runtime": true, "unsafe": true, "reflect": true, "regexp": true,
// Python stdlib
"logging": true, "typing": true, "collections": true, "datetime": true, "json": true, "sys": true, "re": true,
"pathlib": true, "hashlib": true, "base64": true, "asyncio": true, "enum": true, "functools": true, "random": true,
"math": true, "copy": true, "itertools": true, "contextlib": true,
// JS/TS common
"fs": true, "util": true, "events": true, "stream": true, "crypto": true, "https": true,
"react": true, "filepath": true, "embed": true,
}

// normalizeImport normalizes an import string
func normalizeImport(imp, lang string) string {
imp = strings.Trim(imp, "\"'")
if strings.Contains(imp, "/") {
parts := strings.Split(imp, "/")
imp = parts[len(parts)-1]
}
if strings.Contains(imp, ".") && !strings.HasPrefix(imp, ".") {
parts := strings.Split(imp, ".")
imp = parts[len(parts)-1]
}
// Remove file extensions
extPattern := regexp.MustCompile(`\.(py|go|js|ts|jsx|tsx|rb|rs|c|h|cpp|hpp|java|swift)$`)
imp = extPattern.ReplaceAllString(imp, "")
return strings.ToLower(imp)
}

// findInternalDeps finds which files import which other files
func findInternalDeps(files []scanner.FileAnalysis) map[string][]string {
// Build lookup: name -> list of (path, language_group)
type fileInfo struct {
path string
langGroup string
}
nameToInfos := make(map[string][]fileInfo)

for _, f := range files {
langGroup := langGroups[f.Language]
if langGroup == "" {
langGroup = f.Language
}
basename := filepath.Base(f.Path)
extPattern := regexp.MustCompile(`\.[^.]+$`)
name := strings.ToLower(extPattern.ReplaceAllString(basename, ""))
nameToInfos[name] = append(nameToInfos[name], fileInfo{f.Path, langGroup})
}

deps := make(map[string][]string)

for _, f := range files {
srcLang := f.Language
srcGroup := langGroups[srcLang]
if srcGroup == "" {
srcGroup = srcLang
}

for _, imp := range f.Imports {
// Skip stdlib-looking imports
if !strings.Contains(imp, "/") && !strings.Contains(imp, ".") {
if stdlibNames[strings.ToLower(imp)] {
continue
}
}

norm := normalizeImport(imp, srcLang)
if stdlibNames[norm] {
continue
}

if infos, ok := nameToInfos[norm]; ok {
srcBasename := filepath.Base(f.Path)
for _, info := range infos {
if info.path == f.Path {
continue // Skip self
}
if srcGroup != info.langGroup {
continue // Skip cross-language
}
targetName := filepath.Base(info.path)
if targetName == srcBasename {
continue // Skip same-basename
}
// Check if already added
found := false
for _, d := range deps[f.Path] {
if d == targetName {
found = true
break
}
}
if !found {
deps[f.Path] = append(deps[f.Path], targetName)
}
}
}
}
}

return deps
}

// getSystemName infers a system/component name from directory path
func getSystemName(dirPath string) string {
parts := strings.Split(strings.ReplaceAll(dirPath, "\\", "/"), "/")
Expand Down Expand Up @@ -195,14 +67,53 @@ func Depgraph(project scanner.DepsProject) {
internalNames[name] = true
}

internalDeps := findInternalDeps(files)
// Use BuildFileGraph for accurate file-level dependency resolution
fg, err := scanner.BuildFileGraph(project.Root)
var internalDeps map[string][]string
var depCounts map[string]int
if err == nil && fg != nil {
// Build set of files we're displaying (may be filtered by --diff)
displayedFiles := make(map[string]bool)
for _, f := range files {
displayedFiles[f.Path] = true
}

// Count dependencies on each file
depCounts := make(map[string]int)
for _, targets := range internalDeps {
for _, target := range targets {
depCounts[target]++
// Filter imports to only include displayed files
internalDeps = make(map[string][]string)
for file, imports := range fg.Imports {
if !displayedFiles[file] {
continue
}
var filtered []string
for _, imp := range imports {
if displayedFiles[imp] {
filtered = append(filtered, imp)
}
}
if len(filtered) > 0 {
internalDeps[file] = filtered
}
}

// Count importers only among displayed files
depCounts = make(map[string]int)
for file, importers := range fg.Importers {
if !displayedFiles[file] {
continue
}
count := 0
for _, imp := range importers {
if displayedFiles[imp] {
count++
}
}
if count > 0 {
depCounts[file] = count
}
}
} else {
internalDeps = make(map[string][]string)
depCounts = make(map[string]int)
}

// Group by top-level system
Expand Down
8 changes: 7 additions & 1 deletion scanner/filegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ func BuildFileGraph(root string) (*FileGraph, error) {

for _, imp := range a.Imports {
resolved := fuzzyResolve(imp, a.Path, idx, fg.Module)
resolvedImports = append(resolvedImports, resolved...)
// Only count imports that resolve to exactly one file.
// If an import resolves to multiple files, it's a package/module
// import (Go, Python, Rust, etc.) not a file-level import.
// This ensures hub detection works correctly across all languages.
if len(resolved) == 1 {
resolvedImports = append(resolvedImports, resolved[0])
}
}

if len(resolvedImports) > 0 {
Expand Down
Loading