# NEO-109 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-109 | | **Title** | E5M3-10: Slice 3 encounter telemetry hook sites | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-109/e5m3-10-slice-3-encounter-telemetry-hook-sites | | **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-10** | | **Branch** | `NEO-109-slice-3-encounter-telemetry-hook-sites` | | **Blocked by** | [NEO-106](https://linear.app/neon-sprawl/issue/NEO-106) — combat wiring (**Done** on `main`); [NEO-107](https://linear.app/neon-sprawl/issue/NEO-107) — event record (**Done** on `main`) | | **Pattern** | [NEO-49](NEO-49-implementation-plan.md) / [NEO-64](NEO-64-implementation-plan.md) — comment-only engine anchors; [NEO-84](NEO-84-implementation-plan.md) — reserved `encounter_start` in Slice 1 combat | | **Blocks** | None — client capstone [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) do not depend on telemetry | | **Client counterpart** | None — server-only comments; player-visible HUD is [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) | ## Kickoff clarifications | Topic | Question | Agent recommendation | Answer | |--------|----------|----------------------|--------| | **`reward_attribution` shape** | Once per completion vs per grant vs skip? | **Once per successful completion** — batch payload after all grants + completion mark commit; `item_created` remains at inventory layer (**NEO-56**). | **Adopted** — once per completion | | **`encounter_complete` anchor** | Single engine anchor vs dual wiring + ops? | **`EncounterCompletionOperations.TryCompleteAndGrant` only** — remove duplicate in **`EncounterCombatWiring`** (**NEO-49** / **NEO-64** precedent). | **Adopted** — completion ops only | | **Manual QA doc** | `docs/manual-qa/NEO-109.md`? | **Yes** (NEO-64 pattern). | **Adopted** — **skip** (NEO-108 server-only precedent per user) | | **`encounter_complete_denied`** | Keep deny hook on `Deny`? | **Keep** — already landed from NEO-105 review; **NEO-56** deny precedent; not in epic vocabulary but comments-only. | **Adopted** (implicit — no change) | | **Runtime behavior** | Comments-only vs dev `ILogger`? | **Comments-only** — `TODO(E9.M1)`; no production logging (**NEO-49** precedent). | **Adopted** (repo precedent; not asked) | ## Goal, scope, and out-of-scope **Goal:** Document and place **comment-only hook sites** on the server-authoritative encounter funnel for Epic 5 Slice 3 product telemetry **`encounter_start`**, **`encounter_complete`**, and **`reward_attribution`**, plus consolidate anchors so E9.M1 wiring has one documented surface per event. **In scope (from Linear + [E5M3-10](E5M3-prototype-backlog.md#e5m3-10--slice-3-encounter-telemetry-hook-sites)):** - **`TODO(E9.M1)`** markers at encounter start, completion commit, and **once** reward attribution on successful grant batch. - **`server/README.md`** encounter telemetry subsection; epic Slice 3 vocabulary cross-link. - Normalize partial NEO-109 stubs from NEO-104/105/106 into full **NEO-64**-style comment blocks (event name, payload fields, no ingest). - Update **NEO-84** reserved **`encounter_start`** pointer in **`CombatOperations`** to E5.M3 anchor (comments only). - E5M3 backlog + alignment register + module snapshot when implementation completes. **Out of scope (from Linear + backlog):** - Production ingest, dashboards (**E9.M1**). - `ILogger` / metrics / dev-only log lines. - Godot client, Bruno changes, API/wire contract changes. - `docs/manual-qa/NEO-109.md` (kickoff: skip). - Duplicate hook comments in **`AbilityCastApi`** or **`EncounterCombatWiring`** for **`encounter_complete`** / **`reward_attribution`**. ## Acceptance criteria checklist - [x] Hook names match [epic_05 Slice 3](../decomposition/epics/epic_05_pve_combat.md#epic-5-slice-3) vocabulary (`encounter_complete`, reward attribution / **`reward_attribution`**). - [x] No runtime behavior change beyond comments. ## Implementation reconciliation (shipped) - **`encounter_start`:** full comment block in **`EncounterProgressOperations.TryActivateOnFirstEngagement`** after successful **`TryActivate`** (anchor matches once-per-activation emit); NEO-84 **`CombatOperations`** reserved pointer updated. - **`encounter_complete`** + **`reward_attribution`:** full comment blocks in **`EncounterCompletionOperations.TryCompleteAndGrant`** after event record; duplicate removed from **`EncounterCombatWiring`** (delegate pointer only). - **`encounter_complete_denied`:** retained on **`Deny`**. - **Docs:** `server/README.md` encounter telemetry subsection; E5.M3 module snapshot; alignment register; module dependency register; E5M3-10 backlog landed note. ## Technical approach ### Current state (on `main`) Partial **NEO-109** tags already exist from precursor stories: | Event | File | Status | |-------|------|--------| | **`encounter_start`** | `EncounterProgressOperations.TryActivateOnFirstEngagement` | Stub present — expand to full block | | **`encounter_complete`** | `EncounterCompletionOperations` success path | Thin `TODO` — expand; **remove** `EncounterCombatWiring` duplicate | | **`encounter_complete_denied`** | `EncounterCompletionOperations.Deny` | Block present — keep | | **`reward_attribution`** | — | **Missing** — add on completion success path | ### Hook sites (after implementation) 1. **`encounter_start`** — **`EncounterProgressOperations.TryActivateOnFirstEngagement`**, immediately before **`progressStore.TryActivate`** when activation will proceed (not when already started/completed/no encounter): - Comment block: future E9.M1 event **`encounter_start`**. - **`TODO(E9.M1): catalog emit`** — once per player+encounter on first activation. - Payload: `playerId`, `encounterId`, `npcInstanceId` (engagement target), optional `requiredNpcInstanceIds` count. - **NEO-84:** update class-level reserved comment in **`CombatOperations`** to point here (Slice 3 landed). 2. **`encounter_complete`** — **`EncounterCompletionOperations.TryCompleteAndGrant`**, after **`TryMarkCompleted`** + **`TryRecord`** succeed (authoritative commit): - Replace thin TODO with full block naming **`encounter_complete`**. - Payload: `playerId`, `encounterId`, `rewardTableId`, `completedAt`, `idempotencyKey`, `grantedItems` summary from **`EncounterCompleteEvent`**. - **Remove** duplicate hook block at end of **`EncounterCombatWiring.TryProcessCastOutcome`** (wiring only calls completion ops). 3. **`reward_attribution`** — same success path, **once per completion** (kickoff decision): - Comment block naming **`reward_attribution`** (snake_case catalog name; epic prose “reward attribution”). - **`TODO(E9.M1): catalog emit`** — once when reward table grants commit for encounter completion. - Payload: `playerId`, `encounterId`, `rewardTableId`, `grantedItems[]` (batch from **`EncounterGrantApplied`** / event store snapshot). - Note in comment: inventory layer still emits **`item_created`** per stack via **`PlayerInventoryOperations`** (**NEO-56**); **`reward_attribution`** is encounter-scoped attribution for E7.M2 / loot analytics. 4. **`encounter_complete_denied`** — keep existing **`Deny`** block ( **`reasonCode`** from **`EncounterCompletionReasonCodes`** ). 5. **Cross-references:** - **`EncounterCompletionResult`** / **`EncounterCompleteEvent`** xml summaries → telemetry anchors in **`EncounterCompletionOperations`**. - **`EncounterProgressOperations`** class summary → **`encounter_start`** anchor. - **`server/README.md`:** new **Encounter telemetry hooks (NEO-109)** subsection (after NEO-108 or NEO-106 block) listing all three epic events + deny + **`CombatOperations`** reserved pointer. - **`E5_M3_EncounterAndRewardTables.md`**, **`documentation_and_implementation_alignment.md`**, **`module_dependency_register.md`**, **`E5M3-prototype-backlog.md`:** E5M3-10 landed note when complete. ### NEO-56 / NEO-64 analogy | Layer | Inventory / gather | Encounter Slice 3 | |-------|-------------------|-------------------| | Low-level item mutation | **`item_created`** (**NEO-56**) | Same on each **`TryAddStack`** during grants | | Orchestration / funnel | **`resource_gathered`** (**NEO-64**) | **`encounter_complete`** + **`reward_attribution`** (**NEO-109**) at completion commit | | Engagement | — | **`encounter_start`** at first activation | ## Files to add None. ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs` | Expand **`encounter_complete`** block; add **`reward_attribution`** once on success; keep **`encounter_complete_denied`** on **`Deny`**. | | `server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs` | Remove duplicate **`encounter_complete`** hook; one-line pointer to **`EncounterCompletionOperations`**. | | `server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs` | Expand **`encounter_start`** block to NEO-64 style; class summary cross-ref. | | `server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs` | Xml summary → telemetry hook anchor (no behavior change). | | `server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs` | Xml summary → completion + attribution hook anchor (no behavior change). | | `server/NeonSprawl.Server/Game/Combat/CombatOperations.cs` | Update reserved **`encounter_start`** comment → E5.M3 **`EncounterProgressOperations`** anchor. | | `server/README.md` | **NEO-109** encounter telemetry hooks subsection; fix NEO-104 vs NEO-109 label on **`encounter_start`** in NEO-106 table if needed. | | `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | NEO-109 telemetry hooks landed line. | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 row: NEO-109 hook sites. | | `docs/decomposition/modules/module_dependency_register.md` | E5.M3 note — E5M3-10 complete when landed. | | `docs/plans/E5M3-prototype-backlog.md` | E5M3-10 acceptance checkboxes + landed note when complete. | ## Tests | Test file | What it covers | |-----------|----------------| | *(none added or changed)* | Comments-only; no behavior or wire contract change. Regression: `dotnet test NeonSprawl.sln` (existing **`EncounterCompletionOperationsTests`**, **`EncounterProgressOperationsTests`**, cast integration tests unchanged). | **Manual verification (no checklist doc):** grep `NEO-109` / `encounter_complete` / `reward_attribution` / `encounter_start` under `server/NeonSprawl.Server/Game/Encounters/`; confirm **`EncounterCombatWiring`** has no **`encounter_complete`** emit comment; confirm no new **`ILogger`** or ingest calls. ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | **Overlap with NEO-56 `item_created`** | Keep both — **`item_created`** per stack; **`reward_attribution`** once per encounter completion for attribution context. | **adopted** (kickoff) | | **`encounter_complete_denied` not in epic list** | Keep deny hook — **NEO-56** precedent; zero runtime cost. | **adopted** | | **Precursor partial stubs** | Treat as draft; NEO-109 normalizes style and removes wiring duplicate. | **adopted** | None blocking.