# NEO-56 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-56 |
| **Title** | E3.M3: item_created / transfer-failure telemetry hook sites |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-56/e3m3-item-created-transfer-failure-telemetry-hook-sites |
| **Module** | [E3.M3 — ItemizationAndInventorySchema](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · Epic 3 Slice 1 (E3M3-07) |
| **Branch** | `NEO-56-item-created-transfer-failure-telemetry-hooks` |
| **Precursors** | [NEO-54](https://linear.app/neon-sprawl/issue/NEO-54) — `PlayerInventoryOperations` (**Done**); [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55) — inventory GET/POST HTTP (**Done**) |
| **Pattern** | [NEO-49](https://linear.app/neon-sprawl/issue/NEO-49) / [NEO-40](https://linear.app/neon-sprawl/issue/NEO-40) — comment-only engine anchors; [NEO-30](https://linear.app/neon-sprawl/issue/NEO-30) — deny event + `reasonCode` |

## Kickoff clarifications

| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Hook anchor** | Engine only vs engine + HTTP? | **Engine only** — `PlayerInventoryOperations` (NEO-49 single-anchor precedent; NEO-55 deferred hooks here). | **User:** engine only. |
| **Deny event shape** | Per-reason events vs single deny + `reasonCode`? | **`inventory_transfer_denied` + `reasonCode`** (NEO-30 `ability_cast_denied`; epic `inventory_full` maps to a reason code). | **User:** single deny + `reasonCode`. |
| **`item_created` scope** | Add success only vs add + remove? | **Successful add only** (`TryAddStack` → `Applied`). | **User:** add success only. |
| **Runtime behavior** | Comments-only vs dev `ILogger`? | **Comments-only** — `TODO(E9.M1)`; no production logging. | **User:** comments-only. |
| **Manual QA doc** | `docs/manual-qa/NEO-56.md`? | **Yes** — NEO-49 verification pattern. | **User:** yes. |

## Goal, scope, and out-of-scope

**Goal:** Document and place **comment-only hook sites** on the server-authoritative inventory mutation path for future E9.M1 catalog events **`item_created`** (successful add) and **`inventory_transfer_denied`** (transfer failures with stable `reasonCode`). Align with Epic 3 Slice 1 telemetry vocabulary ([epic_03 — Slice 1](../decomposition/epics/epic_03_crafting_economy.md)).

**In scope (from Linear + [E3M3-07](E3M3-prototype-backlog.md#e3m3-07--item_created--transfer-failure-telemetry-hook-sites)):**

- Hook placement in **`PlayerInventoryOperations`** (`TryAddStack` / `TryRemoveStack`) with **`TODO(E9.M1)`** comments.
- **`server/README.md`** inventory subsection + [E3_M3 module](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) hook note.
- **`docs/manual-qa/NEO-56.md`** — verify comments + no behavior/API change.

**Out of scope (from Linear):**

- Telemetry ingest, dashboards (**E9.M1**).
- `ILogger` / metrics / dev-only log lines.
- Duplicate hook comments in **`PlayerInventoryApi`** (HTTP calls engine; NEO-55 plan deferred hooks to NEO-56).
- **`item_created`** on successful **remove** (kickoff decision).
- **`StoreMissing`** hooks (not a transfer failure — HTTP maps to **404**).

## Acceptance criteria checklist

- [x] Hook sites documented in code (`PlayerInventoryOperations`) with **`TODO(E9.M1)`**.
- [x] **`item_created`** on successful add; **`inventory_transfer_denied`** + `reasonCode` on rule denies (including early validation denies).
- [x] `server/README.md` documents hook placement.
- [x] Matches Epic 3 Slice 1 vocabulary (`item_created`, `inventory_full` / transfer failures via `reasonCode`).

## Technical approach

1. **Authoritative surface:** `PlayerInventoryOperations` — all inventory add/remove rules and outcomes. Callers (`PlayerInventoryApi` POST, future gather/craft) funnel here; **no** duplicate markers at HTTP.

2. **Hook site A — `item_created`:** In **`TryAddStack`**, immediately before returning **`PlayerInventoryMutationKind.Applied`** (after successful `TryMutateSnapshot` with no `denyReason`):
   - Comment block names future E9.M1 event **`item_created`**.
   - **`TODO(E9.M1): catalog emit`** (NEO-49 / NEO-30 style).
   - Planned payload fields: `playerId`, `itemId`, `quantity`, optional `mutationKind`/`source` when Slice 2+ callers exist.
   - **Not** on **`TryRemoveStack`** success (kickoff).

3. **Hook site B — `inventory_transfer_denied`:** On every **`Denied`** return path in **`TryAddStack`** and **`TryRemoveStack`**:
   - Early **`Deny(...)`** (e.g. `invalid_quantity`, `invalid_item` before store write).
   - Post-mutation deny when `denyReason` is set (`inventory_full`, `insufficient_quantity`).
   - Single comment pattern: catalog event **`inventory_transfer_denied`** + **`reasonCode`** from [`PlayerInventoryReasonCodes`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryReasonCodes.cs) (`inventory_full`, `invalid_item`, `insufficient_quantity`, `invalid_quantity`).
   - Epic Slice 1 **`inventory_full`** is the flagship transfer-failure code; others share the same deny event shape (NEO-30 precedent).

4. **No hook on `StoreMissing`:** Outcome is missing player/store — not an inventory transfer rule deny.

5. **Cross-references (minimal):**
   - One-line pointer in `PlayerInventoryMutationOutcome.cs` or `PlayerInventoryReasonCodes.cs` xml summary → hook sites in `PlayerInventoryOperations`.
   - **`server/README.md`:** extend **Player inventory (NEO-54 store, NEO-55 HTTP)** with **NEO-56 telemetry hooks** subsection (anchor file + event names).
   - **`E3_M3_ItemizationAndInventorySchema.md`:** **NEO-56 landed** line under Related implementation slices.
   - **`documentation_and_implementation_alignment.md`:** E3.M3 row cites NEO-56 hook sites (still no E9.M1 ingest).

6. **Regression:** Run `dotnet test` on server solution; existing `PlayerInventoryOperationsTests` / `PlayerInventoryApiTests` unchanged (comments-only).

## Files to add

| Path | Purpose |
|------|---------|
| `docs/manual-qa/NEO-56.md` | Manual QA: verify hook comment blocks + no API/behavior change. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs` | **NEO-56:** `item_created` on add `Applied`; `inventory_transfer_denied` on all `Denied` paths in add/remove. |
| `server/NeonSprawl.Server/Game/Items/PlayerInventoryReasonCodes.cs` | Cross-reference deny `reasonCode` values → transfer-denied hook anchor (no behavior change). |
| `server/README.md` | Document inventory telemetry hook placement (NEO-56). |
| `docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md` | Implementation snapshot: NEO-56 telemetry hooks. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M3 row: NEO-56 hook sites. |

## Tests

| Path | Change |
|------|--------|
| **None required** | Comments-only; existing `PlayerInventoryOperationsTests` and `PlayerInventoryApiTests` cover add/remove/deny paths. |

**Manual verification:** `docs/manual-qa/NEO-56.md` — confirm **`item_created`** and **`inventory_transfer_denied`** comments, **`TODO(E9.M1)`**, payload field notes, no new log statements; `dotnet test` regression.

## Open questions / risks

| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Future gather/craft callers** | Hooks stay in engine; callers pass `source`/`mutationKind` in E9.M1 payload when catalog exists — document in hook comments as optional fields. | `adopted` |
| **HTTP request correlation** | Defer `requestId` to E9.M1 / future sink; do not add API-layer hooks (kickoff: engine only). | `adopted` |
| **Partial stack placement on add** | NEO-54 all-or-nothing add — single `item_created` emit for full requested quantity on `Applied`. | `adopted` |

None blocking.

## Decisions (post-review)

| Topic | Decision |
|--------|----------|
| **PR scope** | Branch includes chore `3d43b00` (kickoff `AskQuestion` + recommendation rule hardening). Intentional tooling hygiene on the story branch; call out in PR description if reviewers want story-only diff vs `main`. |
| **Post-mutation deny hooks** | Expanded to multi-line NEO-49-style blocks with payload fields (`mutationKind=add` / `remove`) per code review. |
| **Dependency register** | E3.M3 footnote extended with NEO-56 to match alignment table. |
