# Generation spec: learning content & micro-learnings

Two generators turn a topic into learner-facing material. Both go through
`callLLM` with forced tool use and Zod-validated output. All content is cached in
PocketBase so it is generated once per topic/type.

---

## A. Long-form content — `src/lib/learningService.js`

Stored in the `content` collection (one record per topic, `data` is a merged
object). Three types, generated **on demand**:

| Type | Tool | Min requirements |
|---|---|---|
| `article` | `emit_learning_article` | ≥3 sections, ≥2 takeaways |
| `slides` | `emit_learning_slides` | ≥4 slides |
| `infographic` | `emit_learning_infographic` | ≥3 stats, ≥3 steps |

`generateLearningContent(topic, force, selectedType)`:
- tier `standard`, `maxTokens: 8192`
- `selectedType` is one of the three, or `'all'` (`emit_learning_all`) for admin regeneration
- cache check looks at `content[selectedType]`; on generation the new payload is
  **shallow-merged** into the cached object so other types survive
- there is **no podcast type**

**Article refinement** (`refineLearningContent`): the admin describes a change and
the model edits via targeted patch tools — `set_intro`, `set_section`,
`add_section`, `remove_section`, `replace_takeaways` — so only the affected parts
change. Patches are applied and re-validated in `src/lib/articlePatches.js`.

---

## B. Micro-learnings — `src/lib/microLearningService.js`

Stored in the `micro_learnings` collection (one record per topic per type,
`status='published'`). Three types:

| Type | Tool | Tier | Shape |
|---|---|---|---|
| `concept_explainer` | `emit_concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }`, ≥3 sections |
| `scenario_quiz` | `emit_scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }`, 3–4 options, exactly 1 correct |
| `flashcard_set` | `emit_flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }`, 5–10 cards |

`getOrGenerateMicroLearning(topicId, type)`:
- returns the cached published record if one exists (`findExisting`)
- otherwise loads the topic, calls `callLLM` with forced tool choice, and creates a
  `micro_learnings` record with the validated `content`

> A former `reflection_prompt` type was dropped. Do not re-add it.

Completion is recorded (append-only) by `useMicroLearningCompletions` into
`micro_learning_completions` with `{ team_member_id, micro_learning_id, topic_id,
type, session_week }`.

---

## C. Weekly quiz — `src/lib/testService.js`

Generates a 5-question multiple-choice test for the user's current week.

- **Topic selection** (`selectTestTopics`): primary topic from the active
  curriculum week (else hash fallback) + a few review topics for breadth.
- **Batch generation** (`callQuizBatchModel`): a single `fast`-tier call
  (`emit_quiz_questions`, `maxTokens: 4096`, 25s timeout) returns all 5 questions.
- **Quality gates** (`validateBatchQuality`): no duplicate options; no banned
  fillers ("all/none of the above", "both A and B"); explanations ≥20 chars; reject
  if `correctIndex` is dominated by one position (>80%) and re-roll.
- **Scoring** (`saveTestResult`): `pointsEarned = score * 2`, written to
  `leaderboard` via `db.upsertLeaderboardEntry`.

Question shape: `{ id, question, topicLabel, options[4], correctIndex (0–3),
explanation, difficulty }`.

---

## D. Onboarding overviews — `src/lib/onboardingService.js`

Powers the 5-day onboarding track (issue #30): a short, breadth-first overview of
**one theme** for a brand-new employee — deliberately light, not a deep lesson.

- **Tool:** `emit_onboarding_overview` · **tier:** `fast` (Haiku) · `maxTokens: 1500`,
  60s timeout · schema `onboardingOverviewSchema`.
- **Shape:** `{ title, what_it_is, why_it_matters, key_points[3–5], topics_covered[{topic_id,label}] }`.
  `why_it_matters` is framed around day-to-day / week-to-week work at Respellion.
- **Cache:** `onboarding_overviews`, one row per theme, keyed by theme name plus a
  `topics_fingerprint` (stable hash of the theme's sorted topic ids).
  `getOrGenerateOnboardingOverview(theme, topics, {force})` returns the cached row when
  the fingerprint matches; a mismatch (topic added/removed) or `force` regenerates.
- **Simulation:** `emit_onboarding_overview` has a stub in `SIMULATION_TOOL_STUBS`.

Theme ordering + day grouping are pure helpers in the same module
(`orderThemes`, `distributeThemesIntoDays`, `computeTopicsFingerprint`,
`computeOnboardingProgress`, `buildOnboardingPlan`), unit-tested in
`src/lib/__tests__/onboardingService.test.js`. Completion is recorded per **theme**
in `onboarding_completions` (`{ team_member_id, theme }`), not per day.

---

## Shared infrastructure (`src/lib/llm.js`)

- **Tiers:** `fast` (Haiku 4.5), `standard` (Sonnet 4.6), `reasoning` (Opus 4.7);
  per-tier admin overrides via `admin:model:{tier}`.
- **Structured output:** prefer tool use with forced `toolChoice`; inputs validated
  by `toolSchemaRegistry`. Text responses go through `parseStructuredText`.
- **Caching:** wrap stable system text with `cachedSystem(...)`.
- **Retry/limits:** `src/lib/llmRetry.js` — backoff + jitter on 408/425/429/5xx/529,
  honors `Retry-After`, rate limiters for bulk work.
- **Telemetry:** every call logged to `llm_calls`.
- **Simulation:** with `admin:use_simulation`, calls return stub output (no API hit).
