Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 58 additions & 53 deletions docs/adaptive-gamification/indicators-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ flowchart TB
end

subgraph Ingestion["Aggregation & Context Layer"]
GIS["GamificationIndicatorsService<br/>(Date parsing & period partitioning)"]
CTX["IndicatorComputationContext<br/>(badges, players, checkins, periods)"]
GIS["GamificationIndicatorsService<br/>(Date & threshold validation)"]
CTX["IndicatorComputationContext<br/>(badges, players, checkins, threshold)"]
end

subgraph Strategy["Pluggable Strategy Engine"]
IFS["IndicatorFormulaStrategy<br/>(Injection Token Contract)"]
VBS["VanishingBadgesStrategy<br/>(Pure Mathematical Implementation)"]
end

subgraph Metrics["Calculated Indicators"]
ET["ET_b: Estimated Awarding Time"]
I3["i_3: Individual Player Recency"]
CII["CII: Community Interest Indicator"]
PMI["PMI / relPMI: Player Motivation"]
CMI["CMI: Community Motivation Indicator"]
CAND["Adaptation Candidate (Lowest CII)"]
subgraph Metrics["Calculated Indicators (Sept 18 Revision)"]
AB["AB(p): Achievable Badges [Def 3.1]"]
I3["i_3(p, b): Individual Player Interest [Def 3.2]"]
IGN["ignored_by(p): Ignored Badges [Def 3.3]"]
CII["CII(b): Community Interest Indicator [Def 3.4]"]
FILT["Candidate Pool Filtering [§4.2.1]"]
TRIG["Adaptation Trigger & Candidate [§4.1-4.2]"]
end

subgraph Output["Output Response DTOs"]
Expand All @@ -85,30 +85,29 @@ flowchart TB
| Rayuela Domain Entity / DAO | Input Role in Engine | Output Indicator |
| :--- | :--- | :--- |
| **`Project`** | Establishes the bounded community and context identifier. | `projectId` |
| **`BadgeRule` (`GamificationDao`)** | Defines target requirements, current badge status (`active`, `faded`, `expired`), and prerequisite DAG relationships. | $ep(b)$ (eligible pool), Prerequisite chains |
| **`BadgeRule` (`GamificationDao`)** | Defines target requirements, current badge status (`active`, `faded`, `expired`), and prerequisite DAG relationships. | $AB(p)$, $ep(b)$ (eligible pool), Candidate filtering (`allPlayerBadges`, `unreachableBadges`, `candidateBadges`) |
| **`User` (`UserDao`)** | Supplies player join dates ($t_{\text{join}}$) and registered account profiles. | $t_0(p, b)$ (baseline date for root badges) |
| **`Checkin` (`CheckInDao`)** | Provides timestamped citizen science contributions, partitioned into consecutive calendar periods $s_1, s_2, \dots, s_n$. | $cnum(p, s)$, $PMI(p)$, $relPMI(p)$, $CMI$ |
| **`Move` (`MoveDao`)** | Records historical award events, tracking the exact contributions and timestamps at which players unlocked badges. | $U_b$ (earners set), $ET_b$, $t_0(p, b)$ for child badges |
| **`Checkin` (`CheckInDao`)** | Provides timestamped citizen science contributions. | `totalContributions`, `activePlayers` |
| **`Move` (`MoveDao`)** | Records historical award events, tracking the exact timestamps at which players unlocked badges. | $B_p$, $U_b$ (earners set), $t_0(p, b)$ for child badges |

---

## 3. Mathematical Indicators Summary
## 3. Mathematical Indicators Summary (Sept 18, 2026 Revision)

The default implementation (`VanishingBadgesStrategy`) evaluates:
The default implementation (`VanishingBadgesStrategy`) evaluates Definitions 3.1–3.4 and Section 4.1–4.2 of the Vanishing Badges specification:

