tracker.go

 1package skills
 2
 3import "sync"
 4
 5// Tracker tracks which skills have been loaded (read) during a session.
 6// It is safe for concurrent use.
 7//
 8// Note: Tracking is name-based and limited to active skills only. If a builtin
 9// skill is overridden by a user skill, only the user skill (which is active)
10// can be marked as loaded. This prevents misattribution when reading builtin
11// files that have been overridden.
12type Tracker struct {
13	mu          sync.RWMutex
14	loaded      map[string]bool
15	activeNames map[string]bool // Set of active skill names (post-dedup, post-filter)
16}
17
18// NewTracker creates a new skill tracker with the given active skill names.
19// Only skills in activeSkills can be marked as loaded.
20func NewTracker(activeSkills []*Skill) *Tracker {
21	activeNames := make(map[string]bool, len(activeSkills))
22	for _, s := range activeSkills {
23		activeNames[s.Name] = true
24	}
25	return &Tracker{
26		loaded:      make(map[string]bool),
27		activeNames: activeNames,
28	}
29}
30
31// MarkLoaded marks a skill as having been loaded.
32// Only marks as loaded if the skill is in the active set (not overridden/disabled).
33func (t *Tracker) MarkLoaded(name string) {
34	if t == nil {
35		return
36	}
37	t.mu.Lock()
38	defer t.mu.Unlock()
39	// Only track if this skill is actually active (not overridden by user skill).
40	if t.activeNames[name] {
41		t.loaded[name] = true
42	}
43}
44
45// IsLoaded returns true if the skill has been loaded.
46func (t *Tracker) IsLoaded(name string) bool {
47	if t == nil {
48		return false
49	}
50	t.mu.RLock()
51	defer t.mu.RUnlock()
52	return t.loaded[name]
53}