feat: scoring and frecency (#37)

Enhance lookup mechanics and priority propagation (`spec/lookup.go`)
Remove the requirement of typing a leading `-` when searching for
options, allowing flag suggestions to surface naturally during input
Accurately forward the `Priority` field from registry specs into the
`Suggestion` struct so the scoring engine evaluates relevance properly

Integrate scoring into the main suggestion pipeline
(`root/suggestions.go`)
Collect context signals using `scoring.CollectSignals(cwd, query,
rootCmd, store)` with the live working directory retrieved from
`spec.GetCWD()`
Pass the entire deduplicated list across spec, history and AI items
through `scoring.Score(deduped, signals)` to compute scores and sort by
descending order before rendering on screen

Record execution history in Frecency Store (`root/wrapper.go`)
Save the last executed command to `lastSubmittedCommand` whenever Enter
is pressed or a suggestion is selected
Trigger `store.Record(...)` asynchronously within the `IRIS_CMD_STOP`
hook using a dedicated goroutine guarded by panic recovery and timeout
limits, preventing blocks on the main shell thread

Optimize workspace identification for AI
(`internal/ai/context_provider.go`)
Use `workspace.DetectCached(cwd)` to instantly fetch ecosystem context
across Git, Node, Go, Rust, Python and Docker alongside active git
branch state
This caching mechanism avoids redundant subprocess execution when
outside git repositories, significantly speeding up prompt context
generation
This commit is contained in:
VERSE
2026-07-13 17:56:29 +07:00
committed by GitHub
parent f54e97a6c0
commit 860b475b53
24 changed files with 1706 additions and 65 deletions
+160
View File
@@ -0,0 +1,160 @@
package scoring
import (
"strings"
"github.com/versenilvis/iris/internal/workspace"
)
type ContextRule interface {
Match(ws workspace.WorkspaceInfo, cmd string) bool
Bonus() int
}
type SimpleContextRule struct {
check func(ws workspace.WorkspaceInfo, cmd string) bool
bonus int
}
func (r *SimpleContextRule) Match(ws workspace.WorkspaceInfo, cmd string) bool {
return r.check(ws, cmd)
}
func (r *SimpleContextRule) Bonus() int {
return r.bonus
}
var DefaultContextRules = []ContextRule{
// Git rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasGit && (strings.HasPrefix(cmd, "git status") || strings.HasPrefix(cmd, "git diff") ||
strings.HasPrefix(cmd, "git add") || strings.HasPrefix(cmd, "git push") ||
strings.HasPrefix(cmd, "git pull") || strings.HasPrefix(cmd, "git commit") ||
strings.HasPrefix(cmd, "git switch") || strings.HasPrefix(cmd, "git checkout") ||
strings.HasPrefix(cmd, "git branch"))
},
bonus: 40,
},
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasGit && (strings.HasPrefix(cmd, "git init") || strings.HasPrefix(cmd, "git clone"))
},
bonus: -50,
},
// Node.js & Bun rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasNodeProject && (strings.HasPrefix(cmd, "npm run ") || strings.HasPrefix(cmd, "pnpm run ") ||
strings.HasPrefix(cmd, "yarn run ") || strings.HasPrefix(cmd, "bun run ") ||
strings.HasPrefix(cmd, "npm test") || strings.HasPrefix(cmd, "npm start") ||
strings.HasPrefix(cmd, "bun test") || strings.HasPrefix(cmd, "bun start"))
},
bonus: 50,
},
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasNodeProject && (strings.HasPrefix(cmd, "npm install") || strings.HasPrefix(cmd, "npm i ") ||
strings.HasPrefix(cmd, "pnpm add") || strings.HasPrefix(cmd, "yarn add") ||
strings.HasPrefix(cmd, "bun install") || strings.HasPrefix(cmd, "bun add"))
},
bonus: 40,
},
// Go rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasGoProject && (strings.HasPrefix(cmd, "go test") || strings.HasPrefix(cmd, "go run") ||
strings.HasPrefix(cmd, "go build") || strings.HasPrefix(cmd, "go mod tidy") ||
strings.HasPrefix(cmd, "go vet") || strings.HasPrefix(cmd, "go fmt"))
},
bonus: 50,
},
// Rust rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasRustProject && (strings.HasPrefix(cmd, "cargo test") || strings.HasPrefix(cmd, "cargo run") ||
strings.HasPrefix(cmd, "cargo build") || strings.HasPrefix(cmd, "cargo check") ||
strings.HasPrefix(cmd, "cargo clippy"))
},
bonus: 50,
},
// Python rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasPythonProject && (strings.HasPrefix(cmd, "pytest") || strings.HasPrefix(cmd, "python main.py") ||
strings.HasPrefix(cmd, "pip install") || strings.HasPrefix(cmd, "poetry run ") ||
strings.HasPrefix(cmd, "uv run "))
},
bonus: 50,
},
// Justfile rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasJustfile && strings.HasPrefix(cmd, "just ")
},
bonus: 50,
},
// Makefile & C/C++ rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasMakefile && strings.HasPrefix(cmd, "make")
},
bonus: 50,
},
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasMakefile && (strings.HasPrefix(cmd, "gcc ") || strings.HasPrefix(cmd, "g++ ") ||
strings.HasPrefix(cmd, "clang ") || strings.HasPrefix(cmd, "cmake "))
},
bonus: 40,
},
// Docker & K8s rules
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasDockerfile && (strings.HasPrefix(cmd, "docker build") || strings.HasPrefix(cmd, "docker compose up") ||
strings.HasPrefix(cmd, "docker compose down") || strings.HasPrefix(cmd, "docker compose logs"))
},
bonus: 40,
},
&SimpleContextRule{
check: func(ws workspace.WorkspaceInfo, cmd string) bool {
return ws.HasK8s && (strings.HasPrefix(cmd, "kubectl get ") || strings.HasPrefix(cmd, "kubectl apply -f ") ||
strings.HasPrefix(cmd, "kubectl logs ") || strings.HasPrefix(cmd, "kubectl describe ") ||
strings.HasPrefix(cmd, "helm upgrade ") || strings.HasPrefix(cmd, "helm install "))
},
bonus: 40,
},
}
func ApplyContextRules(ws workspace.WorkspaceInfo, cmd string) int {
return ApplyCustomContextRules(ws, cmd, DefaultContextRules)
}
func ApplyCustomContextRules(ws workspace.WorkspaceInfo, cmd string, rules []ContextRule) int {
cmd = strings.TrimSpace(cmd)
if cmd == "" {
return 0
}
total := 0
for _, rule := range rules {
if rule.Match(ws, cmd) {
total += rule.Bonus()
}
}
if total > 100 {
return 100
}
if total < -100 {
return -100
}
return total
}