Lesson Authoring Guide
How to add, edit, and ship training lessons + quizzes in Kaltiv's LMS without breaking parity, locales, or the KONA tool-unlock pipeline.
This guide is for content authors and developers extending the LMS. End-users do not need to read it.
1. Architecture at a glance
Kaltiv's LMS v2 (FOF-917) uses two storage layers, each addressing a different concern:
| Concern | Storage | File |
|---|---|---|
| Educational content (titles, descriptions, quiz Q&A) | TypeScript source | src/data/learningPaths.ts, src/data/quizzes.ts |
| User-facing strings | i18n JSON ×4 locales | public/locales/{fr,en,pt,sw}/training.json |
| Lesson completion progress (v1) | localStorage | key terraflow-learning-progress |
| Formal HR training catalog (separate concern) | DB (training_programs) | Used by /dashboard/trainings route |
Why TS, not DB? See F-LMS-PHANTOM-DB-TARGET-1. FOF-917 was originally scoped as "DB seed migration" — but the consumer route (
TrainingCenterdialog) reads TS data. Forcing a DB schema would have created a phantom table that nothing queried. Cross-cutting constraint C11 (ai_memory/cross_cutting_constraints.md) now mandates storage-layer verification at /piv NODE-1.
The DB-backed completion log is deferred to EPIC 2 (FOF-920) — see §9.
2. Lesson schema
// src/data/learningPaths.ts
export type LessonType = 'guide' | 'video' | 'quiz' | 'task';
export interface Lesson {
id: string; // prefix-by-path: rh-1, pdg-2, sup-3, emp-4 (see §4)
titleKey: string; // i18n key — e.g. 'training.lessons.rh.missions.title'
descriptionKey: string; // i18n key — e.g. 'training.lessons.rh.missions.description'
type: LessonType;
durationMin: number; // estimated reading/viewing time
resourceUrl?: string; // optional deep-link or external doc
}
export interface LearningPath {
id: 'path-pdg' | 'path-rh' | 'path-superviseur' | 'path-employe';
titleKey: string; // 'onboarding:training.paths.pdg.label'
descriptionKey: string; // 'onboarding:training.paths.pdg.description'
audience: 'pdg' | 'rh' | 'superviseur' | 'employe';
lessons: Lesson[];
}
What changed in v2 (FOF-917)
Pre-v2 lessons hardcoded title: 'Missions et déplacements' (FR-only). v2 replaces every literal with an i18n key. The render layer (LearningPathView.tsx, LearningPathCard.tsx) calls t(lesson.titleKey) instead of reading the literal. No t() call inside the data file — the data file stays pure TS, the rendering does the locale resolution.
3. Quiz schema (multi-quiz per path)
Big 5 SAP SuccessFactors Learning ships multiple short quizzes per role-path (one per topic) instead of one long end-of-path exam. v2 adopts this pattern.
// src/data/quizzes.ts
export interface QuizQuestion {
questionKey: string; // 'training.quizzes.rh.missions.q1.question'
optionKeys: string[]; // ['training.quizzes.rh.missions.q1.opt1', ...] — typically 4
correctIndex: number; // 0-based, must be < optionKeys.length
rationaleKey?: string; // optional 'why this is the right answer' explanation
}
export interface Quiz {
id: string; // 'quiz-rh-missions', 'quiz-pdg-branding', ...
pathId: 'path-pdg' | 'path-rh' | 'path-superviseur' | 'path-employe';
topic: string; // logical topic — UI groups quizzes by topic
titleKey: string;
questions: QuizQuestion[];
passingScore: number; // 50-100, score % required to "pass"
}
export const quizzes: Quiz[] = [/* one row per quiz */];
A path can hold N quizzes. The selector in QuizView.tsx groups them by topic.
4. Naming conventions
Lesson IDs — prefix-by-path
| Path | Lesson ID prefix | Example |
|---|---|---|
path-pdg | pdg- | pdg-1, pdg-2, … |
path-rh | rh- | rh-1, rh-2, … |
path-superviseur | sup- | sup-1, sup-2, … |
path-employe | emp- | emp-1, emp-2, … |
The prefix is load-bearing: useLearningProgress computes path completion by counting localStorage entries whose lesson ID starts with the prefix. Breaking this convention silently breaks the KONA tool-unlock trigger.
The parity ratchet test learningPaths-parity.test.ts enforces both the prefix and uniqueness across paths.
i18n key convention
training.lessons.<audience>.<topic>.{title,description}
training.quizzes.<audience>.<topic>.<qN>.{question,opt1..optN,rationale}
training.quizzes.<audience>.<topic>.title
onboarding:training.paths.<audience>.{label,description}
Path labels live in the onboarding namespace (already loaded when the dialog mounts). Lesson + quiz content lives in the new training namespace registered in src/i18n.ts and lazy-loaded on first dialog open.
5. Locale discipline (4 locales — non-negotiable)
Every key MUST exist in fr, en, pt, sw. The parity ratchet test asserts:
Object.keys(fr.training) — Object.keys(en.training) === ∅
// repeat for pt, sw
CI fails if any locale is missing a key.
PT and SW are NEVER skipped. v1 acceptable: tenant-admin override path is planned for EPIC 2. If translation is uncertain, copy the FR string verbatim + open a Linear follow-up — never leave a key absent. See cross-cutting constraint C1 in
ai_memory/cross_cutting_constraints.md.
6. Deep-linking from tours and Help
The post-tour toast and HelpButton route to:
/help?trainingPath=path-rh&lesson=rh-6
TrainingCenter.tsx reads the query params on mount, opens the dialog, switches to the right path tab, and scrolls the lesson into view. HelpButton.tsx listens at the second mount point (regression-tested).
The bridge map trainingKonaBridge.ts → TOUR_TRAINING_MAP drives the toast: every entry MUST resolve to a real (pathId, lessonId) pair. The parity test asserts this.
7. Big 5 LMS reference patterns
Researched 2026-05-07. Adoption decisions:
| Pattern | Source | v2 status |
|---|---|---|
| Microlearning (5-15 min role-based modules) | LinkedIn Learning, SC Training | Adopted — every path's lessons stay ≤ 15 min |
| Multi-quiz per role-path (one per topic) | SAP SuccessFactors Learning | Adopted (T5 multi-quiz refactor) |
| AI content recommendations | Docebo | Deferred to EPIC 3 (FOF-921) — KONA suggest_lesson based on page context |
| Native HCM analytics integration | Workday Learning | Deferred to EPIC 2 (FOF-920) — DB-backed completion + analytics dashboards |
| Compliance audit trail / attestation | SAP SuccessFactors | Deferred to EPIC 2 (FOF-920) — completion log in lesson_completions table |
Sources:
- Docebo — Corporate LMS comparison
- Software Reviews — LMS alternatives
- Litmos — top SaaS onboarding trends
8. Adding a new lesson — workflow
- Pick the path (
path-pdg,path-rh,path-superviseur,path-employe). - Pick the next lesson ID — increment per the path's prefix (e.g.
rh-7ifrh-1..rh-6exist). - Add the data row in
src/data/learningPaths.ts:{id: 'rh-7',titleKey: 'training.lessons.rh.<topic>.title',descriptionKey: 'training.lessons.rh.<topic>.description',type: 'guide',durationMin: 8,}, - Add 4 i18n entries — one per locale — in
public/locales/{fr,en,pt,sw}/training.json. Use the same key path; translate the value. PT/SW v1 may copy FR if uncertain. - Run the ratchet —
bash scripts/test-changed.sh. The locale parity ratchet, prefix convention test, andMIN_LESSONSderivation test all run. Fix failures before commit. - Update the bridge if a tour should land on this lesson — add a
TOUR_TRAINING_MAPentry intrainingKonaBridge.tspointing to(pathId, lessonId). - Live-verify — load the app, complete the relevant tour, click the toast, confirm the lesson opens in all 4 locales (per
live-verify-before-done.md§6 mandatory checklist). - Commit — conventional commit
feat(lms): FOF-XXX — add <topic> lessonreferencing the parent EPIC.
9. Adding a new quiz — workflow
Same shape as lessons, with quiz-specific guards:
correctIndex < optionKeys.length— ratchet asserts.passingScore∈ [50, 100].- A path may have multiple quizzes; the UI selector groups them by
topic. - Quiz IDs follow
quiz-<audience>-<topic>(e.g.quiz-rh-missions).
KONA tool-unlock for a path requires all lessons completed (count ≥ MIN_LESSONS[pathId], computed dynamically from the data file via useLearningProgress.ts) and at least one quiz passed at the path's passing-score threshold. Adding a quiz alone does not unlock tools.
10. Future evolution
| Pattern | EPIC | Trigger |
|---|---|---|
DB-backed completion (lesson_completions table + RLS + audit trail) | FOF-920 | Cross-device progress, compliance attestation |
| Tenant-admin translation override (replace LLM PT/SW v1 with curated copy) | FOF-920 | Tenant onboarding feedback |
KONA suggest_lesson(currentRoute) tool — AI recommendations | FOF-921 | Docebo-style contextual nudges |
| "?" contextual help button per page (replaces some tours) | FOF-921 | Big 5 SaaS onboarding pattern (40% faster TTV) |
| Cross-module lesson parity (28 modules with tour but no lesson) | FOF-920 | Help-ecosystem H4 closure across modules |
Each EPIC has its own plan in .agents/plans/.
11. Test ratchets that catch drift
Three parity ratchets run on every bash scripts/test-changed.sh:
| Ratchet | What it asserts |
|---|---|
learningPaths-parity.test.ts | TOUR_TRAINING_MAP → real lesson, prefix convention, i18n parity ×4 locales |
quizzes-parity.test.ts | Multi-quiz architecture, correctIndex bounds, passingScore range, locale parity |
TrainingCenter-deeplink.test.ts | Query-param routing, fallback on invalid path, post-tour toast click |
Drift can only decrease over time (cross-cutting constraint C7). Authors who break a ratchet fix the data, never the test.
12. References
- Plan:
.agents/plans/fof-917-lms-architecture-v2.md - Cross-cutting constraints:
ai_memory/cross_cutting_constraints.md(C1 i18n, C7 tests, C11 storage layer) - Storage-layer failure pattern:
ai_memory/common_failures.md§ F-LMS-PHANTOM-DB-TARGET-1 - Live-verify checklist:
.claude/rules/live-verify-before-done.md - Linear EPICs: FOF-917 (this), FOF-920 (parity rollout), FOF-921 (contextual help)