package learnermemory import ( "errors" "fmt" "strings" "sync/atomic" "time" "tutor/internal/workflows" ) type Service struct { store Store ids atomic.Uint64 } func NewService(store Store) *Service { return &Service{store: store} } func (s *Service) EnsureProfile(input ProfileInput) (Profile, error) { if strings.TrimSpace(input.UserID) == "" { return Profile{}, errors.New("user_id is required") } if strings.TrimSpace(input.TargetRole) == "" { return Profile{}, errors.New("target_role is required") } profile := Profile{ UserID: input.UserID, TargetRole: input.TargetRole, Stack: append([]string(nil), input.Stack...), InterviewTimeline: input.InterviewTimeline, Preferences: append([]string(nil), input.Preferences...), UpdatedAt: time.Now().UTC(), } return s.store.UpsertProfile(profile) } func (s *Service) ApplyCandidate(candidate workflows.MemoryUpdateCandidate) error { if strings.TrimSpace(candidate.UserID) == "" { return errors.New("candidate user_id is required") } for _, update := range candidate.Updates { if len(update.Evidence) == 0 { continue } if strings.TrimSpace(update.Concept.ID) == "" { continue } if err := s.applyUpdate(candidate.UserID, update); err != nil { return err } } return nil } func (s *Service) Snapshot(userID string) (Snapshot, error) { if strings.TrimSpace(userID) == "" { return Snapshot{}, errors.New("user_id is required") } return s.store.Snapshot(userID) } func (s *Service) applyUpdate(userID string, update workflows.MemoryUpdate) error { now := time.Now().UTC() switch update.Kind { case workflows.MemoryConceptMastery: return s.store.UpsertMastery(ConceptMastery{ UserID: userID, Concept: update.Concept, State: update.ProposedState, Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...), UpdatedAt: now, }) case workflows.MemoryMisconception: return s.store.AddMisconception(Misconception{ ID: s.nextID("misconception"), UserID: userID, Concept: update.Concept, Label: update.Summary, Description: update.Summary, Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...), UpdatedAt: now, }) case workflows.MemoryIntervention: return s.store.AddIntervention(Intervention{ ID: s.nextID("intervention"), UserID: userID, Concept: update.Concept, Summary: update.Summary, Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...), UpdatedAt: now, }) case workflows.MemoryReviewSchedule: return s.store.AddReviewSchedule(ReviewSchedule{ ID: s.nextID("review"), UserID: userID, Concept: update.Concept, Reason: update.Summary, Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...), UpdatedAt: now, }) default: return nil } } func (s *Service) nextID(prefix string) string { return fmt.Sprintf("%s-%d", prefix, s.ids.Add(1)) }