package learnermemory import ( "errors" "sync" "tutor/internal/workflows" ) var ErrProfileNotFound = errors.New("learner profile not found") type Store interface { UpsertProfile(Profile) (Profile, error) GetProfile(string) (Profile, error) UpsertMastery(ConceptMastery) error AddMisconception(Misconception) error AddIntervention(Intervention) error AddReviewSchedule(ReviewSchedule) error Snapshot(string) (Snapshot, error) } type MemoryStore struct { mu sync.RWMutex profiles map[string]Profile mastery map[string]map[string]ConceptMastery misconceptions map[string][]Misconception interventions map[string][]Intervention reviewSchedules map[string][]ReviewSchedule } func NewMemoryStore() *MemoryStore { return &MemoryStore{ profiles: make(map[string]Profile), mastery: make(map[string]map[string]ConceptMastery), misconceptions: make(map[string][]Misconception), interventions: make(map[string][]Intervention), reviewSchedules: make(map[string][]ReviewSchedule), } } func (s *MemoryStore) UpsertProfile(profile Profile) (Profile, error) { s.mu.Lock() defer s.mu.Unlock() s.profiles[profile.UserID] = cloneProfile(profile) return profile, nil } func (s *MemoryStore) GetProfile(userID string) (Profile, error) { s.mu.RLock() defer s.mu.RUnlock() profile, ok := s.profiles[userID] if !ok { return Profile{}, ErrProfileNotFound } return cloneProfile(profile), nil } func (s *MemoryStore) UpsertMastery(mastery ConceptMastery) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.mastery[mastery.UserID]; !ok { s.mastery[mastery.UserID] = make(map[string]ConceptMastery) } s.mastery[mastery.UserID][mastery.Concept.ID] = cloneMastery(mastery) return nil } func (s *MemoryStore) AddMisconception(misconception Misconception) error { s.mu.Lock() defer s.mu.Unlock() s.misconceptions[misconception.UserID] = append( s.misconceptions[misconception.UserID], cloneMisconception(misconception), ) return nil } func (s *MemoryStore) AddIntervention(intervention Intervention) error { s.mu.Lock() defer s.mu.Unlock() s.interventions[intervention.UserID] = append( s.interventions[intervention.UserID], cloneIntervention(intervention), ) return nil } func (s *MemoryStore) AddReviewSchedule(schedule ReviewSchedule) error { s.mu.Lock() defer s.mu.Unlock() s.reviewSchedules[schedule.UserID] = append( s.reviewSchedules[schedule.UserID], cloneReviewSchedule(schedule), ) return nil } func (s *MemoryStore) Snapshot(userID string) (Snapshot, error) { s.mu.RLock() defer s.mu.RUnlock() profile, ok := s.profiles[userID] if !ok { return Snapshot{}, ErrProfileNotFound } snapshot := Snapshot{ Profile: cloneProfile(profile), Mastery: make([]ConceptMastery, 0, len(s.mastery[userID])), Misconceptions: cloneMisconceptions(s.misconceptions[userID]), Interventions: cloneInterventions(s.interventions[userID]), ReviewSchedule: cloneReviewSchedules(s.reviewSchedules[userID]), } for _, mastery := range s.mastery[userID] { snapshot.Mastery = append(snapshot.Mastery, cloneMastery(mastery)) } return snapshot, nil } func cloneProfile(profile Profile) Profile { profile.Stack = append([]string(nil), profile.Stack...) profile.Preferences = append([]string(nil), profile.Preferences...) return profile } func cloneMastery(mastery ConceptMastery) ConceptMastery { mastery.Evidence = append([]workflows.EvidenceRef(nil), mastery.Evidence...) return mastery } func cloneMisconception(misconception Misconception) Misconception { misconception.Evidence = append([]workflows.EvidenceRef(nil), misconception.Evidence...) return misconception } func cloneIntervention(intervention Intervention) Intervention { intervention.Evidence = append([]workflows.EvidenceRef(nil), intervention.Evidence...) return intervention } func cloneReviewSchedule(schedule ReviewSchedule) ReviewSchedule { schedule.Evidence = append([]workflows.EvidenceRef(nil), schedule.Evidence...) return schedule } func cloneMisconceptions(items []Misconception) []Misconception { cloned := make([]Misconception, len(items)) for i, item := range items { cloned[i] = cloneMisconception(item) } return cloned } func cloneInterventions(items []Intervention) []Intervention { cloned := make([]Intervention, len(items)) for i, item := range items { cloned[i] = cloneIntervention(item) } return cloned } func cloneReviewSchedules(items []ReviewSchedule) []ReviewSchedule { cloned := make([]ReviewSchedule, len(items)) for i, item := range items { cloned[i] = cloneReviewSchedule(item) } return cloned }