# Curriculum spec: 26-week per-user cycle The curriculum sequences the knowledge base into a 26-week schedule. Every employee runs their **own** cycle, starting when they enroll. Implemented in `src/lib/curriculumService.js`; admin UI in `src/components/admin/CurriculumManager.jsx`. --- ## Data - **`curriculum_versions`** — generated schedules. Lifecycle `draft → active → superseded`; exactly one `active`. The `schedule` JSON is an array of 26 week objects: `{ week_number (1–26), theme, topic_ids[], estimated_duration (15–45), week_rationale }`. `coverage_stats` records theme/topic coverage. - **`topics`** — supply `theme`, `complexity_weight` (1–5), and `difficulty` as generation input. --- ## Topic enrichment (prerequisite) Generation needs themed, weighted topics. `enrichTopicsForCurriculum()` finds topics missing a `theme` (excluding `type='fact'` and `learning_relevance='exclude'`) and, in batches of 20, calls Claude with `emit_topic_enrichment` to assign `theme`, `complexity_weight`, and `difficulty`. Triggered from the Curriculum tab. --- ## Generation `generateCurriculumDraft(reason)`: 1. Group learning topics by `theme` (`buildThemeTopicMap`), sorted by `complexity_weight` ascending. 2. Build a prompt describing themes and their topic ids. If there are more than 26 themes, the model is instructed to **merge** closely related themes. 3. `callLLM` (standard tier, `maxTokens: 8192`, temp 0) with forced `emit_curriculum_schedule`. Up to 2 attempts; on validation failure the errors are fed back into the retry prompt. 4. Validate (`validateSchedule`): exactly 26 weeks, correct `week_number` sequence, durations 15–45, every `topic_id` exists, ≥1 topic per week. Unscheduled themes are warnings, not hard errors. 5. Supersede any existing draft and store the new `draft` version with coverage stats. `confirmVersion(versionId, adminUserId)` activates a draft (and supersedes the old active version); `rejectVersion` discards a draft. --- ## Per-user scheduling (the cycle) The cycle is **detached from the calendar**. Enrollment sets `team_members.curriculum_started_at` (see `docs/frontend-spec.md` for onboarding). `AppContext` derives the user's position: ```js getPersonalWeekNumber(startedAt) // floor(daysSinceStart / 7) + 1, ≥1 (0 if not enrolled) getCurriculumWeek(personalWeek) // ((n - 1) % 26) + 1 → 1..26 slot getCurriculumCycle(personalWeek) // floor((n - 1) / 26) + 1 → 1, 2, 3, ... ``` - Week 1 begins the day the employee enrolls. - After week 26 the cycle restarts at week 1 with the **same** content. - `state.weekNumber` (the absolute counter) is `0` until the user enrolls; pages are gated behind onboarding so they never render with week 0. --- ## Content & progress for a week - `getCurrentWeekContent(personalWeek)` reads the active version's schedule, maps the 26-week slot to its `topic_ids`, and returns `{ cycle, weekNumber, theme, topics, estimatedDuration, rationale }`. - `getAssignedTopic(userId, week)` returns the week's primary topic, falling back to a deterministic hash of `userId:week` when no curriculum is active. **Keep the fallback.** - `getYearProgress(userId, personalWeek)` computes completion for the current cycle. --- ## Notes - There is no shared "current week" and no `admin:current_week` setting. - Regeneration produces a new version; activating it changes future weeks for all users. Completion history (`micro_learning_completions`) is append-only and never rewritten.