Merge pull request #181 from ViPro-Technologies/NEO-141-faction-telemetry-hooks
NEO-141: E7M3-09 faction telemetry hook sites (comment-only)pull/183/head
commit
e102a6b539
|
|
@ -7,7 +7,7 @@
|
|||
| **Module ID** | E7.M3 |
|
||||
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
||||
| **Stage target** | Pre-production |
|
||||
| **Status** | In Progress — **E7M3-01 catalog landed** ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)); **E7M3-02 server faction catalog load landed** ([NEO-134](https://linear.app/neon-sprawl/issue/NEO-134)): fail-fast `content/factions/*_factions.json`, `IFactionDefinitionRegistry`, quest faction cross-ref + E7M3 bundle/grid gates; **E7M3-03 faction standing + reputation delta stores landed** ([NEO-135](https://linear.app/neon-sprawl/issue/NEO-135)): `IFactionStandingStore`, `IReputationDeltaStore`, in-memory + Postgres `V009`/`V010`; **E7M3-04 `ReputationOperations` landed** ([NEO-136](https://linear.app/neon-sprawl/issue/NEO-136)): auditable `TryApplyDelta` orchestration ([NEO-136 plan](../../plans/NEO-136-implementation-plan.md)); **E7M3-05 `FactionGateOperations` landed** ([NEO-137](https://linear.app/neon-sprawl/issue/NEO-137)): quest accept gate eval + `faction_gate_blocked` ([NEO-137 plan](../../plans/NEO-137-implementation-plan.md)); **E7M3-06 reward router rep grants landed** ([NEO-138](https://linear.app/neon-sprawl/issue/NEO-138)): `RewardRouterOperations` applies `reputationGrants` idempotently via `ReputationOperations` ([NEO-138 plan](../../plans/NEO-138-implementation-plan.md)); **E7M3-07 faction standing HTTP read landed** ([NEO-139](https://linear.app/neon-sprawl/issue/NEO-139)): `GET …/faction-standing` snapshot API + Bruno ([NEO-139 plan](../../plans/NEO-139-implementation-plan.md)); **E7M3-08 quest HTTP projections landed** ([NEO-140](https://linear.app/neon-sprawl/issue/NEO-140)): world GET `factionGateRules` + quest-progress `completionRewardSummary.reputationGrants` ([NEO-140 plan](../../plans/NEO-140-implementation-plan.md)). Slice 3 backlog **E7M3-09** [NEO-141](https://linear.app/neon-sprawl/issue/NEO-141) → **E7M3-11** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143). Upstream: E7.M1 **Ready**, E7.M2 **Ready**. |
|
||||
| **Status** | In Progress — **E7M3-01 catalog landed** ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)); **E7M3-02 server faction catalog load landed** ([NEO-134](https://linear.app/neon-sprawl/issue/NEO-134)): fail-fast `content/factions/*_factions.json`, `IFactionDefinitionRegistry`, quest faction cross-ref + E7M3 bundle/grid gates; **E7M3-03 faction standing + reputation delta stores landed** ([NEO-135](https://linear.app/neon-sprawl/issue/NEO-135)): `IFactionStandingStore`, `IReputationDeltaStore`, in-memory + Postgres `V009`/`V010`; **E7M3-04 `ReputationOperations` landed** ([NEO-136](https://linear.app/neon-sprawl/issue/NEO-136)): auditable `TryApplyDelta` orchestration ([NEO-136 plan](../../plans/NEO-136-implementation-plan.md)); **E7M3-05 `FactionGateOperations` landed** ([NEO-137](https://linear.app/neon-sprawl/issue/NEO-137)): quest accept gate eval + `faction_gate_blocked` ([NEO-137 plan](../../plans/NEO-137-implementation-plan.md)); **E7M3-06 reward router rep grants landed** ([NEO-138](https://linear.app/neon-sprawl/issue/NEO-138)): `RewardRouterOperations` applies `reputationGrants` idempotently via `ReputationOperations` ([NEO-138 plan](../../plans/NEO-138-implementation-plan.md)); **E7M3-07 faction standing HTTP read landed** ([NEO-139](https://linear.app/neon-sprawl/issue/NEO-139)): `GET …/faction-standing` snapshot API + Bruno ([NEO-139 plan](../../plans/NEO-139-implementation-plan.md)); **E7M3-08 quest HTTP projections landed** ([NEO-140](https://linear.app/neon-sprawl/issue/NEO-140)): world GET `factionGateRules` + quest-progress `completionRewardSummary.reputationGrants` ([NEO-140 plan](../../plans/NEO-140-implementation-plan.md)); **E7M3-09 faction telemetry hooks landed** ([NEO-141](https://linear.app/neon-sprawl/issue/NEO-141)): comment-only **`reputation_delta`** + **`faction_gate_blocked`** in ops layers + README ([NEO-141 plan](../../plans/NEO-141-implementation-plan.md)). Slice 3 backlog **E7M3-10** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) → **E7M3-11** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143). Upstream: E7.M1 **Ready**, E7.M2 **Ready**. |
|
||||
| **Linear** | Label **`E7.M3`** · [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md) |
|
||||
|
||||
## Purpose
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,40 +0,0 @@
|
|||
# NEO-140 — Manual QA checklist
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Key | NEO-140 |
|
||||
| Title | E7M3-08: Extend quest HTTP projections for rep + gates |
|
||||
| Linear | https://linear.app/neon-sprawl/issue/NEO-140/e7m3-08-extend-quest-http-projections-for-rep-gates |
|
||||
| Plan | `docs/plans/NEO-140-implementation-plan.md` |
|
||||
| Branch | `NEO-140-e7m3-quest-http-rep-gate-projections` |
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Server running: `cd server/NeonSprawl.Server && dotnet run` (in-memory dev player `dev-local-1`).
|
||||
- Bruno collection `bruno/neon-sprawl-server` loaded with `baseUrl` = `http://localhost:5253`, `playerId` = `dev-local-1`.
|
||||
- NEO-138 reward router rep grants and NEO-139 faction-standing GET landed on `main`.
|
||||
|
||||
## Setup sanity
|
||||
|
||||
- [ ] `curl -sS http://localhost:5253/health` returns `"status":"ok"`.
|
||||
- [ ] `dotnet test NeonSprawl.sln` passes (821+ tests).
|
||||
|
||||
## Quest definitions — factionGateRules
|
||||
|
||||
- [ ] Run Bruno **`quest-definitions/Get quest definitions`**. Response **200**; `prototype_quest_grid_contract.factionGateRules` = `[{ factionId: prototype_faction_grid_operators, minStanding: 15 }]`.
|
||||
- [ ] Same response: `prototype_quest_gather_intro` and `prototype_quest_operator_chain` have **no** `factionGateRules` key.
|
||||
- [ ] `curl -sS http://localhost:5253/game/world/quest-definitions | jq '.quests[] | select(.id=="prototype_quest_grid_contract") | .factionGateRules'` matches the Bruno assertion.
|
||||
|
||||
## Quest progress — completionRewardSummary.reputationGrants
|
||||
|
||||
- [ ] Run Bruno **`quest-progress/Get quest progress after gather intro complete`**. Gather row `completionRewardSummary.reputationGrants` = `[]`.
|
||||
- [ ] Run Bruno **`quest-progress/Get quest progress after operator chain complete`**. Operator-chain row `completionRewardSummary.reputationGrants` = `[{ factionId: prototype_faction_grid_operators, amount: 15 }]`; item and skill XP lines unchanged from NEO-129 freeze.
|
||||
- [ ] Second GET in the operator-chain bru matches the first (idempotent summary).
|
||||
|
||||
## No regressions
|
||||
|
||||
- [ ] Bruno **`faction-standing/Get faction standing after operator chain`** still shows Grid Operators standing **15** after operator-chain flow.
|
||||
- [ ] Bruno **`quest-progress/Accept grid contract after operator chain`** accepts grid contract when rep gate met and embedded row **`completionRewardSummary.reputationGrants`** = `[{ factionId: prototype_faction_rust_collective, amount: 10 }]`.
|
||||
- [ ] NEO-129 gather-intro **`completionRewardSummary`** shape unchanged except new empty **`reputationGrants`** array.
|
||||
|
||||
**Client HUD verification** deferred to [NEO-142 (Godot HUD)](https://linear.app/neon-sprawl/issue/NEO-142/e7m3-10-client-faction-standing-gate-feedback-hud-godot) — this story is server HTTP projection only.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Manual QA — NEO-141 (faction telemetry hook sites)
|
||||
|
||||
Reference: [implementation plan](../plans/NEO-141-implementation-plan.md), [NEO-136 implementation plan](../plans/NEO-136-implementation-plan.md) (reputation ops), [NEO-137 implementation plan](../plans/NEO-137-implementation-plan.md) (gate ops).
|
||||
|
||||
## 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**; faction standing, gate eval, and reward delivery behavior match NEO-136/137/138.
|
||||
|
||||
## Code review check
|
||||
|
||||
- [ ] Open `server/NeonSprawl.Server/Game/Factions/ReputationOperations.cs`.
|
||||
- [ ] Confirm **`reputation_delta`** comment block immediately after **`auditStore.TryAppend`** returns **`true`**.
|
||||
- [ ] Confirm **`TODO(E9.M1)`** markers and planned payload fields; **no** new `ILogger` or metrics calls.
|
||||
- [ ] Open `server/NeonSprawl.Server/Game/Factions/FactionGateOperations.cs`.
|
||||
- [ ] Confirm **`faction_gate_blocked`** comment on standing read-failure deny and threshold deny branches.
|
||||
- [ ] Confirm **no** duplicate hook comments in **`QuestStateOperations`**, **`QuestAcceptApi`**, **`FactionStandingApi`**, or **`RewardRouterOperations`** for these event names.
|
||||
- [ ] Open [server README — Faction telemetry hooks (NEO-141)](../../server/README.md#faction-telemetry-hooks-neo-141) — NEO-141 subsection present with event table.
|
||||
- [ ] Open [server README — Reward telemetry hooks (NEO-130)](../../server/README.md#reward-telemetry-hooks-neo-130) — **`reputation_delta`** stub row removed; cross-link to NEO-141 present.
|
||||
|
||||
## Regression (optional)
|
||||
|
||||
```bash
|
||||
cd server && dotnet test NeonSprawl.sln
|
||||
```
|
||||
|
||||
Expect all tests pass (comments-only change).
|
||||
|
||||
## Bruno (optional sanity)
|
||||
|
||||
From `bruno/neon-sprawl-server/` against a running server:
|
||||
|
||||
- **Accept grid contract faction gate deny** — **`faction_gate_blocked`** deny unchanged.
|
||||
- **Accept grid contract after operator chain** — rep grant + accept success unchanged.
|
||||
|
|
@ -296,7 +296,7 @@ Accepting **`prototype_quest_grid_contract`** before operator-chain completion o
|
|||
**In scope**
|
||||
|
||||
- Comment-only **`reputation_delta`** in **`ReputationOperations`** (successful apply).
|
||||
- Comment-only **`faction_gate_blocked`** in **`FactionGateOperations`** / **`TryAccept`** deny path.
|
||||
- Comment-only **`faction_gate_blocked`** in **`FactionGateOperations.TryEvaluate`** (threshold + standing read failure deny paths).
|
||||
- `server/README.md` hook list.
|
||||
|
||||
**Out of scope**
|
||||
|
|
@ -305,7 +305,9 @@ Accepting **`prototype_quest_grid_contract`** before operator-chain completion o
|
|||
|
||||
**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-141):** comment-only **`reputation_delta`** in **`ReputationOperations.TryApplyDelta`**; **`faction_gate_blocked`** in **`FactionGateOperations.TryEvaluate`** (threshold + standing read failure); router duplicate stub removed; [server README — Faction telemetry hooks (NEO-141)](../../../server/README.md#faction-telemetry-hooks-neo-141); manual QA [`NEO-141.md`](../../manual-qa/NEO-141.md).
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ Client can see gate requirements and completion rep summary without local math.
|
|||
- **Quest progress GET:** `QuestProgressApi.MapCompletionRewardSummary` maps `RewardDeliveryEvent.GrantedReputation` → `reputationGrants` (`QuestReputationGrantJson`: `factionId`, `amount`); required array when summary present.
|
||||
- **Accept POST:** inherits updated mapper via `QuestProgressApi.MapQuestProgressRow` — no accept-specific changes.
|
||||
- **Tests:** `QuestDefinitionsWorldApiTests` grid-contract gate row + wire-level `factionGateRules` omission; `QuestProgressApiTests` operator-chain +15 rep, grid-contract +10 rep, gather-intro empty rep; **823** tests green.
|
||||
- **Bruno:** extended `Get quest definitions.bru`; new `Get quest progress after operator chain complete.bru`; gather-intro bru asserts empty `reputationGrants`.
|
||||
- **Docs:** `server/README.md` quest-definitions + quest-progress sections; E7.M3 module + alignment register updated.
|
||||
- **Manual QA:** [`docs/manual-qa/NEO-140.md`](../manual-qa/NEO-140.md).
|
||||
- **Manual QA:** none — server-only HTTP projections; Bruno + `dotnet test` cover verification (manual QA checklists reserved for client-facing stories per project convention).
|
||||
|
||||
## Technical approach
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
# 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** | **`docs/manual-qa/NEO-141.md`** — NEO-121/130 comment-only verification pattern | Infrastructure-only server story |
|
||||
|
||||
## 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.
|
||||
- **`docs/manual-qa/NEO-141.md`**.
|
||||
|
||||
**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:** `docs/manual-qa/NEO-141.md`.
|
||||
|
||||
## 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
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `docs/manual-qa/NEO-141.md` | Manual QA: verify comment blocks + no behavior/API change (NEO-121/130 pattern) |
|
||||
|
||||
## 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). Manual verification: **`docs/manual-qa/NEO-141.md`**. |
|
||||
|
||||
## 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` |
|
||||
|
|
@ -23,7 +23,7 @@ This branch lands **E7M3-08**: additive HTTP projections so clients can read fac
|
|||
| `docs/decomposition/modules/E7_M3_FactionReputationLedger.md` | **Matches** — Status line notes E7M3-08 landed (NEO-140). |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7.M3 row updated with quest HTTP projection links. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **N/A change** — no register row edit required beyond module doc / alignment table. |
|
||||
| `docs/manual-qa/NEO-140.md` | **Matches** — Bruno + curl checklist covers gate rules and rep summary assertions. |
|
||||
| `docs/manual-qa/NEO-140.md` | **Removed post-merge** — server-only story; manual QA reserved for client-facing work (story-end decision). |
|
||||
| `server/README.md` | **Matches** — Quest-definitions and quest-progress sections document new fields with NEO-140 cross-links. |
|
||||
|
||||
## Blocking issues
|
||||
|
|
@ -48,7 +48,7 @@ None.
|
|||
cd /Users/don/neon-sprawl && dotnet test NeonSprawl.sln
|
||||
```
|
||||
|
||||
Manual (see `docs/manual-qa/NEO-140.md`):
|
||||
Manual (Bruno — no separate manual QA doc for this server-only story):
|
||||
|
||||
- `cd server/NeonSprawl.Server && dotnet run`
|
||||
- Bruno: `quest-definitions/Get quest definitions`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
# Code review — NEO-141 (E7M3-09)
|
||||
|
||||
**Date:** 2026-06-17
|
||||
**Scope:** Branch `NEO-141-faction-telemetry-hooks` — commits `5ab8121` … `3c1ade4` vs `main`
|
||||
**Base:** `main`
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approve with nits**
|
||||
|
||||
## Summary
|
||||
|
||||
This branch lands **E7M3-09**: comment-only telemetry anchor sites for future E9.M1 catalog events **`reputation_delta`** and **`faction_gate_blocked`**, consolidating partial stubs from NEO-136/137/138 into single ops-layer hooks. **`ReputationOperations.TryApplyDelta`** gains a full NEO-121-style block after audit append success; **`FactionGateOperations.TryEvaluate`** adds the missing read-failure deny hook and tightens threshold-branch payload notes; **`RewardRouterOperations`** duplicate **`reputation_delta`** stub is removed. **`server/README.md`** adds a dedicated NEO-141 subsection and cross-links NEO-130. Docs (plan, module, alignment register, backlog, manual QA) are reconciled. The branch also includes **NEO-140 story-end cleanup** (remove `docs/manual-qa/NEO-140.md`, plan/review wording). No runtime, wire, or test behavior changes — **823** server tests pass. Low merge risk.
|
||||
|
||||
## Documentation checked
|
||||
|
||||
| Path | Result |
|
||||
|------|--------|
|
||||
| `docs/plans/NEO-141-implementation-plan.md` | **Matches** — kickoff decisions, AC checklist, reconciliation section, and file list align with the shipped diff. |
|
||||
| `docs/plans/E7M3-pre-production-backlog.md` | **Matches** — E7M3-09 AC checked and landed note correct; in-scope bullets align with ops-only anchors. |
|
||||
| `docs/decomposition/modules/E7_M3_FactionReputationLedger.md` | **Matches** — Status line notes E7M3-09 landed (NEO-141); Responsibilities “Emit telemetry hook sites” satisfied at comment layer. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7.M3 row updated with NEO-141 telemetry + README link. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **N/A change** — no register row edit required beyond module doc / alignment table. |
|
||||
| `docs/manual-qa/NEO-141.md` | **Matches** — NEO-130-style code-review checklist; Bruno optional sanity steps. |
|
||||
| `docs/plans/NEO-140-implementation-plan.md` | **Matches** — manual QA removal note consistent with story-end convention (bundled in branch). |
|
||||
| `server/README.md` | **Matches** — Faction telemetry hooks (NEO-141) subsection; NEO-130 table cross-link; no duplicate router stub. |
|
||||
|
||||
## Blocking issues
|
||||
|
||||
None.
|
||||
|
||||
## Suggestions
|
||||
|
||||
1. ~~**Align E7M3-09 backlog in-scope bullet with shipped anchor.** [E7M3-pre-production-backlog.md](../plans/E7M3-pre-production-backlog.md) §E7M3-09 still lists “`**TryAccept`** deny path” alongside **`FactionGateOperations`**, but the adopted plan and code place **`faction_gate_blocked`** only in **`FactionGateOperations.TryEvaluate`** (quest layer maps HTTP reason without duplicating the hook). Update the in-scope bullet to ops-only wording so future readers do not expect a **`QuestStateOperations`** comment block.~~ **Done.** In-scope bullet now reads **`FactionGateOperations.TryEvaluate`** (threshold + standing read failure deny paths).
|
||||
|
||||
## Nits
|
||||
|
||||
- ~~Nit: **`FactionGateOperations`** class summary says “deny return path below” (singular) while two deny branches now carry hooks — consider “deny return paths” for accuracy.~~ **Done.**
|
||||
- Nit: Branch bundles **NEO-140** manual QA removal (`ef75111`) with **NEO-141** work — acceptable hygiene; PR description should mention both if opened as one branch.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd /Users/don/neon-sprawl && dotnet test NeonSprawl.sln
|
||||
```
|
||||
|
||||
Manual (see `docs/manual-qa/NEO-141.md` — code review checklist):
|
||||
|
||||
- Confirm hook comment blocks in `ReputationOperations.cs`, `FactionGateOperations.cs`; no **`reputation_delta`** stub in `RewardRouterOperations.cs`.
|
||||
- Confirm README NEO-141 subsection and NEO-130 cross-link.
|
||||
|
||||
Optional Bruno (unchanged behavior):
|
||||
|
||||
- `quest-progress/Accept grid contract faction gate deny`
|
||||
- `quest-progress/Accept grid contract after operator chain`
|
||||
|
|
@ -5,7 +5,7 @@ namespace NeonSprawl.Server.Game.Factions;
|
|||
/// <summary>
|
||||
/// Evaluates <see cref="FactionGateRuleRow"/> minimum-standing gates (NEO-137).
|
||||
/// Quest accept wiring: <see cref="QuestStateOperations.TryAccept"/>.
|
||||
/// NEO-141 telemetry hook site: deny return path below.
|
||||
/// NEO-141 telemetry hook site: deny return paths below.
|
||||
/// </summary>
|
||||
public static class FactionGateOperations
|
||||
{
|
||||
|
|
@ -30,13 +30,16 @@ public static class FactionGateOperations
|
|||
var read = standingStore.TryGetStanding(normalizedPlayerId, factionId);
|
||||
if (!read.Success)
|
||||
{
|
||||
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `faction_gate_blocked` ---
|
||||
// TODO(E9.M1): catalog emit — ops reasonCode (e.g. unknown_faction), playerId, factionId, minStanding, currentStanding (null).
|
||||
// No ingest or ILogger here (comments-only).
|
||||
return Deny(read.ReasonCode, factionId, rule.MinStanding, null);
|
||||
}
|
||||
|
||||
if (read.Standing < rule.MinStanding)
|
||||
{
|
||||
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `faction_gate_blocked` ---
|
||||
// TODO(E9.M1): catalog emit — playerId, factionId, minStanding, currentStanding, gate context.
|
||||
// TODO(E9.M1): catalog emit — ops reasonCode (gate_blocked), playerId, factionId, minStanding, currentStanding.
|
||||
// No ingest or ILogger here (comments-only).
|
||||
return Deny(
|
||||
FactionGateEvaluateReasonCodes.GateBlocked,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ public static class ReputationOperations
|
|||
if (auditStore.TryAppend(row))
|
||||
{
|
||||
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `reputation_delta` ---
|
||||
// TODO(E9.M1): catalog emit — once per successful auditable apply (standing + audit committed).
|
||||
// Not on deny paths, audit append failure + compensating revert, or caller rollback paths.
|
||||
// Planned payload fields: playerId, factionId, appliedDelta, newStanding, sourceKind, sourceId, deltaId, appliedAt.
|
||||
// No ingest or ILogger here (comments-only).
|
||||
return new ReputationApplyOutcome(
|
||||
true,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -220,9 +220,6 @@ public static class RewardRouterOperations
|
|||
|
||||
var appliedAmount = repOutcome.NewStanding - repOutcome.PreviousStanding;
|
||||
appliedReputation.Add(new RewardReputationGrantApplied(grant.FactionId, appliedAmount));
|
||||
|
||||
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `reputation_delta` ---
|
||||
// TODO(E9.M1): catalog emit — once per applied rep row on TryRecord success only (not rollback paths).
|
||||
}
|
||||
|
||||
var deliveredAt = timeProvider.GetUtcNow();
|
||||
|
|
|
|||
|
|
@ -134,6 +134,17 @@ Quest accept evaluates **`FactionGateRule`** rows through **`FactionGateOperatio
|
|||
|
||||
Plan: [NEO-137 implementation plan](../../docs/plans/NEO-137-implementation-plan.md).
|
||||
|
||||
### Faction telemetry hooks (NEO-141)
|
||||
|
||||
Comment-only hook sites for future E9.M1 catalog events ([Epic 7 Slice 3](../../docs/decomposition/modules/E7_M3_FactionReputationLedger.md#related-implementation-slices)). **`TODO(E9.M1)`** — no production ingest. Quest accept and reward router delegate to these ops layers only (no duplicate API-layer or router-layer hooks for these names). Quest rep grants emit **`reputation_delta`** via **`ReputationOperations`** — see [Reward telemetry hooks (NEO-130)](#reward-telemetry-hooks-neo-130) cross-link.
|
||||
|
||||
| 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). |
|
||||
|
||||
Manual QA: [`docs/manual-qa/NEO-141.md`](../../docs/manual-qa/NEO-141.md); plan: [NEO-141 implementation plan](../../docs/plans/NEO-141-implementation-plan.md).
|
||||
|
||||
**Dev quest fixture (NEO-137, Bruno/manual QA):** When `Game:EnableQuestFixtureApi` is **true** (default in **Development** via `appsettings.Development.json`, **Testing**, or CI Bruno step) or the host environment is **Development** / **Testing**, **`POST /game/players/{id}/__dev/quest-fixture`** accepts `schemaVersion` **1** with optional (resets run before **`completedQuestIds`** when combined):
|
||||
- **`resetQuestIds`** — deletes each quest's progress row for the player and clears matching **`IRewardDeliveryStore`** + quest-completion **`IReputationDeltaStore`** audit rows (idempotent when absent).
|
||||
- **`resetFactionIds`** — clears each faction's standing row for the player (missing row reads as **0**; idempotent when absent).
|
||||
|
|
@ -408,9 +419,10 @@ Comment-only hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`*
|
|||
| Event | Anchor | When |
|
||||
|-------|--------|------|
|
||||
| **`reward_delivery`** | **`TryDeliverQuestCompletion`** | After **`TryRecord`** returns **`true`** on first-time delivery (not idempotent **`TryGet`** replay or race-loser paths). |
|
||||
| **`reputation_delta`** | **`TryDeliverQuestCompletion`** (stub) | After each successful reputation row apply (NEO-138); consolidated in NEO-141. |
|
||||
| **`unlock_granted`** | **`TryDeliverQuestCompletion`** (stub) | Future **`UnlockGrant`** apply from bundle rows — prototype bundles have item + skill XP only; no runtime unlock apply in Slice 2. |
|
||||
|
||||
**`reputation_delta`** from quest bundle rep rows emits via **`ReputationOperations.TryApplyDelta`** ([NEO-141](#faction-telemetry-hooks-neo-141)) — not duplicated in the router.
|
||||
|
||||
**`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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue