diff --git a/.agents/skills/constructive-sdk-events.zip b/.agents/skills/constructive-sdk-events.zip index d60a73f..9edda0e 100644 Binary files a/.agents/skills/constructive-sdk-events.zip and b/.agents/skills/constructive-sdk-events.zip differ diff --git a/.agents/skills/constructive-sdk-events/SKILL.md b/.agents/skills/constructive-sdk-events/SKILL.md index f1ea10e..820e16b 100644 --- a/.agents/skills/constructive-sdk-events/SKILL.md +++ b/.agents/skills/constructive-sdk-events/SKILL.md @@ -1,6 +1,6 @@ --- name: constructive-sdk-events -description: "Events, achievements, and gamification — EventTracker blueprint node for recording events on row changes, blueprint achievements[] for defining levels with requirements and credit rewards (limit_credit + meter_credit with expires_interval), invite-based achievements (has_invite_achievements), period-aware event_aggregates (lazy count reset), re-triggerable achievements (per-period re-qualification), EventReferral for attributing events to inviters, and the full virality chain. Use when asked to 'add analytics', 'track events', 'add achievements', 'gamification', 'record events', 'EventTracker', 'level requirements', 'achievement rewards', 'invite achievements', 'invite virality', 'credit grants for achievements', 'meter_credit', 'expires_interval', 'period_interval', 'recurring credits', 'referral credits', 'EventReferral', or when working with events_module in blueprints." +description: "Events, achievements, and gamification — EventTracker blueprint node for recording events on row changes, blueprint achievements[] for defining levels with requirements and credit rewards (limit_credit + meter_credit with expires_interval), invite-based achievements (has_invite_achievements), period-aware event_aggregates (lazy count reset), re-triggerable achievements (per-period re-qualification), EventReferral for attributing events to inviters (with multi-level max_depth for MLM referral chains), and the full virality chain. Use when asked to 'add analytics', 'track events', 'add achievements', 'gamification', 'record events', 'EventTracker', 'level requirements', 'achievement rewards', 'invite achievements', 'invite virality', 'credit grants for achievements', 'meter_credit', 'expires_interval', 'period_interval', 'recurring credits', 'referral credits', 'EventReferral', 'max_depth', 'multi-level referral', 'MLM', 'referral chain', or when working with events_module in blueprints." metadata: author: constructive-io version: "1.0.0" @@ -14,7 +14,7 @@ Three capabilities compose together: - **`EventTracker`** — table-level node. Attach to any table to declaratively record events when rows change. Same compound condition system as JobTrigger. - **`achievements[]`** — top-level blueprint section. Define levels with requirements (event counts) and optional rewards (`limit_credit` or `meter_credit` grants, with optional `expires_interval`). - **`has_invite_achievements`** — entity type flag. Auto-attaches EventTracker to `claimed_invites` and wires the invitee achievement virality chain. -- **`EventReferral`** — entity type node. Wires referral attribution so that when an invitee's event is recorded, the inviter also gets an attributed event. +- **`EventReferral`** — table-level node. Wires referral attribution so that when an invitee performs an action, the inviter gets an attributed event. Supports `max_depth` (1–10) for multi-level referral chains — walks up the `claimed_invites` chain N levels, crediting each ancestor. - **Period-aware counting** — event types with `period_interval` auto-reset aggregate counts each period (lazy reset). Enables re-triggerable achievements for recurring credit grants. Related skills: @@ -116,6 +116,64 @@ See [references/event-tracker.md](references/event-tracker.md) for compound cond --- +## EventReferral Blueprint Node + +Add `EventReferral` to a table's `nodes[]` to credit the actor's inviter (and optionally their inviter's inviter, etc.) when a row changes: + +```json +{ + "tables": [{ + "table_name": "user_uploads", + "nodes": [ + { "$type": "EventReferral", "data": { + "event_name": "invitee_uploaded", + "events": ["INSERT"], + "actor_field": "owner_id", + "max_depth": 5 + }} + ] + }] +} +``` + +### Configuration Reference + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `event_name` | string | **(required)** | Event type name to record for each ancestor in the invite chain | +| `events` | `("INSERT" \| "UPDATE" \| "DELETE")[]` | `["INSERT"]` | Which DML events fire the trigger | +| `actor_field` | string (column-ref) | `"owner_id"` | Column containing the invitee (actor) ID — used to look up the referrer via `claimed_invites.receiver_id` | +| `entity_field` | string (column-ref) | — | Column containing the entity ID for entity-scoped referral events. **Cannot be combined with `max_depth > 1`.** | +| `max_depth` | integer | `1` | How many levels up the invite chain to walk. `1` = direct inviter only (default, backward compatible). `2`–`10` = multi-level referral chain. Hard cap at 10. | +| `auto_register_type` | boolean | `true` | Automatically register the `event_name` in the `event_types` catalog during provisioning | +| `conditions` | object \| array | — | Compound conditions for WHEN clause (same syntax as EventTracker) | + +### Toggles & Controls + +- **Build-time:** `max_depth` is the toggle. Default `1` = today's single-hop behavior. Set `2`–`10` to opt in to multi-level. Omitting or setting to `1` produces the exact same trigger as before. +- **Runtime:** `event_types.is_active` — flip to `false` to pause referral rewards without redeploying. +- **Scope constraint:** `max_depth > 1` requires app-level scope only (`entity_field` must be omitted). The generator raises an exception if both are set. + +### How Multi-Level Works + +When `max_depth > 1`, the trigger builds a FOR loop that walks `claimed_invites`: + +``` +User action (INSERT on user_uploads) + → trigger resolves NEW.owner_id + → FOR depth IN 1..max_depth: + SELECT sender_id FROM claimed_invites WHERE receiver_id = current + EXIT WHEN no sender found + record_event(event_name, sender_id) + current := sender_id +``` + +Each ancestor in the chain receives the same event. Combined with tiered achievement thresholds, this creates natural attenuation — direct inviters accumulate events quickly (low threshold, high reward), while deeper ancestors accumulate slowly (high threshold, low reward). + +See [references/event-referral.md](references/event-referral.md) for multi-level blueprint examples, the viral loop pattern, and attenuation design. + +--- + ## Blueprint Achievements The top-level `achievements[]` section defines levels with requirements and optional rewards. Processed in **Phase 7** of `constructBlueprint()` — after tables, relations, and entity types. @@ -265,6 +323,7 @@ See [references/invite-virality.md](references/invite-virality.md) for detailed | File | Contents | |------|----------| | [references/event-tracker.md](references/event-tracker.md) | Full EventTracker parameter reference, compound conditions, toggle mode, entity-scoped examples | +| [references/event-referral.md](references/event-referral.md) | EventReferral parameter reference, multi-level `max_depth` chain walk, MLM blueprint examples, attenuation design | | [references/achievements.md](references/achievements.md) | Achievement definitions, requirements, rewards (limit_credit + meter_credit), expires_interval, period-aware aggregates, re-triggerable achievements | | [references/invite-virality.md](references/invite-virality.md) | Simple + meta invite tiers, full virality chain, cross-entity examples | | [references/triggers.md](references/triggers.md) | Internal trigger reference: tg_check_achievements, tg_achievement_reward, tg_invitee_achievement | diff --git a/.agents/skills/constructive-sdk-events/references/event-referral.md b/.agents/skills/constructive-sdk-events/references/event-referral.md new file mode 100644 index 0000000..018a620 --- /dev/null +++ b/.agents/skills/constructive-sdk-events/references/event-referral.md @@ -0,0 +1,176 @@ +# EventReferral Reference + +EventReferral is a table-level blueprint node that attributes events to the actor's inviter(s) when a row changes. It resolves the referrer via the invites module's `claimed_invites` table. + +## Single-Level Referral (Default) + +With `max_depth: 1` (or omitted), EventReferral credits only the direct inviter: + +```json +{ + "tables": [{ + "table_name": "user_profiles", + "nodes": [ + { "$type": "EventReferral", "data": { + "event_name": "invitee_completed_profile", + "events": ["UPDATE"], + "actor_field": "owner_id" + }} + ] + }] +} +``` + +When User B (invited by User A) updates their profile: +1. Trigger fires → resolves `NEW.owner_id` (User B) +2. Looks up `claimed_invites WHERE receiver_id = B` → finds User A +3. Calls `record_event('invitee_completed_profile', A)` + +## Multi-Level Referral (max_depth > 1) + +Set `max_depth` to walk up the invite chain multiple levels: + +```json +{ + "tables": [{ + "table_name": "user_uploads", + "nodes": [ + { "$type": "EventReferral", "data": { + "event_name": "invitee_uploaded", + "events": ["INSERT"], + "actor_field": "owner_id", + "max_depth": 5 + }} + ] + }] +} +``` + +When User D (chain: A invited B invited C invited D) uploads a file: +1. Depth 1: `claimed_invites WHERE receiver_id = D` → C. Records event for C. +2. Depth 2: `claimed_invites WHERE receiver_id = C` → B. Records event for B. +3. Depth 3: `claimed_invites WHERE receiver_id = B` → A. Records event for A. +4. Depth 4: `claimed_invites WHERE receiver_id = A` → NULL. Loop exits. + +All three ancestors (C, B, A) receive the same `invitee_uploaded` event. The loop stops early if the chain is shorter than `max_depth`. + +## Configuration Reference + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `event_name` | string | **(required)** | Event type name to record for each ancestor | +| `events` | `("INSERT" \| "UPDATE" \| "DELETE")[]` | `["INSERT"]` | Which DML events fire the trigger | +| `actor_field` | string (column-ref) | `"owner_id"` | Column containing the invitee (actor) ID | +| `entity_field` | string (column-ref) | — | Entity ID column for entity-scoped events. **Cannot be combined with `max_depth > 1`.** | +| `max_depth` | integer | `1` | Levels to walk up the invite chain. Range: 1–10. | +| `auto_register_type` | boolean | `true` | Auto-register `event_name` in `event_types` during provisioning | +| `conditions` | object \| array | — | Compound conditions for WHEN clause | + +## Constraints + +- **`max_depth` range:** 1–10. The generator raises an exception for values outside this range. +- **App-level scope only:** When `max_depth > 1`, `entity_field` must be omitted. The chain walk uses `claimed_invites` which is scoped by membership_type at the app level. Entity-scoped actions still credit the chain — the trigger resolves the *user* who performed the action, not the entity. +- **Backward compatible:** `max_depth: 1` (or omitted) produces the exact same single-lookup trigger as before the feature existed. + +## Toggles + +| Toggle | Type | How | +|--------|------|-----| +| Build-time on/off | `max_depth` parameter | `1` = off (single hop), `2`–`10` = on (multi-level) | +| Runtime on/off | `event_types.is_active` | Set to `false` to pause referral event recording without redeploying | + +## Multi-Level MLM Blueprint Example + +A complete blueprint showing 5-level referral rewards with tiered achievements: + +```json +{ + "entity_types": [ + { + "name": "App Members", + "prefix": "app", + "has_invites": true, + "has_levels": true, + "has_limits": true + } + ], + + "tables": [ + { + "table_name": "databases", + "fields": [ + { "name": "name", "type": "text", "is_required": true }, + { "name": "owner_id", "type": "uuid", "is_required": true } + ], + "nodes": [ + { "$type": "EventReferral", "data": { + "event_name": "invitee_created_db", + "events": ["INSERT"], + "actor_field": "owner_id", + "max_depth": 5 + }}, + { "$type": "LimitCounter", "data": { "limit_name": "databases" } } + ] + } + ], + + "achievements": [ + { + "name": "referral_bronze", + "description": "3 people in your network created a database", + "priority": 10, + "requirements": [ + { "event_name": "invitee_created_db", "count": 3 } + ], + "rewards": [ + { "reward_type": "limit_credit", "target_name": "databases", "amount": 5 } + ] + }, + { + "name": "referral_silver", + "description": "10 people in your network created a database", + "priority": 20, + "requirements": [ + { "event_name": "invitee_created_db", "count": 10 } + ], + "rewards": [ + { "reward_type": "limit_credit", "target_name": "databases", "amount": 3 } + ] + }, + { + "name": "referral_gold", + "description": "25 people in your network created a database", + "priority": 30, + "requirements": [ + { "event_name": "invitee_created_db", "count": 25 } + ], + "rewards": [ + { "reward_type": "limit_credit", "target_name": "databases", "amount": 2 } + ] + } + ] +} +``` + +## Attenuation Design + +Multi-level referrals create natural attenuation without any per-depth tracking: + +- **Direct inviters** (depth 1) see events frequently — their invitees' actions directly generate events. They hit achievement thresholds quickly. +- **2nd-degree ancestors** see events less often — only when their invitees' invitees act. +- **5th-degree ancestors** accumulate events very slowly. + +The tiered achievement thresholds (3 → 10 → 25) create decreasing rewards at each tier. Combined with the natural event decay at deeper levels, this produces an MLM-style attenuation curve without any schema changes or depth-tracking infrastructure. + +### Performance + +Each depth level is one indexed lookup on `claimed_invites(receiver_id)`. With `max_depth=10`, that's at most 10 index scans per trigger fire — negligible overhead. + +## Composing with Invite Virality + +EventReferral composes with `has_invite_achievements`. They serve different purposes: + +- **`has_invite_achievements`**: Credits the inviter when an invite is *claimed* (`invite_claimed` event) and when an invitee *earns an achievement* (`invitee_achieved_*` events). Always single-level. +- **`EventReferral`**: Credits the inviter(s) when an invitee performs a *table action*. Supports multi-level via `max_depth`. + +Both can be used simultaneously. A common pattern is `has_invite_achievements` for the social/gamification loop and `EventReferral` with `max_depth > 1` for the MLM referral reward chain. diff --git a/.agents/skills/constructive-sdk-events/references/invite-virality.md b/.agents/skills/constructive-sdk-events/references/invite-virality.md index 94347d6..aa093c4 100644 --- a/.agents/skills/constructive-sdk-events/references/invite-virality.md +++ b/.agents/skills/constructive-sdk-events/references/invite-virality.md @@ -141,6 +141,21 @@ User A invites User B → A invites more people (viral loop) ``` +## Multi-Level Referral Chains (EventReferral + max_depth) + +For MLM-style referral rewards that go beyond direct inviters, use `EventReferral` with `max_depth > 1` on table nodes. This walks the `claimed_invites` chain up to N levels: + +``` +User A invites B, B invites C, C invites D + +D creates a database (EventReferral with max_depth=5): + → C gets 'invitee_created_db' event (depth 1) + → B gets 'invitee_created_db' event (depth 2) + → A gets 'invitee_created_db' event (depth 3) +``` + +This is separate from `has_invite_achievements` (which only tracks invite claims and achievement completions). See [event-referral.md](event-referral.md) for the full reference, blueprint examples, and attenuation design. + ## Entity-Scoped Invites Each entity type gets its own independent invite achievement tracking. If both `app` and `org` have `has_invite_achievements: true`: