Motivation / problem
Most knowledge bases are written by a handful of people while everyone else only reads. Recognition is the cheapest lever to widen that base — but heavy-handed gamification (points spam, public shaming leaderboards) does more harm than good in a professional tool. AskMyDocs ships a tasteful layer: a small set of badges that reflect real contribution — on by default since v8.18 (one env flag turns the whole layer off), fully tunable.Theory & background
Gamification computes nothing new. A badge is a threshold over an all-time engagement metric already derived from the contribution log:
A badge is earned when the user has an award row or their live metric
currently meets the threshold — so the dashboard never lags a nightly run.
Awarding is idempotent (a unique
(tenant, user, badge) constraint +
insertOrIgnore).
Design
WhenKB_GAMIFICATION_ENABLED=false (gamification is on by default since v8.18;
set this to turn it off) evaluate() awards nothing and
badgesFor() returns enabled:false with an empty roster, so the dashboard
section is absent. Both states are tested (R43).
Data model / contract
The default catalog (config/kb.php → gamification.badges) — fully operator-tunable:
Surfaces (tri-surface, R44):
- Command:
gamification:recompute {--tenant=}— per-tenant, no-op when disabled; scheduled nightly at 05:20. - HTTP:
GET /api/me/badges— the caller’s catalog with earned/progress. - MCP:
KbUserBadgesTool— badges for anyuser_idin the tenant.
GamificationService.
Decision rationale (ADR-style)
- Default-ON (since v8.18), both states tested (R43). Recognition is a default
worth shipping, so a fresh deploy now has gamification on — but it stays a single
env flag (
KB_GAMIFICATION_ENABLED=false) away from fully off, and BOTH states are covered by tests so flipping the knob holds no surprises. - A view, not a new signal. Badges read the same metrics the dashboards and digests use; there is no separate scoring system to keep in sync.
- Config-driven catalog. Labels, icons, metrics and thresholds live in config so an operator can retune (and per-tenant overrides can layer on later) without a code change. Malformed entries are defensively dropped, never crash.
- Single-sourced awarding. The nightly command and the live
/api/me/badgesread both call the same evaluation, so a badge can’t appear in one surface and not the other.
Worked example
Enable gamification and award badges for one tenant:AI coaching insights (v8.18)
Threshold badges measure quantity. v8.18 adds an AI layer that measures, and narrates, curation quality — at the user, project and tenant level — without adding a single new write path. It mirrors the digest narrator: deterministic metrics first, an LLM only to phrase them, and it always degrades to deterministic copy.What it computes
GamificationQualityMetricsService derives quality signals from data that
already exists (all tenant-scoped, all SQL-aggregated — R30/R3, no PHP-side id
lists):
A transparent weighted mean of canonicalization rate, frontmatter completeness, evidence coverage, and stewardship/freshness is the composite health score (0–100).
What it narrates
GamificationNarratorService (mirrors AiDigestNarrator) turns the metrics JSON
into: a user coaching card (strengths · growth · next steps · summary) + fun
AI-awarded period titles (“Il Cartografo”, “Evidence Champion”, “Lo Steward”);
a project health narrative + 3 concrete actions; and a tenant executive
narrative. On any LLM failure — or when KB_GAMIFICATION_AI_ENABLED=false — it
returns the deterministic copy with model=null. The merge is type-reconciled
so a partial/mis-typed model response can never white-screen the FE (R14).
Surfaces & cadence (tri-surface, R44)
- Command:
gamification:narrate {--tenant=} {--period=}— weekly Tier-1 scheduler slot (gamification_narrate), no-op when disabled. - HTTP:
GET /api/me/coaching(self-scoped),GET /api/admin/engagement/insights?scope=project|tenant&id=…(admin),POST …/regenerate(super-admin, on-demand). - MCP:
KbGamificationInsightsTool(scope=user|project|tenant).
kb_gamification_insights (tenant-aware; composite UNIQUE
(tenant_id, scope_type, scope_id, period_label), atomic upsert) so no LLM call
runs in a request hot path. Config: kb.gamification.ai.*
(KB_GAMIFICATION_AI_{ENABLED,PROVIDER,MODEL,MAX_TOKENS}), default a free
OpenRouter model. The AI layer requires the master KB_GAMIFICATION_ENABLED
to be on (R43 — both states tested for badges AND insights).
Gotchas & operations
- With gamification off, the
/app/mebadges section does not render at all (not an empty box) andgamification:recomputeis a clean no-op. - The nightly recompute evaluates one contributor at a time (bounded by contributor count) — a deliberate trade-off that keeps the awarding logic single-sourced with the live read path. See Scheduler & Maintenance.