# NEO-141 — E7M3-09: Faction telemetry hook sites (comment-only) **Linear:** [NEO-141](https://linear.app/neon-sprawl/issue/NEO-141) **Branch:** `NEO-141-faction-telemetry-hooks` **Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-09** **Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Pattern:** [NEO-121](NEO-121-implementation-plan.md) / [NEO-130](NEO-130-implementation-plan.md) — comment-only engine anchors + `TODO(E9.M1)`; engine-only (no duplicate API-layer hooks) **Precursors:** [NEO-136](https://linear.app/neon-sprawl/issue/NEO-136) **`Done`** — `ReputationOperations.TryApplyDelta` + partial `reputation_delta` hook comment; [NEO-137](https://linear.app/neon-sprawl/issue/NEO-137) **`Done`** — `FactionGateOperations.TryEvaluate` + `faction_gate_blocked` hook comment; [NEO-138](https://linear.app/neon-sprawl/issue/NEO-138) **`Done`** — router rep apply + duplicate router stub comment (to consolidate here) **Blocks:** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143) (E7M3-11 client capstone — module telemetry vocabulary complete) **Client counterpart:** none (infrastructure-only per [E7M3-09](E7M3-pre-production-backlog.md#e7m3-09--faction-telemetry-hook-sites-comment-only)) ## Goal Anchor future E9.M1 catalog events **`reputation_delta`** and **`faction_gate_blocked`** on the server-authoritative faction path without ingest or behavior change. ## Kickoff clarifications **No clarifications needed.** Linear AC, [E7M3-09 backlog scope](E7M3-pre-production-backlog.md#e7m3-09--faction-telemetry-hook-sites-comment-only), and NEO-121/130 comment-only telemetry precedents settle scope. Partial hook comments landed in NEO-136/137/138; this story **consolidates and documents** them. | Topic | Decision | Evidence | |-------|----------|----------| | **Runtime behavior** | Comments-only + `TODO(E9.M1)` — no ingest, no `ILogger` | NEO-121/130 precedent | | **`reputation_delta` anchor** | **`ReputationOperations.TryApplyDelta`** only — after successful audit append | Backlog E7M3-09; NEO-121 engine-only precedent | | **Router duplicate stub** | **Remove** `reputation_delta` comment from **`RewardRouterOperations`**; cross-link in README | NEO-130 reward table already notes “consolidated in NEO-141”; single authoritative ops layer | | **`faction_gate_blocked` anchor** | **`FactionGateOperations.TryEvaluate`** deny paths only — not duplicate in **`QuestStateOperations.TryAccept`** or **`QuestAcceptApi`** | Backlog lists ops + TryAccept flow; NEO-137 placed hook at eval engine; HTTP maps all gate failures to `faction_gate_blocked` | | **Standing read failure deny** | **Same event name** with ops **`reasonCode`** in planned payload (`unknown_faction` vs `gate_blocked`) — add comment block on read-failure branch | NEO-137 review: ops may carry `unknown_faction` for telemetry while quest HTTP surfaces `faction_gate_blocked` | | **Emit timing for `reputation_delta`** | After **`auditStore.TryAppend`** returns **`true`** (standing + audit committed) — not on deny, audit rollback, or compensating revert paths | NEO-136 success path; mirror NEO-49 idempotent “commit site only” | | **Manual QA doc** | **None** — comment-only server story; `dotnet test` covers regression ([NEO-140](NEO-140-implementation-plan.md) precedent; [planning-implementation-docs](../.cursor/rules/planning-implementation-docs.md) skip when AC fully automated) | Infrastructure-only; no user-visible behavior | ## Scope and out-of-scope **In scope (from Linear + backlog):** - Complete comment-only **`reputation_delta`** hook in **`ReputationOperations`** (successful apply path — add `TODO(E9.M1)` + planned payload fields). - Confirm / extend comment-only **`faction_gate_blocked`** in **`FactionGateOperations`** (threshold deny + standing read failure deny). - Remove duplicate **`reputation_delta`** stub from **`RewardRouterOperations`**. - **`server/README.md`** — dedicated **Faction telemetry hooks (NEO-141)** subsection; update **Reward telemetry hooks (NEO-130)** table to cross-link (remove stub row). - **`E7_M3_FactionReputationLedger.md`**, alignment register, E7M3 backlog updates when implementation completes. **Out of scope:** - E9.M1 ingest pipeline; runtime telemetry emit. - `ILogger` / metrics / dev-only log lines. - Duplicate hooks in **`QuestAcceptApi`**, **`QuestProgressApi`**, **`FactionStandingApi`**, or HTTP routes. - Godot client (NEO-142/143). **Client counterpart:** none. ## Acceptance criteria checklist - [x] Hook sites documented in README; no behavior change beyond comments. ## Implementation reconciliation (shipped) - **`ReputationOperations.TryApplyDelta`:** complete **`reputation_delta`** comment block after audit append success (`TODO(E9.M1)` + planned payload fields). - **`FactionGateOperations.TryEvaluate`:** **`faction_gate_blocked`** hooks on threshold deny and standing read-failure deny (ops `reasonCode` in payload note). - **`RewardRouterOperations`:** duplicate **`reputation_delta`** stub removed — single ops-layer anchor. - **Docs:** `server/README.md` **Faction telemetry hooks (NEO-141)** subsection; NEO-130 reward table cross-link; E7.M3 module + alignment register + E7M3 backlog updated. - **Manual QA:** none — comment-only server story; `dotnet test` covers regression (manual QA checklists reserved for user-visible stories per project convention). ## Technical approach ### 1. Current state (precursor partial work) | Location | State today | NEO-141 action | |----------|-------------|----------------| | **`ReputationOperations.TryApplyDelta`** | One-line hook comment after audit append; **missing** `TODO(E9.M1)` + payload | **Complete** comment block | | **`FactionGateOperations.TryEvaluate`** | Full hook on threshold deny (`standing < minStanding`); read-failure deny **without** hook | **Add** hook on read-failure deny with ops `reasonCode` in payload note | | **`RewardRouterOperations`** | Duplicate `reputation_delta` stub after each rep row apply | **Remove** stub; router delegates to ops | | **`server/README.md`** | `reputation_delta` only as NEO-130 stub row | **Add** NEO-141 subsection; **update** NEO-130 table | ### 2. Hook site A — `reputation_delta` **Anchor:** **`ReputationOperations.TryApplyDelta`**, immediately after **`auditStore.TryAppend(row)`** returns **`true`**. - Comment block names future E9.M1 event **`reputation_delta`**. - **`TODO(E9.M1): catalog emit`** — once per successful auditable apply (standing mutation + audit row committed). - **Not** on: zero delta deny, standing store deny, audit append failure + compensating standing revert, or caller rollback paths. - Planned payload fields from **`ReputationDeltaRow`**: `playerId`, `factionId`, `appliedDelta`, `newStanding`, `sourceKind`, `sourceId`, `deltaId`, `appliedAt`. - Callers include **`RewardRouterOperations`** (quest completion), future admin tools — all funnel through this ops layer. ### 3. Hook site B — `faction_gate_blocked` **Anchor:** **`FactionGateOperations.TryEvaluate`** deny returns only. **Branch B1 — threshold fail** (`read.Standing < rule.MinStanding`): hook comment **already present** — verify `TODO(E9.M1)` + payload fields match NEO-121 style. **Branch B2 — standing read fail** (`!read.Success`): **add** parallel hook comment before deny return. - Same event name **`faction_gate_blocked`**; planned payload includes ops **`reasonCode`** (`unknown_faction`, etc.), `playerId`, `factionId`, `minStanding`, optional `currentStanding` (null when unknown). - **`QuestStateOperations.TryAccept`** maps all gate failures to HTTP **`faction_gate_blocked`** regardless of ops reason — E9.M1 can use ops payload for drill-down. - Optional quest context (`questId`) deferred to E9.M1 wiring at **`TryAccept`** call site if needed — not duplicated at quest layer in prototype. ### 4. README consolidation Add **`### Faction telemetry hooks (NEO-141)`** after [FactionGateOperations (NEO-137)](../../server/README.md#factiongateoperations-neo-137): | Event | Anchor | When | |-------|--------|------| | **`reputation_delta`** | **`ReputationOperations.TryApplyDelta`** | After successful audit append (auditable apply committed). | | **`faction_gate_blocked`** | **`FactionGateOperations.TryEvaluate`** | On any rule deny (threshold or standing read failure). | Update [Reward telemetry hooks (NEO-130)](../../server/README.md#reward-telemetry-hooks-neo-130): remove **`reputation_delta`** stub row; add one-line pointer to NEO-141 (quest rep grants emit via ops, not router duplicate). ### 5. Relationship to adjacent hooks | Event | Layer | Story | |-------|-------|-------| | **`quest_accept_denied`** | **`QuestStateOperations.TryAccept`** | NEO-121 (generic accept deny — includes gate denies at HTTP level) | | **`faction_gate_blocked`** | **`FactionGateOperations.TryEvaluate`** | NEO-141 (faction-specific gate eval detail) | | **`reward_delivery`** | **`RewardRouterOperations`** | NEO-130 | | **`reputation_delta`** | **`ReputationOperations.TryApplyDelta`** | NEO-141 | Gate deny on accept may correlate **`quest_accept_denied`** (if added for gate path) vs **`faction_gate_blocked`** — E9.M1 can sample both or dedupe by correlation id. ### 6. Flow (comment anchors only) ```mermaid sequenceDiagram participant Accept as QuestStateOperations.TryAccept participant Gate as FactionGateOperations participant Router as RewardRouterOperations participant Rep as ReputationOperations Accept->>Gate: TryEvaluate(rules) alt gate deny Gate-->>Accept: fail (hook: faction_gate_blocked) else gate pass Accept->>Accept: TryActivate (hook: quest_start NEO-121) end Note over Router,Rep: quest completion path Router->>Rep: TryApplyDelta per rep grant Rep-->>Router: success (hook: reputation_delta) Router->>Router: TryRecord delivery (hook: reward_delivery NEO-130) ``` ## Files to add None — comment-only changes to existing ops files and docs; no new source or test files. ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/Factions/ReputationOperations.cs` | Complete `reputation_delta` hook block (`TODO(E9.M1)` + planned payload fields) after audit append success | | `server/NeonSprawl.Server/Game/Factions/FactionGateOperations.cs` | Add `faction_gate_blocked` hook on standing read-failure deny; verify threshold branch comment completeness | | `server/NeonSprawl.Server/Game/Rewards/RewardRouterOperations.cs` | Remove duplicate `reputation_delta` stub comment (consolidate to ops layer) | | `server/README.md` | Add **Faction telemetry hooks (NEO-141)** subsection; update NEO-130 reward table cross-link | | `docs/decomposition/modules/E7_M3_FactionReputationLedger.md` | Status line: E7M3-09 NEO-141 telemetry hooks landed | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M3 row: E7M3-09 hook sites | | `docs/plans/E7M3-pre-production-backlog.md` | E7M3-09 checkboxes + landed note | ## Tests | File | Coverage | |------|----------| | *(none added or changed)* | Comments-only change; no behavior or wire contract change. Regression: `dotnet test NeonSprawl.sln` (existing faction/quest/reward tests unchanged). | ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | Duplicate `reputation_delta` if both router and ops had hooks | **Remove router stub** — single ops anchor | `adopted` | | Gate deny emits both `quest_accept_denied` and `faction_gate_blocked` | **Expected** — different granularity; E9.M1 correlates by time/player/quest | `adopted` | | `unknown_faction` on gate eval in prototype | Rare (catalog-validated rules); still document hook on read-failure deny for E9.M1 payload fidelity | `adopted` | | E7.M3 module Ready vs In Progress | Leave **In Progress** until client capstone NEO-143; only note E7M3-09 complete in register | `adopted` |