feat: AI suggestion (#34)
## Feat * feat(ai): add AI suggestion engine, context gathering, and ghost text overlay (`v0.3.0`) ## Security & Perf * fix(ai): restrict background `--help` execution to a hardcoded command allowlist to prevent RCE * fix(root): defer context cancellation in goroutine to prevent resource leaks * perf(ai): implement LRU eviction and maximum size limit for `ProviderCache` * perf(ai): truncate command context output (`CommandContextProvider.Gather`) to 1000 characters to save tokens ## Bug Fixes * fix(ai): add mutex synchronization and thread-safe snapshotting to `AIEngine.RegisterProvider` and `GatherDynamicContext` * fix(ai): return a copy of fresh `Suggestion` in `AIEngine.Suggest` to prevent cache mutation * fix(ai): use length-prefixed encoding in `EnvSnapshot.Hash` to prevent delimiter collisions * fix(ai): skip variable assignment lines containing `=` when extracting Makefile targets * fix(runner): check `scanner.Err()` and return `nil` on scan errors in justfile generator * test(ci): rename `git commit` to `git checkout` in overlay test to resolve CI `typos` false positive
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/versenilvis/iris/spec"
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
data string
|
||||
expireTime time.Time
|
||||
}
|
||||
|
||||
type ProviderCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]cacheEntry
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func NewProviderCache(ttl time.Duration) *ProviderCache {
|
||||
if ttl == 0 {
|
||||
ttl = 4 * time.Second
|
||||
}
|
||||
return &ProviderCache{
|
||||
entries: make(map[string]cacheEntry),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderCache) GetOrGather(ctx context.Context, p ContextProvider) string {
|
||||
c.mu.Lock()
|
||||
entry, ok := c.entries[p.Name()]
|
||||
if ok && time.Now().Before(entry.expireTime) {
|
||||
c.mu.Unlock()
|
||||
return entry.data
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
data, err := p.Gather(ctx)
|
||||
if err != nil || ctx.Err() != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if len(c.entries) >= 50 {
|
||||
now := time.Now()
|
||||
for k, v := range c.entries {
|
||||
if now.After(v.expireTime) {
|
||||
delete(c.entries, k)
|
||||
}
|
||||
}
|
||||
if len(c.entries) >= 50 {
|
||||
c.entries = make(map[string]cacheEntry)
|
||||
}
|
||||
}
|
||||
c.entries[p.Name()] = cacheEntry{
|
||||
data: data,
|
||||
expireTime: time.Now().Add(c.ttl),
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return data
|
||||
}
|
||||
|
||||
func (c *ProviderCache) Clear() {
|
||||
c.mu.Lock()
|
||||
c.entries = make(map[string]cacheEntry)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
type ContextCache struct {
|
||||
mu sync.Mutex
|
||||
lastSnapshotHash string
|
||||
lastSuggestion *spec.Suggestion
|
||||
lastFetchedAt time.Time
|
||||
}
|
||||
|
||||
func NewContextCache() *ContextCache {
|
||||
return &ContextCache{}
|
||||
}
|
||||
|
||||
func (c *ContextCache) ShouldCallAI(snap EnvSnapshot, minInterval time.Duration) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
hash := snap.Hash()
|
||||
if hash == c.lastSnapshotHash {
|
||||
return false
|
||||
}
|
||||
if time.Since(c.lastFetchedAt) < minInterval {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ContextCache) GetCachedSuggestion(snap EnvSnapshot) *spec.Suggestion {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if snap.Hash() == c.lastSnapshotHash {
|
||||
return c.lastSuggestion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ContextCache) Update(snap EnvSnapshot, sugg *spec.Suggestion) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastSnapshotHash = snap.Hash()
|
||||
c.lastSuggestion = sugg
|
||||
c.lastFetchedAt = time.Now()
|
||||
}
|
||||
|
||||
func (c *ContextCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastSnapshotHash = ""
|
||||
c.lastSuggestion = nil
|
||||
}
|
||||
Reference in New Issue
Block a user