package progression import ( "errors" "sort" "strings" "tutor/internal/learnermemory" "tutor/internal/workflows" ) type Service struct { memory *learnermemory.Service } func NewService(memory *learnermemory.Service) *Service { return &Service{memory: memory} } func (s *Service) ReadinessMap(userID string) (ReadinessMap, error) { snapshot, err := s.snapshot(userID) if err != nil { return ReadinessMap{}, err } concepts := make([]ConceptProgress, 0, len(snapshot.Mastery)) for _, mastery := range snapshot.Mastery { concepts = append(concepts, ConceptProgress{ Concept: mastery.Concept, State: mastery.State, LadderLevel: ladderForState(mastery.State), NextAction: actionForState(mastery.State), Evidence: append([]workflows.EvidenceRef(nil), mastery.Evidence...), }) } sort.Slice(concepts, func(i, j int) bool { return concepts[i].Concept.ID < concepts[j].Concept.ID }) readiness := ReadinessMap{ UserID: snapshot.Profile.UserID, Track: trackFromConcepts(concepts), ReadinessPercentage: readinessPercentage(concepts), Concepts: concepts, Rewards: rewardsFromConcepts(concepts), Unlocks: unlocksFromConcepts(concepts), } return readiness, nil } func (s *Service) NextChallenge(userID string) (workflows.NextChallenge, error) { readiness, err := s.ReadinessMap(userID) if err != nil { return workflows.NextChallenge{}, err } if len(readiness.Concepts) == 0 { return workflows.NextChallenge{}, errors.New("no learner memory concepts available") } target := readiness.Concepts[0] for _, concept := range readiness.Concepts[1:] { if readinessScore(concept.State) < readinessScore(target.State) { target = concept } } return workflows.NextChallenge{ UserID: readiness.UserID, Track: readiness.Track, Concept: target.Concept, LadderLevel: target.LadderLevel, Question: challengeQuestion(target), Rationale: "Selected from the weakest evidenced learner-memory concept.", DifficultyAction: workflowDifficulty(target.NextAction), Evidence: append([]workflows.EvidenceRef(nil), target.Evidence...), }, nil } func (s *Service) snapshot(userID string) (learnermemory.Snapshot, error) { if s.memory == nil { return learnermemory.Snapshot{}, errors.New("learner memory not configured") } if strings.TrimSpace(userID) == "" { return learnermemory.Snapshot{}, errors.New("user_id is required") } return s.memory.Snapshot(userID) } func ladderForState(state workflows.ReadinessState) workflows.LadderLevel { switch state { case workflows.ReadinessFragile: return workflows.LadderDefine case workflows.ReadinessImproving: return workflows.LadderTradeoffs case workflows.ReadinessInterviewReady: return workflows.LadderDesignConstraints case workflows.ReadinessStrongSignal: return workflows.LadderInterviewPressure default: return workflows.LadderDefine } } func actionForState(state workflows.ReadinessState) DifficultyAction { switch state { case workflows.ReadinessFragile: return ActionRecover case workflows.ReadinessImproving: return ActionHold case workflows.ReadinessInterviewReady, workflows.ReadinessStrongSignal: return ActionRaise default: return ActionLower } } func workflowDifficulty(action DifficultyAction) workflows.DifficultyAction { switch action { case ActionRecover: return workflows.DifficultyRecover case ActionLower: return workflows.DifficultyLower case ActionRaise: return workflows.DifficultyRaise default: return workflows.DifficultyHold } } func readinessPercentage(concepts []ConceptProgress) int { if len(concepts) == 0 { return 0 } total := 0 for _, concept := range concepts { total += readinessScore(concept.State) } return total * 100 / (len(concepts) * 4) } func readinessScore(state workflows.ReadinessState) int { switch state { case workflows.ReadinessFragile: return 1 case workflows.ReadinessImproving: return 2 case workflows.ReadinessInterviewReady: return 3 case workflows.ReadinessStrongSignal: return 4 default: return 0 } } func rewardsFromConcepts(concepts []ConceptProgress) []Reward { rewards := []Reward{} for _, concept := range concepts { if len(concept.Evidence) == 0 || readinessScore(concept.State) < 2 { continue } rewards = append(rewards, Reward{ Kind: RewardConceptProgress, Label: "Evidence-backed progress on " + concept.Concept.Label, Evidence: append([]workflows.EvidenceRef(nil), concept.Evidence...), }) } return rewards } func unlocksFromConcepts(concepts []ConceptProgress) []Unlock { stable := []workflows.EvidenceRef{} for _, concept := range concepts { if readinessScore(concept.State) < 3 || len(concept.Evidence) == 0 { continue } stable = append(stable, concept.Evidence...) } if len(stable) < 2 { return []Unlock{} } return []Unlock{{ Kind: UnlockBossQuestion, Label: "Integrated backend interview boss question", Evidence: append([]workflows.EvidenceRef(nil), stable...), }} } func trackFromConcepts(concepts []ConceptProgress) string { if len(concepts) == 0 { return "" } return concepts[0].Concept.Track } func challengeQuestion(concept ConceptProgress) string { switch concept.LadderLevel { case workflows.LadderTradeoffs: return "Explain a production tradeoff for " + concept.Concept.Label + "." case workflows.LadderDesignConstraints: return "Design a constrained backend scenario that uses " + concept.Concept.Label + "." case workflows.LadderInterviewPressure: return "Answer a timed interview follow-up about " + concept.Concept.Label + "." default: return "Define " + concept.Concept.Label + " and give one concrete backend example." } }