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}