1. **Estimated Awarding Time ($ET_b$)**: Historical average check-ins required by players who earned badge $b$. Falls back to immediate prerequisite thresholds during cold starts.
2. **Individual Recency Interest ($i_3(p, b)$)**: Measures how recently player $p$ became eligible for badge $b$:
$$\text{recency}(p, b) = \text{asOfDate} - t_0(p, b)$$
$$i_3(p, b) = \frac{1}{\text{recency}(p, b) + 1}$$
If badge $b$ is unachievable for player $p$ (unmet prerequisites), $i_3(p, b) = 1.0$.
3. **Community Interest Indicator ($CII(b)$)**: The median individual interest across all players eligible to earn badge $b$ ($ep(b)$):
$$CII(b) = \text{median}(\{i_3(p, b) : p \in ep(b)\})$$
A declining $CII$ signals that eligible players are stalling and losing interest in badge $b$.
4. **Player Motivation Indicator ($PMI(p)$)**: The number of periods where player $p$ sustained or increased contributions compared to the previous period:
$$cnum(p, s) \ge cnum(p, \text{prev}(s)) \land (cnum(p, s) + cnum(p, \text{prev}(s)) > 0)$$
5. **Community Motivation Indicator ($CMI$)**: The median relative motivation across all active contributors:
$$\overline{PMI} = \frac{1}{|P|} \sum_{p \in P} PMI(p), \quad relPMI(p) = \frac{PMI(p)}{\overline{PMI}}, \quad CMI = \text{median}(\{relPMI(p) : p \in P\})$$
6. **Adaptation Candidate Badge ($b^*$)**: Identifies the active badge with the lowest $CII$, representing the primary bottleneck where community interest has waned the most.
1. **Achievable Badges ($AB(p)$ — Def 3.1)**: A badge is achievable to a player when all prerequisite badges (`previousBadges`) have been earned:
$$AB(p) = \{\, b \in \text{Badges} \mid \forall prev \in \text{prerequisites}(b),\, prev \in B_p \,\}$$
2. **Individual Interest Indicator ($i_3(p, b)$ — Def 3.2)**: Measures the inverse of the elapsed time (in days) since player $p$ met the prerequisites for badge $b$ ($t_0(p, b)$):
$$i_3(p, b) = \begin{cases} \dfrac{1}{\text{now} - t_0(p, b)} & : b \in AB(p) \\ 1.0 & : b \notin AB(p) \end{cases}$$
3. **Ignored Badges ($\text{ignored\_by}(p)$ — Def 3.3)**: The subset of achievable, unearned badges for player $p$ whose individual interest has fallen below reference threshold $x$ (default $x = 0.20$, equivalent to $> 5$ days elapsed):
$$\text{ignored\_by}(p) = \{\, b \in AB(p) \setminus B_p \mid i_3(p, b) < x \,\}$$
4. **Community Interest Indicator ($CII(b)$ — Def 3.4)**: The median individual interest across all players eligible to earn badge $b$ ($ep(b) = \{\, p \in P \mid b \in AB(p) \setminus B_p \,\}$):
$$CII(b) = \text{median}(\{\, i_3(p, b) \mid p \in ep(b) \,\})$$
5. **Candidate Filtering (§4.2.1)**: Excludes badges already earned by everyone in the community (`allPlayerBadges`), badges unreachable by anyone (`unreachableBadges`), and expired badges to yield `candidateBadges`.
6. **Adaptation Trigger & Selection (§4.1 & §4.2.2–4.2.3)**:
$$\text{Trigger Adaptation} \iff \exists b \in \text{Candidates} : CII(b) < x$$
Candidate badges are sorted ascending by $CII(b)$ to identify the primary `adaptationCandidateBadge`.

---

