# Curriculum service spec

## Responsibility

Generates a versioned 26-week learning schedule from the published knowledge
base. Manages perpetual cycling, version transitions, and employee curriculum
state. Handles regeneration when the KB changes.

---

## Service location

```
app/services/curriculum/
  ├── src/
  │     ├── index.ts                 entry point, Fastify server
  │     ├── routes/
  │     │     ├── curriculum.ts      POST /generate, GET /current, GET /preview
  │     │     └── employee.ts        GET /state/:userId, POST /advance/:userId
  │     ├── generator/
  │     │     ├── build.ts           KB graph → 26-week schedule (AI call)
  │     │     ├── sequence.ts        prerequisite + complexity ordering
  │     │     └── cycle.ts           cycle 2+ variation logic
  │     ├── versioning/
  │     │     ├── apply.ts           apply new version to active employees
  │     │     └── freeze.ts          protect completed weeks
  │     └── lib/
  │           ├── pocketbase.ts
  │           └── anthropic.ts
  ├── package.json
  ├── tsconfig.json
  └── .env.example
```

---

## API surface

### POST /generate

Triggers curriculum generation from current published KB.
Called by admin app after confirming regeneration.

Request:
```json
{
  "triggeredBy": "string",
  "reason": "new_topics" | "manual"
}
```

Response (202 Accepted):
```json
{
  "jobId": "string",
  "status": "queued"
}
```

---

### GET /preview

Returns proposed new curriculum before admin confirms.
Called by admin app to show preview before regeneration is applied.

Response:
```json
{
  "version": 3,
  "weeks": [
    {
      "weekNumber": 1,
      "theme": { "id": "string", "title": "string" },
      "topics": [
        { "id": "string", "title": "string", "complexityWeight": 2 }
      ],
      "estimatedDurationMinutes": 25
    }
  ],
  "coverageStats": {
    "themesTotal": 8,
    "themesCovered": 8,
    "topicsTotal": 42,
    "topicsCovered": 42
  }
}
```

---

### GET /current

Returns the currently active curriculum version with all week slots.

---

### GET /state/:userId

Returns an employee's current curriculum state.

Response:
```json
{
  "userId": "string",
  "currentCycle": 1,
  "currentWeek": 7,
  "startDate": "2026-01-15T00:00:00Z",
  "activeVersionId": "string",
  "nextSessionTheme": { "id": "string", "title": "string" },
  "nextSessionTopics": []
}
```

---

### POST /advance/:userId

Called by progress service when an employee completes a week.
Increments currentWeek, handles cycle transition at week 26.

Request:
```json
{
  "completedWeek": 7
}
```

---

## Curriculum generation

### Input

All published Themes and Topics retrieved from PocketBase:

```typescript
type KBSnapshot = {
  themes: {
    id: string
    title: string
    description: string
    topics: {
      id: string
      title: string
      complexityWeight: number       // 1–5
      difficulty: string
      prerequisiteTopics: string[]   // topic IDs
      relatedTopics: string[]
      contrastTopics: string[]
    }[]
  }[]
}
```

---

### Pre-processing: sequence topics within themes

Before the AI call, the service resolves topic ordering within each Theme
using a topological sort on prerequisite relationships.

```
For each Theme:
  Build directed graph: prerequisite_topics edges
  Topological sort → ordered topic list
  If cycle detected (should not occur but handle): log warning, fall back to
  complexity_weight ascending order
```

This pre-processing means the AI does not need to reason about prerequisites —
it receives already-ordered topic lists and focuses on Theme sequencing.

---

### AI call: Theme sequencing across 26 weeks

System prompt:
```
You are a curriculum designer. Your task is to distribute a set of learning
Themes across 26 weekly sessions to create an effective learning journey.

Output ONLY valid JSON matching the schema provided. No preamble, no
explanation, no markdown fences.

Rules:
- Every Theme must appear at least once across 26 weeks
- Themes with more Topics (higher topic count) may span multiple weeks or
  appear in multiple cycles within the 26 weeks
- Sequence Themes so foundational concepts precede dependent ones
- Distribute complexity progressively: introductory Themes early, advanced
  Themes in the second half
- If total Topics across all Themes exceeds what 26 weeks can cover in depth,
  prioritise breadth in cycle 1 — every Theme covered, key Topics per Theme
- Assign an estimated duration in minutes per week (15–45 minutes per session)
- Return exactly 26 week slots
```

User prompt:
```
Knowledge base snapshot:
{KBSnapshot as JSON}

Generate a 26-week curriculum schedule.
```

