Skip to main content

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

When KB_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.phpgamification.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 any user_id in the tenant.
All three delegate to the single 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/badges read 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:
Read the caller’s badges (progress shown for locked ones):
Retune the catalog — e.g. a stricter “author” badge:

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).
Persisted to 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

The catalog is operator input. Entries missing key / metric / threshold (or with non-scalar values) are silently dropped rather than crashing the awarding loop or the dashboard — but a typo means a badge simply won’t appear. Validate your catalog after editing.
  • With gamification off, the /app/me badges section does not render at all (not an empty box) and gamification:recompute is 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.
See the Engagement Suite overview and Dashboards for where badges surface.