Expand All @@ -117,20 +116,20 @@ The default implementation (`VanishingBadgesStrategy`) evaluates:
### Current State: On-Demand REST Endpoint
The engine is currently exposed via an authenticated HTTP endpoint:
```http
GET /v1/gamification-indicators/:projectId?startDate=01-07-2026&asOfDate=20-07-2026&daysPerPeriod=7
GET /v1/gamification-indicators/:projectId?asOfDate=20-07-2026&threshold=0.20
Authorization: Bearer <JWT_TOKEN>
```
This allows:
- Real-time exploration and parameter tuning (`daysPerPeriod`, custom `startDate`, point-in-time `asOfDate`).
- Real-time exploration and parameter tuning (`threshold`, point-in-time `asOfDate`, `minActiveCheckins`).
- Visual debugging through documentation tools and simulators.
- Empirical verification of community health before enabling automated rule mutations.
- Empirical verification of community interest before enabling automated rule mutations.

### Future State: Automated Badge Fading Adaptation Loop
In upcoming phases, this engine will directly drive the **Badge Fading** adaptation strategy:
1. A recurring cron worker periodically runs `calculateIndicators` for active projects.
2. If community motivation drops below threshold ($CMI < \tau_{\text{trigger}}$), the adaptation pipeline is triggered.
1. Periodically or upon check-in registration, the system runs `calculateIndicators` for active projects.
2. If any candidate badge's community interest drops below threshold ($\exists b \in \text{Candidates} : CII(b) < x$), `isTriggered` becomes `true`.
3. The engine selects the candidate badge with lowest interest ($b^* = \arg\min CII(b)$).
4. The system automatically transitions badge $b^*$ to `faded` status with an expiration timestamp (`expiresAt`), broadcasting notifications to prompt community participation.
4. The system automatically transitions badge $b^*$ to `faded` status with an expiration timestamp (`expiresAt`), broadcasting notifications to prompt community participation during the Step 5 running impact window.

---

Expand All @@ -142,35 +141,39 @@ In upcoming phases, this engine will directly drive the **Badge Fading** adaptat

| Parameter | Type | Required | Default | Description |
| :--- | :--- | :--- | :--- | :--- |
| `startDate` | string | No | Earliest checkin | Timeline origin for Period 1. Accepts ISO-8601, `DD-MM-YYYY`, or `DD/MM/YYYY`. |
| `asOfDate` | string | No | Now | Horizon timestamp. Accepts ISO-8601, `DD-MM-YYYY`, or `DD/MM/YYYY`. |
| `daysPerPeriod` | number | No | `7` | Duration of each period $s$ in days (default: weekly). |
| `minActiveCheckins` | number | No | `1` | Minimum contributions required to qualify as an active player. |
| `threshold` | number | No | `0.2` | Reference threshold $x$ for ignored badges (Def 3.3) and adaptation trigger (§4.1). |
| `minActiveCheckins` | number | No | `0` | Optional minimum contributions required to include a player in the evaluated pool $P$. |

#### Sample Response (`200 OK`)

