NEO-130: Add comment-only reward telemetry hook sites.

Anchor future E9.M1 reward_delivery and unlock_granted events in
RewardRouterOperations; document hooks in server README and module docs.
pull/169/head
VinPropane 2026-06-07 22:46:27 -04:00
parent 7493a6b078
commit c461a822f1
8 changed files with 74 additions and 3 deletions

View File

@ -75,3 +75,5 @@ Epic 7 **Slice 2** — `reward_delivery`, `unlock_granted`; no double-claim on r
**NEO-128 (E7M2-05 quest-state wiring):** **`QuestStateOperations.TryMarkComplete`** calls **`RewardRouterOperations.TryDeliverQuestCompletion`** on the first-time completion path (deliver-then-mark); idempotent **`completed`** replay skips the router. **`QuestObjectiveWiring`** threads reward dependencies through gather/craft/encounter/GET refresh into the same path. See [NEO-128 implementation plan](../../plans/NEO-128-implementation-plan.md); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117).
**NEO-129 (E7M2-06 HTTP read):** **`GET /game/players/{id}/quest-progress`** projects optional **`completionRewardSummary`** on **`completed`** rows from **`IRewardDeliveryStore.TryGet`** — nested **`itemGrants`** + **`skillXpGrants`** mirroring delivery event snapshots; omitted when no delivery record. **`QuestAcceptApi`** embed rows use the same mapper. See [NEO-129 implementation plan](../../plans/NEO-129-implementation-plan.md); [server README — Per-player quest progress (NEO-119)](../../../server/README.md#per-player-quest-progress-neo-119); Bruno `bruno/neon-sprawl-server/quest-progress/Get quest progress after gather intro complete.bru`.
**NEO-130 (E7M2-07 telemetry hooks):** comment-only **`reward_delivery`** + **`unlock_granted`** stub hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`** — first-time **`TryRecord`** success and future **`UnlockGrant`** apply region respectively; no production ingest. See [NEO-130 implementation plan](../../plans/NEO-130-implementation-plan.md); [server README — Reward telemetry hooks (NEO-130)](../../../server/README.md#reward-telemetry-hooks-neo-130).

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
# Manual QA — NEO-130 (reward telemetry hook sites)
Reference: [implementation plan](../plans/NEO-130-implementation-plan.md), [NEO-127 implementation plan](../plans/NEO-127-implementation-plan.md) (reward router), [NEO-128 implementation plan](../plans/NEO-128-implementation-plan.md) (quest-state wiring).
## Preconditions
- Run `NeonSprawl.Server` (`dotnet run` from `server/NeonSprawl.Server`; default `http://localhost:5253`) if exercising Bruno; not required for code review only.
- This story adds **comments only**; reward delivery behavior matches NEO-127/128.
## Code review check
- [ ] Open `server/NeonSprawl.Server/Game/Rewards/RewardRouterOperations.cs`.
- [ ] Confirm **`unlock_granted`** stub comment block after item + skill XP apply loops, before **`RewardDeliveryEvent`** construction.
- [ ] Confirm **`reward_delivery`** comment block immediately after **`deliveryStore.TryRecord`** returns **`true`** (inside the success branch).
- [ ] Confirm **`TODO(E9.M1)`** markers and planned payload fields; **no** new `ILogger` or metrics calls.
- [ ] Confirm **no** duplicate hook comments in **`QuestStateOperations`**, **`QuestAcceptApi`**, or **`QuestProgressApi`**.
- [ ] Open `server/NeonSprawl.Server/Game/Rewards/RewardDeliveryEvent.cs` — xml summary cross-references **`reward_delivery`** hook site.
- [ ] Open [server README — Reward telemetry hooks (NEO-130)](../../server/README.md#reward-telemetry-hooks-neo-130) — NEO-130 subsection present with event table.
## Regression (optional)
```bash
cd server && dotnet test NeonSprawl.sln
```
Expect all tests pass (comments-only change).
## Bruno (optional sanity)
From `bruno/neon-sprawl-server/quest-progress/` against a running server:
- **Get quest progress after gather intro complete****`completionRewardSummary`** unchanged when quest completes via wiring.
- **Health after quest complete reward wiring** — server health unchanged.

View File

@ -241,7 +241,9 @@ Combat intro bundle is **skill XP only** — encounter loot already granted **`c
**Acceptance criteria**
- [ ] Hook sites documented in README; no behavior change beyond comments.
- [x] Hook sites documented in README; no behavior change beyond comments.
**Landed ([NEO-130](https://linear.app/neon-sprawl/issue/NEO-130)):** comment-only **`reward_delivery`** + **`unlock_granted`** stub in **`RewardRouterOperations`**; [server README — Reward telemetry hooks (NEO-130)](../../server/README.md#reward-telemetry-hooks-neo-130); plan [NEO-130](../../plans/NEO-130-implementation-plan.md).
**Client counterpart:** none (infrastructure-only).

View File

@ -46,7 +46,15 @@
## Acceptance criteria checklist
- [ ] Hook sites documented in README; no behavior change beyond comments.
- [x] Hook sites documented in README; no behavior change beyond comments.
## Implementation reconciliation (shipped)
- **Engine anchor:** **`RewardRouterOperations.TryDeliverQuestCompletion`** — two comment-only hook sites with **`TODO(E9.M1)`** (no runtime emit).
- **`reward_delivery`:** after **`deliveryStore.TryRecord`** returns **`true`** on first-time delivery.
- **`unlock_granted`:** stub after item + skill XP apply loops, before delivery event build — future **`UnlockGrant`** apply region; prototype bundles have no unlock rows.
- **Docs:** `server/README.md` reward telemetry subsection; E7.M2 module + alignment register + E7M2 backlog updated.
- **Manual QA:** `docs/manual-qa/NEO-130.md`.
## Technical approach

View File

@ -3,6 +3,7 @@ namespace NeonSprawl.Server.Game.Rewards;
/// <summary>
/// Idempotent quest completion reward delivery record (NEO-126).
/// Persisted via <see cref="IRewardDeliveryStore"/> after router apply succeeds (NEO-127).
/// E9.M1 <c>reward_delivery</c> hook site: <see cref="RewardRouterOperations"/> <c>TryDeliverQuestCompletion</c> (NEO-130).
/// </summary>
public readonly record struct RewardDeliveryEvent(
string PlayerId,

View File

@ -9,6 +9,7 @@ namespace NeonSprawl.Server.Game.Rewards;
/// <summary>
/// Applies quest <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127).
/// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128).
/// E9.M1 telemetry hook sites: <see cref="TryDeliverQuestCompletion"/> (NEO-130).
/// </summary>
public static class RewardRouterOperations
{
@ -134,6 +135,12 @@ public static class RewardRouterOperations
perksUnlockedByThisCall);
}
// --- Telemetry hook site (NEO-130): future E9.M1 catalog event `unlock_granted` (stub) ---
// TODO(E9.M1): catalog emit — once per applied UnlockGrant row when bundle unlock grants land (pre-production).
// Prototype completionRewardBundle has item + skill XP rows only; no runtime unlock apply in Slice 2.
// Planned payload fields: playerId, questId, unlock id / kind — finalize at E9.M1 wiring time.
// Distinct from `perk_unlock` (PerkUnlockEngine, NEO-49) which may fire as a side effect of skill XP grants.
var deliveredAt = timeProvider.GetUtcNow();
var deliveryEvent = new RewardDeliveryEvent(
normalizedPlayerId,
@ -145,6 +152,13 @@ public static class RewardRouterOperations
if (deliveryStore.TryRecord(deliveryEvent))
{
// --- Telemetry hook site (NEO-130): future E9.M1 catalog event `reward_delivery` ---
// TODO(E9.M1): catalog emit — once per first-time quest completion delivery (TryRecord success).
// Not on TryGet idempotent replay, deny paths, or TryRecord race-loser rollback paths.
// Planned payload fields: playerId, questId, deliveredAt, grantedItems (itemId, quantity),
// grantedSkillXp (skillId, amount), idempotencyKey ({playerId}:quest_complete:{questId}).
// No ingest or ILogger here (comments-only).
return SuccessFromEvent(deliveryEvent, reasonCode: null);
}

View File

@ -306,6 +306,17 @@ Encounter loot remains on **`IEncounterCompleteEventStore`** (E5.M3); quest comp
Apply order: **items first**, then **skill XP**. Inventory deny or skill deny fails closed without store record (compensating item rollback on partial apply or skill failure after items). Idempotent replay checks **`TryGet`** before apply. If grants succeed but **`TryRecord`** returns `false` (concurrent race), this call **rolls back** its item grants and calls **`SkillProgressionGrantOperations.TryRevertAppliedSkillGrants`** — negative XP delta per applied skill row plus removal of perks unlocked during this call only — then **`ReevaluateAfterLevelUp`** at post-rollback levels so a concurrent winner's path-auto unlocks are preserved — then returns the existing store event (mirror **`EncounterCompletionOperations`** compensating rollback when **`TryMarkCompleted`** loses). **Quest-state wiring (NEO-128):** **`QuestStateOperations.TryMarkComplete`** and **`QuestObjectiveWiring`** terminal-step completion call this operation before **`IPlayerQuestStateStore.TryMarkComplete`**. Plans: [NEO-127](../../docs/plans/NEO-127-implementation-plan.md), [NEO-128](../../docs/plans/NEO-128-implementation-plan.md).
### Reward telemetry hooks (NEO-130)
Comment-only hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`** for future E9.M1 catalog events ([Epic 7 Slice 2](../../docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md#related-implementation-slices)). **`TODO(E9.M1)`** — no production ingest. **`QuestStateOperations`** and HTTP quest routes delegate to the router only (no duplicate API-layer or quest-state hooks for these names). **`quest_complete`** remains in **`QuestStateOperations`** ([NEO-121](#quest-telemetry-hooks-neo-121)); **`reward_delivery`** fires earlier on the deliver-then-mark path (NEO-128).
| Event | Anchor | When |
|-------|--------|------|
| **`reward_delivery`** | **`TryDeliverQuestCompletion`** | After **`TryRecord`** returns **`true`** on first-time delivery (not idempotent **`TryGet`** replay or race-loser paths). |
| **`unlock_granted`** | **`TryDeliverQuestCompletion`** (stub) | Future **`UnlockGrant`** apply from bundle rows — prototype bundles have item + skill XP only; no runtime unlock apply in Slice 2. |
**`perk_unlock`** side effects from skill XP grants use **`PerkUnlockEngine`** ([NEO-49](#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49)) — distinct from content **`UnlockGrant`** rows. Manual QA: [`docs/manual-qa/NEO-130.md`](../../docs/manual-qa/NEO-130.md); plan: [NEO-130 implementation plan](../../docs/plans/NEO-130-implementation-plan.md).
## Encounter definitions (NEO-103)
**`GET /game/world/encounter-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`encounters`**) backed by **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, nested **`completionCriteria`** (`kind`), **`requiredNpcInstanceIds`**, and nested **`rewardTable`** (`id`, `displayName`, **`fixedGrants`** with `itemId` + `quantity`). Plan: [NEO-103 implementation plan](../../docs/plans/NEO-103-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/encounter-definitions/`.