Output schema:
```typescript
type CurriculumDraft = {
  weeks: {
    weekNumber: number            // 1–26
    themeId: string
    topicIds: string[]            // ordered subset of theme's topics
    estimatedDurationMinutes: number
    rationale: string             // one sentence — shown to admin in preview
  }[]
}
```

AI call configuration:
```typescript
{
  model: 'claude-sonnet-4-20250514',
  max_tokens: 4000,
  temperature: 0
}
```

Validation: Zod schema on output. Check all themeIds and topicIds exist in
the KB snapshot before writing. Reject and retry once on validation failure.

---

### Write to PocketBase

```
Create curriculum_versions record {
  version: latest + 1,
  status: 'draft',
  generated_at: now,
  generation_notes: reason
}

For each week in CurriculumDraft:
  Create curriculum_weeks record {
    curriculum_version: versionId,
    week_number: weekNumber,
    theme: themeId,
    topics: topicIds,
    topic_order: [0, 1, 2, ...],
    estimated_duration_minutes: value,
    admin_notes: ''
  }

Set curriculum_versions.status → 'draft'
Notify admin: preview available at GET /preview
```

Draft version is not applied until admin confirms via POST /generate confirm.

---

## Versioning and regeneration

### Applying a new version

When admin confirms, `apply.ts` runs:

```
Get all employees from employee_curriculum_state

For each employee:
  frozenWeek = employee.current_week
  
  Update employee_curriculum_state:
    active_version = new version ID
  
  Note: completed weeks are protected by current_week value
  The frontend only renders weeks >= current_week from active_version
  Weeks < current_week are rendered from session_completions history
  (immutable records — not from curriculum_weeks)

Set old curriculum_versions.status → 'superseded'
Set new curriculum_versions.status → 'active'
```

Completed weeks are never stored against a curriculum version — they live
in session_completions. The version only determines future week content.

---

## Perpetual cycling

### Week 26 completion → cycle transition

When progress service calls POST /advance/:userId with completedWeek: 26:

```
employee.currentCycle += 1
employee.currentWeek = 1
employee.startDate = now
employee.activeVersion = current active version

Generate cycle variant (see below)
```

### Cycle variant generation

Cycle 2+ is not identical to cycle 1. The AI call receives additional context:

Additional fields in user prompt for cycle 2+:
```json
{
  "cycleNumber": 2,
  "employeeHistory": {
    "typesUsed": ["concept_explainer", "scenario_quiz", "how_to"],
    "typesNotUsed": ["case_study", "myth_vs_evidence", "comparison_card"],
    "lowEngagementTopics": ["topic-id-1", "topic-id-2"]
  }
}
```

Additional rules added to system prompt for cycle 2+:
```
- Vary the Theme sequence from the previous cycle
- Topics identified as low engagement should appear earlier in this cycle
- The rationale field should note what is different from cycle 1
```

Low engagement is determined by: topics where the employee completed only
one micro learning type (minimum engagement). Retrieved from session_completions
by progress service and passed to curriculum service on cycle transition.

---

## Admin curriculum editor

The curriculum editor in the admin app (built in frontend phase) calls:
- GET /preview to display the proposed schedule
- PATCH /weeks/:weekId to update theme or topic assignment
- POST /confirm to apply the version

The PATCH route allows admin to:
- Reassign a Theme to a different week (swap two weeks)
- Add or remove Topics from a week's topic list
- Edit admin_notes per week

Changes made via PATCH update the draft curriculum_weeks records before
the version is confirmed and applied.

---

## Environment variables

```
ANTHROPIC_API_KEY=
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
CURRICULUM_PORT=3003
```

---

## Dependencies

```json
{
  "dependencies": {
    "fastify": "^4",
    "@anthropic-ai/sdk": "^0.24",
    "pocketbase": "^0.21",
    "zod": "^3",
    "uuid": "^9"
  }
}
```

---

## TypeScript strict mode requirements

- No `any` types
- KBSnapshot typed explicitly — validated against PocketBase response
- CurriculumDraft validated through Zod before any PocketBase writes
- Topological sort implemented with explicit typed graph structure

---

## What this service does NOT do

- Does not generate micro learnings → generation service
- Does not record completions → progress service
- Does not serve KB content → frontend reads PocketBase directly
- Does not handle auth → PocketBase + frontend

---

## Testing checkpoints

1. Generate curriculum from a KB with 5+ themes → confirm 26 weeks produced
2. Confirm all themes appear at least once
3. Confirm topic order within a week respects prerequisites
4. Add a new theme to KB → trigger regeneration → confirm employee at week 5
   sees weeks 1–5 unchanged, weeks 6–26 updated
5. Advance employee through week 26 → confirm cycle 2 starts with varied sequence
6. Admin edits week 3 theme → confirm patch updates draft before confirmation
