tracker.go

 1package skills
 2
 3import (
 4	"sort"
 5	"sync"
 6)
 7
 8// Tracker tracks which skills have been loaded (read) during a session.
 9// It is safe for concurrent use.
10//
11// Note: Tracking is name-based and limited to active skills only. If a builtin
12// skill is overridden by a user skill, only the user skill (which is active)
13// can be marked as loaded. This prevents misattribution when reading builtin
14// files that have been overridden.
15type Tracker struct {
16	mu          sync.RWMutex
17	loaded      map[string]bool
18	activeNames map[string]bool // Set of active skill names (post-dedup, post-filter)
19}
20
21// NewTracker creates a new skill tracker with the given active skill names.
22// Only skills in activeSkills can be marked as loaded.
23func NewTracker(activeSkills []*Skill) *Tracker {
24	activeNames := make(map[string]bool, len(activeSkills))
25	for _, s := range activeSkills {
26		activeNames[s.Name] = true
27	}
28	return &Tracker{
29		loaded:      make(map[string]bool),
30		activeNames: activeNames,
31	}
32}
33
34// MarkLoaded marks a skill as having been loaded.
35// Only marks as loaded if the skill is in the active set (not overridden/disabled).
36func (t *Tracker) MarkLoaded(name string) {
37	if t == nil {
38		return
39	}
40	t.mu.Lock()
41	defer t.mu.Unlock()
42	// Only track if this skill is actually active (not overridden by user skill).
43	if t.activeNames[name] {
44		t.loaded[name] = true
45	}
46}
47
48// IsLoaded returns true if the skill has been loaded.
49func (t *Tracker) IsLoaded(name string) bool {
50	if t == nil {
51		return false
52	}
53	t.mu.RLock()
54	defer t.mu.RUnlock()
55	return t.loaded[name]
56}
57
58// LoadedNames returns the names of all skills that have been loaded, sorted
59// alphabetically. Safe to call on a nil Tracker (returns nil).
60func (t *Tracker) LoadedNames() []string {
61	if t == nil {
62		return nil
63	}
64	t.mu.RLock()
65	defer t.mu.RUnlock()
66	if len(t.loaded) == 0 {
67		return nil
68	}
69	names := make([]string, 0, len(t.loaded))
70	for name := range t.loaded {
71		names = append(names, name)
72	}
73	sort.Strings(names)
74	return names
75}
76
77// LoadedCount returns the number of unique skills that have been loaded.
78// Safe to call on a nil Tracker (returns 0).
79func (t *Tracker) LoadedCount() int {
80	if t == nil {
81		return 0
82	}
83	t.mu.RLock()
84	defer t.mu.RUnlock()
85	return len(t.loaded)
86}