```json
{
"projectId": "67702f23258db9ef444b0e8b",
"currentPeriod": 3,
"startDate": "2026-07-01T00:00:00.000Z",
"asOfDate": "2026-07-20T23:59:59.999Z",
"daysPerPeriod": 7,
"totalPlayers": 12,
"threshold": 0.2,
"totalPlayers": 10,
"activePlayers": 8,
"totalContributions": 45,
"avgPMI": 1.5,
"CMI": 1.0,
"isTriggered": true,
"triggerBadges": ["b-expert-mapper"],
"communityIgnoredCount": 1,
"totalPlayerIgnored": 4,
"allPlayerBadges": ["b-first-checkin"],
"unreachableBadges": ["b-master-scientist"],
"candidateBadges": ["b-expert-mapper"],
"lowestCII": 0.1429,
"adaptationCandidateBadge": {
"badgeId": "b-expert-mapper",
"badgeName": "Mapeador Experto",
"status": "active",
"earnedCount": 1,
"earnedUsers": ["user_101"],
"ET_b": 15.0,
"eligibleCount": 4,
"eligibleUsers": ["user_102", "user_103", "user_104", "user_105"],
"CII": 0.0526,
"CII": 0.1429,
"isCommunityIgnored": true,
"isCandidate": true,
"isLowestCII": true
},
"badges": [
Expand All @@ -180,24 +183,26 @@ In upcoming phases, this engine will directly drive the **Badge Fading** adaptat
"status": "active",
"earnedCount": 1,
"earnedUsers": ["user_101"],
"ET_b": 15.0,
"eligibleCount": 4,
"eligibleUsers": ["user_102", "user_103", "user_104", "user_105"],
"CII": 0.0526,
"CII": 0.1429,
"isCommunityIgnored": true,
"isCandidate": true,
"isLowestCII": true
}
],
"players": [
{
"playerId": "user_102",
"totalContributions": 7,
"periodContributions": {
"1": 2,
"2": 2,
"3": 3
},
"PMI": 2,
"relPMI": 1.333
"earnedBadges": ["b-first-checkin"],
"achievableBadges": ["b-first-checkin", "b-expert-mapper"],
"ignoredBadges": ["b-expert-mapper"],
"individualInterest": {
"b-first-checkin": 1.0,
"b-expert-mapper": 0.1429,
"b-master-scientist": 1.0
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,7 @@ export interface IndicatorFormulaStrategy {
): CommunityIndicatorResult;

/**
* Definition 3.1: Estimated Awarding Time (ET_b).
* Historical average of contributions at award time for players who earned badge b.
*/
computeEstimatedAwardingTime(
badge: BadgeDefinition,
allBadges: BadgeDefinition[],
earnedPlayers: { playerId: string; contribsAtEarn: number }[],
): number;

/**
* Definition 3.1: Achievable Badges AB(p) & t_0(p, b).
* Evaluates if badge b is achievable by player p (all prerequisites met)
* and calculates t_0(p, b), the timestamp when badge b became achievable.
*/
Expand All @@ -43,7 +34,7 @@ export interface IndicatorFormulaStrategy {
): { isAchievable: boolean; t0Date: Date };

/**
* Definition 3.2: Individual Interest Indicator i3(p, b).
* Definition 3.2: Individual Interest Indicator i_3(p, b).
* 1 / (now - t0) for achievable badges; 1.0 if not achievable.
*/
computeIndividualInterest(
Expand All @@ -55,29 +46,20 @@ export interface IndicatorFormulaStrategy {
): number;

/**
* Definition 3.3: Community Interest Indicator CII(b).
* Median of i3(p, b) across all eligible players ep(b).
* Returns null if eligible pool is empty.
*/
computeCommunityInterest(eligibleI3Values: number[]): number | null;

/**
* Definition 3.4: Player Motivation Indicator PMI(p).
* Counts periods s in S where contributions grew or held steady with non-zero activity.
* Definition 3.3: Ignored Badges ignored_by(p).
* ignored_by(p) = { b in AB(p) - B_p : i_3(p, b) < x }
*/
computePlayerMotivation(
periodContributions: Record<number, number>,
currentPeriod: number,
): number;
computeIgnoredBadges(
achievableBadgeIds: string[],
earnedBadgeIds: Set<string>,
playerI3Map: Record<string, number>,
threshold: number,
): string[];

/**
* Definitions 3.4 & 3.5: Relative PMI and Community Motivation Indicator (CMI).
* relPMI(p) = PMI(p) / avgPMI
* CMI = median({ relPMI(p) : p in P })
* Definition 3.4: Community Interest Indicator CII(b).
* Median of i_3(p, b) across all eligible players ep(b) = { p in P : b in AB(p) - B_p }.
* Returns null if eligible pool is empty.
*/
computeCommunityMotivation(playerPMIs: { playerId: string; pmi: number }[]): {
avgPMI: number;
playerRelPMIs: Record<string, number>;
CMI: number;
};
computeCommunityInterest(eligibleI3Values: number[]): number | null;
}
55 changes: 36 additions & 19 deletions src/module/gamification-indicators/domain/indicator.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ export interface IndicatorComputationContext {
badges: BadgeDefinition[];
players: PlayerProfile[];
checkins: CheckinRecord[];
startDate?: Date;
asOfDate: Date;
daysPerPeriod: number;
/** Threshold x for ignored badges (Def 3.3) and adaptation trigger (§4.1), default 0.20 */
threshold?: number;
/** Optional minimum check-ins required to include a player in the evaluated pool P */
minActiveCheckins?: number;
}

Expand All @@ -52,41 +53,57 @@ export interface BadgeIndicatorResult {
status: string;
earnedCount: number;
earnedUsers: string[];
/** ET_b: Estimated Awarding Time (historical average contributions at award time) */
ET_b: number;
/** ep(b): Eligible players who can achieve b and have not yet earned it */
/** ep(b): Eligible players who can achieve b (b in AB(p)) and have not yet earned it */
eligibleCount: number;
eligibleUsers: string[];
/** CII(b): Community Interest Indicator (median of i3 across eligible players) */
/** CII(b): Community Interest Indicator [Def 3.4] (median of i3 across eligible players) */
CII: number | null;
/** True when CII(b) < threshold x (§4.1) */
isCommunityIgnored: boolean;
/** True when badge is in the filtered candidate pool (§4.2.1) */
isCandidate: boolean;
isLowestCII: boolean;
}

export interface PlayerIndicatorResult {
playerId: string;
totalContributions: number;
periodContributions: Record<number, number>;
/** PMI(p): Player Motivation Indicator (count of periods with positive/non-declining activity) */
PMI: number;
/** relPMI(p): Relative Player Motivation normalized to community average */
relPMI: number;
/** B_p: Badge IDs already earned by player p */
earnedBadges: string[];
/** AB(p): Achievable badges for player p whose prerequisites are met [Def 3.1] */
achievableBadges: string[];
/** ignored_by(p): Achievable unearned badges with i3(p, b) < threshold x [Def 3.3] */
ignoredBadges: string[];
/** i_3(p, b): Individual interest score per badge ID [Def 3.2] */
individualInterest: Record<string, number>;
}

export interface CommunityIndicatorResult {
projectId: string;
currentPeriod: number;
startDate: string;
asOfDate: string;
daysPerPeriod: number;
/** Reference threshold x used for Def 3.3 and §4.1 */
threshold: number;
totalPlayers: number;
activePlayers: number;
totalContributions: number;
/** Community average PMI across players */
avgPMI: number;
/** CMI: Community Motivation Indicator (median relPMI across players) */
CMI: number;
/** §4.1 Trigger status: true if there exists a candidate badge b with CII(b) < x */
isTriggered: boolean;
/** Candidate badge IDs whose CII(b) < x (§4.1 / §4.2.3) */
triggerBadges: string[];
/** Number of badges ignored at the community level (CII(b) < x) */
communityIgnoredCount: number;
/** Sum of |ignored_by(p)| across all evaluated players */
totalPlayerIgnored: number;
/** §4.2.1 Badges earned by all evaluated players (assigned_badges) */
allPlayerBadges: string[];
/** §4.2.1 Badges unreachable by any evaluated player (unreachable_badges) */
unreachableBadges: string[];
/** §4.2.1 Filtered candidate badge IDs eligible for vanishing selection */
candidateBadges: string[];
/** Lowest CII value across badges with non-empty eligible pools */
lowestCII: number | null;
badges: BadgeIndicatorResult[];
players: PlayerIndicatorResult[];
/** Lowest CII candidate badge recommended for fading if adaptation is triggered */
/** Lowest CII active candidate badge recommended for fading if adaptation is triggered */
adaptationCandidateBadge: BadgeIndicatorResult | null;
}
Loading
Loading