> ## Documentation Index
> Fetch the complete documentation index at: https://doc.askmydocs.padosoft.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Gamification

> A tasteful, default-ON (since v8.18) contributor-motivation layer: badges awarded when all-time engagement crosses a configurable threshold, plus an AI coaching layer over curation-quality metrics — a view over engagement data, computing nothing new for badges.

## 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:

| Metric        | Definition (all-time, SQL-aggregated)   |
| ------------- | --------------------------------------- |
| `score`       | Sum of contribution-event `weight`.     |
| `events`      | Count of contribution events.           |
| `authored`    | Distinct documents created or promoted. |
| `active_days` | Distinct calendar days with activity.   |

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

```mermaid theme={null}
flowchart LR
  CE[kb_contribution_events] --> GS[GamificationService]
  CFG[config kb.gamification.badges] --> GS
  GS -->|evaluate, idempotent| BADGE[(kb_user_badges)]
  GS -->|badgesFor| HTTP[/api/me/badges]
  GS -->|badgesFor| MCP[KbUserBadgesTool]
  CMD[gamification:recompute] --> GS
  HTTP --> UI[My KB dashboard · MeBadges]
```

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.php` → `gamification.badges`) — fully operator-tunable:

| Badge                | Icon | Metric        | Threshold |
| -------------------- | ---- | ------------- | --------- |
| `first_contribution` | 🌱   | `events`      | 1         |
| `contributor`        | ✍️   | `score`       | 25        |
| `prolific`           | 🚀   | `score`       | 100       |
| `author`             | 📚   | `authored`    | 5         |
| `regular`            | 🔥   | `active_days` | 5         |

| Knob      | Env                       | Default                         |
| --------- | ------------------------- | ------------------------------- |
| `enabled` | `KB_GAMIFICATION_ENABLED` | `true` (default-ON since v8.18) |
| `badges`  | — (config catalog)        | the five above                  |

**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:

```bash theme={null}
# .env
KB_GAMIFICATION_ENABLED=true

php artisan gamification:recompute --tenant=acme
# [acme] contributors=18 badges_awarded=23
```

Read the caller's badges (progress shown for locked ones):

```bash theme={null}
curl -s -H "Authorization: Bearer $TOKEN" https://kb.example.com/api/me/badges
# {"enabled":true,"badges":[{"key":"contributor","earned":true,"progress":25,"threshold":25}, ...]}
```

Retune the catalog — e.g. a stricter "author" badge:

```php theme={null}
// config/kb.php → gamification.badges
['key' => 'author', 'label' => 'Author', 'icon' => '📚', 'metric' => 'authored', 'threshold' => 10],
```

## 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):

| Signal                   | Derived from                                             |
| ------------------------ | -------------------------------------------------------- |
| Frontmatter completeness | `knowledge_documents.frontmatter_json` on canonical docs |
| Evidence rigor           | distribution of `evidence_tier`                          |
| Canonicalization rate    | % authored docs reaching `accepted` canonical status     |
| Stewardship / freshness  | avg `kb_canonical_health_snapshot.health_score`          |
| Graph connectivity       | `kb_edges` sourced from the docs                         |
| Structural depth         | avg chunks per doc                                       |

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

```mermaid theme={null}
flowchart LR
  QM[GamificationQualityMetricsService<br/>SQL, tenant-scoped] --> CORE[GamificationInsightsService]
  NAR[GamificationNarratorService<br/>LLM → deterministic fallback] --> CORE
  CORE -->|upsert, idempotent| T[(kb_gamification_insights)]
  CMD[gamification:narrate<br/>weekly] --> CORE
  T --> HTTP1[/api/me/coaching]
  T --> HTTP2[/api/admin/engagement/insights]
  T --> MCP[KbGamificationInsightsTool]
  HTTP1 --> UI1[My KB · CoachingCard]
  HTTP2 --> UI2[Admin · GamificationInsightsPanel]
```

### 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

<Warning>
  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.
</Warning>

* 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](/scheduler-and-maintenance).

See the [Engagement Suite overview](/engagement-suite) and
[Dashboards](/dashboards) for where badges surface.
