Add specifications for gamification, generation, and R42 chat services

- Introduced gamification service spec detailing responsibilities, API surface, XP calculation, levels, streaks, badges, milestone cards, and heatmap data.
- Added generation service spec outlining the process for generating micro learning content, including API endpoints, AI call configuration, prompt strategies, and error handling.
- Created R42 chat service spec covering chatbot interactions, retrieval pipeline, prompt construction, response generation, and stateless design principles.
This commit is contained in:
RaymondVerhoef
2026-05-23 18:13:08 +02:00
parent dda20612e9
commit 472685f0d7
62 changed files with 11552 additions and 21 deletions

469
docs/gamification-spec.md Normal file
View File

@@ -0,0 +1,469 @@
# Gamification and progress service spec
## Responsibility
Records session completions, calculates XP (commits), manages levels and
streaks, evaluates badge conditions, generates milestone cards, and serves
leaderboard data. All gamification data is public to all employees.
---
## Service location
```
app/services/progress/
├── src/
│ ├── index.ts entry point, Fastify server
│ ├── routes/
│ │ ├── completions.ts POST /complete
│ │ ├── profile.ts GET /profile/:userId
│ │ ├── leaderboard.ts GET /leaderboard
│ │ └── feed.ts GET /feed
│ ├── engine/
│ │ ├── xp.ts commit calculation per type
│ │ ├── level.ts level thresholds + promotion
│ │ ├── streak.ts streak evaluation
│ │ ├── badges.ts badge condition evaluation
│ │ └── milestone.ts milestone card generation
│ └── lib/
│ └── pocketbase.ts
├── package.json
├── tsconfig.json
└── .env.example
```
---
## API surface
### POST /complete
Called by the frontend when an employee completes a micro learning.
Request:
```json
{
"userId": "string",
"topicId": "string",
"microLearningId": "string",
"microLearningType": "string",
"weekNumber": 7,
"cycle": 1
}
```
Response:
```json
{
"commitsEarned": 15,
"totalCommits": 342,
"levelBefore": "junior",
"levelAfter": "junior",
"streakWeeks": 5,
"newBadges": [
{ "key": "deep_reader", "label": "Deep reader", "tier": "content" }
],
"milestoneCard": null
}
```
`newBadges` is empty array if no badges earned this completion.
`milestoneCard` is populated only at weeks 13 and 26.
---
### GET /profile/:userId
Returns full gamification profile for an employee.
Response:
```json
{
"userId": "string",
"displayName": "string",
"totalCommits": 342,
"level": "junior",
"currentStreakWeeks": 5,
"longestStreakWeeks": 8,
"typesUsed": ["concept_explainer", "scenario_quiz", "how_to"],
"typesNotUsed": ["case_study", "myth_vs_evidence"],
"badges": [
{ "key": "bronze_1", "label": "First commit", "tier": "bronze", "earnedAt": "..." }
],
"heatmap": [
{ "week": 1, "completions": 3 },
{ "week": 2, "completions": 0 }
]
}
```
Heatmap returns 26 entries per cycle. `completions` = number of micro
learning types completed that week.
---
### GET /leaderboard
Returns all employee gamification profiles for leaderboard rendering.
Not paginated at 150 employees.
Response:
```json
{
"employees": [
{
"userId": "string",
"displayName": "string",
"totalCommits": 847,
"currentStreakWeeks": 18,
"typesUsedCount": 9,
"badgeCount": 5,
"level": "senior"
}
]
}
```
Sorted by totalCommits descending. Frontend renders as multi-dimension
table — not a ranked list.
---
### GET /feed
Returns recent milestone cards for the public activity feed.
Most recent first.
Response:
```json
{
"milestones": [
{
"userId": "string",
"displayName": "string",
"cycle": 1,
"week": 26,
"totalCommits": 847,
"streakWeeks": 18,
"badges": ["on_streak", "shipped"],
"createdAt": "string"
}
]
}
```
---
## XP (commits) calculation
Each micro learning type earns a different number of commits based on
cognitive effort required.
```typescript
const COMMITS_PER_TYPE: Record<MicroLearningType, number> = {
concept_explainer: 10,
glossary_anchor: 10,
misconceptions: 15,
how_to: 15,
flashcard_set: 15,
comparison_card: 20,
reflection_prompt: 20,
scenario_quiz: 25,
myth_vs_evidence: 25,
case_study: 30,
}
```
First completion of a type the employee has never used before: +5 bonus
commits (rewards type diversity).
Duplicate completion (same topic + same type in same cycle): 0 commits.
Check session_completions before awarding — idempotent.
---
## Levels
Thresholds based on cumulative commits across all cycles.
```typescript
const LEVEL_THRESHOLDS = {
intern: 0,
junior: 100,
medior: 300,
senior: 600,
staff: 1000,
principal: 1500,
}
```
Level evaluated after every commit update. Level can only increase —
never decreases between cycles.
---
## Streak
Counts consecutive weeks with at least one completion.
Evaluation logic on every POST /complete:
```typescript
const lastActiveWeek = profile.last_active_week
const currentWeek = completionWeekNumber
if (currentWeek === lastActiveWeek) {
// Same week — streak unchanged
} else if (currentWeek === lastActiveWeek + 1) {
// Consecutive — increment
streak += 1
} else {
// Gap — reset to 1
streak = 1
}
profile.last_active_week = currentWeek
profile.longest_streak_weeks = Math.max(streak, profile.longest_streak_weeks)
```
Week number resets to 1 on cycle start. Streak does not reset on cycle
transition — a streak spanning the cycle boundary is maintained.
---
## Badges
### Badge seed data
Seeded into PocketBase badges collection at startup.
```typescript
const BADGE_DEFINITIONS = [
{ key: 'first_commit', tier: 'bronze',
label: 'First commit',
description: 'Complete any session' },
{ key: 'five_sessions', tier: 'silver',
label: 'Five sessions',
description: 'Complete 5 sessions using 5 different micro learning types' },
{ key: 'on_streak', tier: 'gold',
label: 'On a streak',
description: 'Complete 13 sessions without skipping a week' },
{ key: 'shipped', tier: 'legendary',
label: 'Shipped',
description: 'Complete all 26 sessions using all 10 micro learning types' },
{ key: 'governance_nerd', tier: 'content',
label: 'Governance nerd',
description: 'Complete all topics in the holacratic structure theme' },
{ key: 'process_architect', tier: 'content',
label: 'Process architect',
description: 'Complete all topics in the internal processes theme' },
{ key: 'deep_reader', tier: 'content',
label: 'Deep reader',
description: 'Use the case study micro learning type 5 or more times' },
{ key: 'handbook_expert', tier: 'content',
label: 'Handbook expert',
description: 'Complete all topics in the employee handbook theme' },
{ key: 'type_collector', tier: 'content',
label: 'Type collector',
description: 'Use all 10 micro learning types at least once' },
]
```
Content badges are theme-specific. Theme association resolved at runtime
by matching badge key to theme title pattern — not hardcoded to theme IDs.
```typescript
const THEME_BADGE_PATTERNS: Record<string, string> = {
'governance_nerd': 'holacrat',
'process_architect': 'process',
'handbook_expert': 'handbook',
}
```
Case-insensitive substring match on theme title.
---
### Badge evaluation
Run after every POST /complete. Check all conditions, award unearned badges.
```typescript
async function evaluateBadges(userId: string, profile: GamificationProfile):
Promise<BadgeDefinition[]> {
const earnedKeys = await getEarnedBadgeKeys(userId)
const newBadges: string[] = []
if (!earnedKeys.includes('first_commit')) {
const count = await countCompletions(userId)
if (count >= 1) newBadges.push('first_commit')
}
if (!earnedKeys.includes('five_sessions')) {
const sessions = await countUniqueSessions(userId)
if (sessions >= 5 && profile.types_used.length >= 5)
newBadges.push('five_sessions')
}
if (!earnedKeys.includes('on_streak')) {
if (profile.longest_streak_weeks >= 13) newBadges.push('on_streak')
}
if (!earnedKeys.includes('shipped')) {
const cycleComplete = await isCycleComplete(userId, profile.current_cycle)
if (cycleComplete && profile.types_used.length === 10)
newBadges.push('shipped')
}
if (!earnedKeys.includes('deep_reader')) {
const count = await countTypeCompletions(userId, 'case_study')
if (count >= 5) newBadges.push('deep_reader')
}
if (!earnedKeys.includes('type_collector')) {
if (profile.types_used.length === 10) newBadges.push('type_collector')
}
for (const [badgeKey, pattern] of Object.entries(THEME_BADGE_PATTERNS)) {
if (!earnedKeys.includes(badgeKey)) {
const complete = await isThemeComplete(userId, pattern)
if (complete) newBadges.push(badgeKey)
}
}
for (const key of newBadges) {
await awardBadge(userId, key, profile.current_cycle)
}
return newBadges.map(k => BADGE_DEFINITIONS.find(b => b.key === k)!)
}
```
---
## Milestone cards
Generated at weeks 13 and 26 of each cycle.
```typescript
async function generateMilestoneCard(
userId: string,
weekNumber: number,
cycle: number,
profile: GamificationProfile
): Promise<MilestoneCard | null> {
if (weekNumber !== 13 && weekNumber !== 26) return null
const exists = await milestoneExists(userId, cycle, weekNumber)
if (exists) return null
const badges = await getEarnedBadges(userId)
return await pocketbase.collection('milestone_cards').create({
user: userId,
cycle,
week: weekNumber,
total_commits: profile.total_commits,
streak_weeks: profile.current_streak_weeks,
badge_keys: badges.map(b => b.key),
created_at: new Date().toISOString()
})
}
```
---
## Heatmap data
Built from session_completions filtered by userId + cycle.
Returns 26 entries — one per week.
```typescript
async function buildHeatmap(userId: string, cycle: number):
Promise<HeatmapEntry[]> {
const completions = await pocketbase
.collection('session_completions')
.getFullList({ filter: `user="${userId}" && cycle=${cycle}` })
const byWeek = groupBy(completions, c => c.week_number)
return Array.from({ length: 26 }, (_, i) => ({
week: i + 1,
completions: byWeek[i + 1]?.length ?? 0
}))
}
```
---
## Environment variables
```
POCKETBASE_URL=
POCKETBASE_ADMIN_EMAIL=
POCKETBASE_ADMIN_PASSWORD=
PROGRESS_PORT=3005
```
---
## Dependencies
```json
{
"dependencies": {
"fastify": "^4",
"pocketbase": "^0.21",
"zod": "^3"
}
}
```
No AI calls. No Qdrant. No OpenAI. Pure business logic over PocketBase.
---
## TypeScript strict mode requirements
- No `any` types
- MicroLearningType as explicit string union
- Badge keys as explicit string union matching BADGE_DEFINITIONS
- COMMITS_PER_TYPE keyed by MicroLearningType — compile-time exhaustiveness
- HeatmapEntry, MilestoneCard, BadgeDefinition typed explicitly
---
## What this service does NOT do
- Does not generate content
- Does not handle curriculum scheduling → curriculum service
- Does not serve KB or micro learning data → frontend reads PocketBase
- Does not handle auth → PocketBase + frontend
---
## Testing checkpoints
1. POST /complete for new user → first_commit badge awarded, commits added
2. POST /complete same topic + type twice → 0 commits second call (idempotent)
3. Complete 5 sessions with 5 types → five_sessions badge awarded
4. Simulate 13 consecutive weekly completions → on_streak badge awarded
5. Skip a week → streak resets to 1
6. Complete all topics in a theme → content badge awarded
7. Use all 10 types → type_collector badge awarded
8. Complete week 13 → milestone_card created and returned in response
9. GET /leaderboard → all employees returned with correct fields
10. GET /feed → milestone cards ordered most recent first
11. Cycle transition: week 26 complete → streak spans boundary, level preserved,
heatmap resets for new cycle