NEO-121: Add comment-only quest telemetry hook sites in QuestStateOperations.

Document quest_start, quest_step_complete, quest_complete, and quest_accept_denied
anchors for future E9.M1 ingest with README, manual QA, and module doc updates.
pull/160/head
VinPropane 2026-06-07 12:29:52 -04:00
parent 72b98a627c
commit 705a7cd028
8 changed files with 125 additions and 15 deletions

View File

@ -7,7 +7,7 @@
| **Module ID** | E7.M1 | | **Module ID** | E7.M1 |
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) | | **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
| **Stage target** | Prototype | | **Stage target** | Prototype |
| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) **`QuestObjectiveWiring`** **landed**; **E7M1-08** [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) per-player GET **landed**; **E7M1-09** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) accept POST **landed**; client from **E7M1-11** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | | **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) **`QuestObjectiveWiring`** **landed**; **E7M1-08** [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) per-player GET **landed**; **E7M1-09** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) accept POST **landed**; **E7M1-10** [NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) telemetry hooks **landed**; client from **E7M1-11** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
| **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) | | **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) |
## Purpose ## Purpose
@ -92,12 +92,15 @@ The **first shipped quest spine** is **frozen** for prototype tuning until a del
**Quest accept POST (NEO-120):** **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi` + DTOs; wires **`QuestStateOperations.TryAccept`**; structured **`accepted`** / **`reasonCode`** / optional **`quest`** row ([NEO-120](../../plans/NEO-120-implementation-plan.md)); [server README — Quest accept POST (NEO-120)](../../../server/README.md#quest-accept-post-neo-120); Bruno accept spine in `bruno/neon-sprawl-server/quest-progress/`. **Quest accept POST (NEO-120):** **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi` + DTOs; wires **`QuestStateOperations.TryAccept`**; structured **`accepted`** / **`reasonCode`** / optional **`quest`** row ([NEO-120](../../plans/NEO-120-implementation-plan.md)); [server README — Quest accept POST (NEO-120)](../../../server/README.md#quest-accept-post-neo-120); Bruno accept spine in `bruno/neon-sprawl-server/quest-progress/`.
**Quest telemetry hooks (NEO-121):** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** hook sites in **`QuestStateOperations`** ([NEO-121](../../plans/NEO-121-implementation-plan.md)); [server README — Quest telemetry hooks (NEO-121)](../../../server/README.md#quest-telemetry-hooks-neo-121).
**Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2. **Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2.
## Risks and telemetry ## Risks and telemetry
- Scripting weight: keep `QuestDef` data-first; templates in decomposition/tooling path per epic. - Scripting weight: keep `QuestDef` data-first; templates in decomposition/tooling path per epic.
- Double-claim: design for idempotency before E7.M2; lint bundles vs E6.M4 / E3.M5. - Double-claim: design for idempotency before E7.M2; lint bundles vs E6.M4 / E3.M5.
- Epic 7 Slice 1 catalog events (comment-only until E9.M1): **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** — anchored in **`QuestStateOperations`** (NEO-121).
## Source anchors ## Source anchors

File diff suppressed because one or more lines are too long

View File

@ -109,7 +109,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|---|---|---|---|---|---| |---|---|---|---|---|---|
| E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned | | E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned |
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); **E7M1-07 / NEO-118** **`QuestObjectiveWiring`** landed ([NEO-118 plan](../../plans/NEO-118-implementation-plan.md)); **E7M1-08 / NEO-119** per-player GET landed ([NEO-119 plan](../../plans/NEO-119-implementation-plan.md)); **E7M1-09 / NEO-120** accept POST landed ([NEO-120 plan](../../plans/NEO-120-implementation-plan.md)); register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); **E7M1-07 / NEO-118** **`QuestObjectiveWiring`** landed ([NEO-118 plan](../../plans/NEO-118-implementation-plan.md)); **E7M1-08 / NEO-119** per-player GET landed ([NEO-119 plan](../../plans/NEO-119-implementation-plan.md)); **E7M1-09 / NEO-120** accept POST landed ([NEO-120 plan](../../plans/NEO-120-implementation-plan.md)); **E7M1-10 / NEO-121** comment-only quest telemetry hook sites landed ([NEO-121 plan](../../plans/NEO-121-implementation-plan.md)); register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123).
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned | | E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned |
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned | | E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned | | E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |

View File

@ -0,0 +1,35 @@
# Manual QA — NEO-121 (quest telemetry hook sites)
Reference: [implementation plan](../plans/NEO-121-implementation-plan.md), [NEO-117 implementation plan](../plans/NEO-117-implementation-plan.md) (quest operations), [NEO-118 implementation plan](../plans/NEO-118-implementation-plan.md) (objective 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**; quest behavior matches NEO-117/118/119/120.
## Code review check
- [ ] Open `server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs`.
- [ ] Confirm **`quest_start`** comment block immediately after successful **`TryActivate`** in **`TryAccept`** (before objective wiring).
- [ ] Confirm **`quest_step_complete`** comment block after successful **`TryAdvanceStep`** store commit.
- [ ] Confirm **`quest_complete`** comment block after first-time **`TryMarkComplete`** commit (not on idempotent **`completed`** early return).
- [ ] Confirm **`quest_accept_denied`** comment in **`DenyAccept`** (used by all **`TryAccept`** deny paths).
- [ ] Confirm **`TODO(E9.M1)`** markers and planned payload fields; **no** new `ILogger` or metrics calls.
- [ ] Confirm **no** duplicate hook comments in **`QuestAcceptApi`**, **`QuestProgressApi`**, or **`QuestObjectiveWiring`**.
- [ ] Open [server README — Quest telemetry hooks (NEO-121)](../../server/README.md#quest-telemetry-hooks-neo-121) — NEO-121 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 default** — all quests **`not_started`** unchanged.
- **Post accept gather intro****`accepted: true`** unchanged.
- **Post accept duplicate****`accepted: false`**, **`reasonCode: already_active`** unchanged.

View File

@ -315,8 +315,10 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco
**Acceptance criteria** **Acceptance criteria**
- [ ] Hook sites exist at accept, step complete, quest complete paths. - [x] Hook sites exist at accept, step complete, quest complete paths.
- [ ] Epic 7 Slice 1 telemetry vocabulary documented. - [x] Epic 7 Slice 1 telemetry vocabulary documented.
**Landed (NEO-121):** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** in **`QuestStateOperations`**; [server README — Quest telemetry hooks (NEO-121)](../../server/README.md#quest-telemetry-hooks-neo-121); plan [NEO-121](../../plans/NEO-121-implementation-plan.md).
**Client counterpart:** none (infrastructure-only). **Client counterpart:** none (infrastructure-only).

View File

@ -47,8 +47,18 @@
## Acceptance criteria checklist ## Acceptance criteria checklist
- [ ] Hook sites exist at accept, step complete, quest complete paths. - [x] Hook sites exist at accept, step complete, quest complete paths.
- [ ] Epic 7 Slice 1 telemetry vocabulary documented in `server/README.md`. - [x] Epic 7 Slice 1 telemetry vocabulary documented in `server/README.md`.
## Implementation reconciliation (shipped)
- **Engine anchor:** **`QuestStateOperations`** — four comment-only hook sites with **`TODO(E9.M1)`** (no runtime emit).
- **`quest_start`:** after successful **`TryActivate`** in **`TryAccept`**.
- **`quest_step_complete`:** after successful **`TryAdvanceStep`** store commit.
- **`quest_complete`:** after first-time **`TryMarkComplete`** commit (not idempotent replay).
- **`quest_accept_denied`:** **`DenyAccept`** helper on all **`TryAccept`** deny paths.
- **Docs:** `server/README.md` quest telemetry subsection; E7.M1 module + alignment register + E7M1 backlog updated.
- **Manual QA:** `docs/manual-qa/NEO-121.md`.
## Technical approach ## Technical approach

View File

@ -5,6 +5,8 @@ namespace NeonSprawl.Server.Game.Quests;
/// <summary> /// <summary>
/// Server-authoritative quest accept, step advance, and complete with reason codes (NEO-117). /// Server-authoritative quest accept, step advance, and complete with reason codes (NEO-117).
/// Objective wiring: E7M1-07 (NEO-118). HTTP: E7M1-08/09 (NEO-119/NEO-120). /// Objective wiring: E7M1-07 (NEO-118). HTTP: E7M1-08/09 (NEO-119/NEO-120).
/// Telemetry hook sites: E7M1-10 (NEO-121) — comment-only <c>quest_start</c>, <c>quest_step_complete</c>,
/// <c>quest_complete</c>, <c>quest_accept_denied</c> for future E9.M1.
/// </summary> /// </summary>
public static class QuestStateOperations public static class QuestStateOperations
{ {
@ -23,22 +25,22 @@ public static class QuestStateOperations
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition)) !questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
{ {
return Deny(QuestStateReasonCodes.UnknownQuest); return DenyAccept(QuestStateReasonCodes.UnknownQuest);
} }
if (!progressStore.CanWritePlayer(playerId)) if (!progressStore.CanWritePlayer(playerId))
{ {
return Deny(QuestStateReasonCodes.UnknownPlayer); return DenyAccept(QuestStateReasonCodes.UnknownPlayer);
} }
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing)) if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing))
{ {
if (existing.Status == QuestProgressStatus.Completed) if (existing.Status == QuestProgressStatus.Completed)
{ {
return Deny(QuestStateReasonCodes.AlreadyCompleted, existing); return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, existing);
} }
return Deny(QuestStateReasonCodes.AlreadyActive, existing); return DenyAccept(QuestStateReasonCodes.AlreadyActive, existing);
} }
foreach (var prerequisiteId in definition.PrerequisiteQuestIds) foreach (var prerequisiteId in definition.PrerequisiteQuestIds)
@ -46,12 +48,16 @@ public static class QuestStateOperations
if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) || if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) ||
prerequisite.Status != QuestProgressStatus.Completed) prerequisite.Status != QuestProgressStatus.Completed)
{ {
return Deny(QuestStateReasonCodes.PrerequisiteIncomplete); return DenyAccept(QuestStateReasonCodes.PrerequisiteIncomplete);
} }
} }
if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot)) if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot))
{ {
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_start` ---
// TODO(E9.M1): catalog emit — first activation only (deny paths excluded).
// Planned payload fields: playerId, questId, currentStepIndex (0). No ingest or ILogger here (comments-only).
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest( QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
playerId, playerId,
normalizedQuestId, normalizedQuestId,
@ -120,6 +126,11 @@ public static class QuestStateOperations
if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot)) if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot))
{ {
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_step_complete` ---
// TODO(E9.M1): catalog emit — once per committed step advance (chain quests may emit multiple per wiring pass).
// Planned payload fields: playerId, questId, completedStepIndex (prior index), newStepIndex;
// optional step id from catalog at E9.M1 wiring time. No ingest or ILogger here (comments-only).
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest( QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
playerId, playerId,
normalizedQuestId, normalizedQuestId,
@ -180,6 +191,11 @@ public static class QuestStateOperations
if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot)) if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot))
{ {
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_complete` ---
// TODO(E9.M1): catalog emit — first-time completion only; idempotent replay returns above without hook.
// Planned payload fields: playerId, questId, completedAt; optional total step count.
// No ingest or ILogger here (comments-only).
return Success(snapshot); return Success(snapshot);
} }
@ -208,11 +224,11 @@ public static class QuestStateOperations
string playerId, string playerId,
string questId, string questId,
QuestStepState attemptSnapshot) => QuestStepState attemptSnapshot) =>
DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.AlreadyActive) DenyFromProgressSnapshotAccept(attemptSnapshot, QuestStateReasonCodes.AlreadyActive)
?? (progressStore.TryGetProgress(playerId, questId, out var reread) ?? (progressStore.TryGetProgress(playerId, questId, out var reread)
? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.AlreadyActive) ? DenyFromProgressSnapshotAccept(reread, QuestStateReasonCodes.AlreadyActive)
: null) : null)
?? Deny(QuestStateReasonCodes.UnknownPlayer); ?? DenyAccept(QuestStateReasonCodes.UnknownPlayer);
private static QuestStateOperationResult DenyAdvanceFailure( private static QuestStateOperationResult DenyAdvanceFailure(
IPlayerQuestStateStore progressStore, IPlayerQuestStateStore progressStore,
@ -248,9 +264,40 @@ public static class QuestStateOperations
return null; return null;
} }
private static QuestStateOperationResult? DenyFromProgressSnapshotAccept(
QuestStepState? snapshot,
string activeOrIntermediateReasonCode)
{
if (snapshot is null)
{
return null;
}
if (snapshot.Status == QuestProgressStatus.Completed)
{
return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, snapshot);
}
if (snapshot.Status == QuestProgressStatus.Active)
{
return DenyAccept(activeOrIntermediateReasonCode, snapshot);
}
return null;
}
private static QuestStateOperationResult Success(QuestStepState snapshot) => private static QuestStateOperationResult Success(QuestStepState snapshot) =>
new(Success: true, ReasonCode: null, Snapshot: snapshot); new(Success: true, ReasonCode: null, Snapshot: snapshot);
private static QuestStateOperationResult DenyAccept(string reasonCode, QuestStepState? snapshot = null)
{
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_accept_denied` ---
// TODO(E9.M1): catalog emit — reasonCode from QuestStateReasonCodes (unknown_quest, prerequisite_incomplete, etc.).
// Planned payload fields: playerId, questId, reasonCode. No ingest or ILogger here (comments-only).
return Deny(reasonCode, snapshot);
}
private static QuestStateOperationResult Deny(string reasonCode, QuestStepState? snapshot = null) => private static QuestStateOperationResult Deny(string reasonCode, QuestStepState? snapshot = null) =>
new(Success: false, ReasonCode: reasonCode, Snapshot: snapshot); new(Success: false, ReasonCode: reasonCode, Snapshot: snapshot);
} }

View File

@ -250,6 +250,19 @@ Sample success:
{"schemaVersion":1,"accepted":true,"quest":{"questId":"prototype_quest_gather_intro","status":"active","currentStepIndex":0,"objectiveCounters":{}}} {"schemaVersion":1,"accepted":true,"quest":{"questId":"prototype_quest_gather_intro","status":"active","currentStepIndex":0,"objectiveCounters":{}}}
``` ```
### Quest telemetry hooks (NEO-121)
Comment-only hook sites in **`QuestStateOperations`** for future E9.M1 catalog events ([Epic 7 Slice 1](../../docs/decomposition/modules/E7_M1_QuestStateMachine.md#risks-and-telemetry)). **`TODO(E9.M1)`** — no production ingest. HTTP **`POST …/accept`** and **`QuestObjectiveWiring`** delegate to the engine only (no duplicate API-layer or wiring-layer hooks).
| Event | Anchor | When |
|-------|--------|------|
| **`quest_start`** | **`TryAccept`** | After successful **`TryActivate`** (first activation only). |
| **`quest_step_complete`** | **`TryAdvanceStep`** | After successful store commit (one emit per advance; chain quests may emit multiple per wiring pass). |
| **`quest_complete`** | **`TryMarkComplete`** | After first-time completion commit (not idempotent replay when already **`completed`**). |
| **`quest_accept_denied`** | **`TryAccept`** deny paths | Every structured accept deny with stable **`reasonCode`**. |
Manual QA: [`docs/manual-qa/NEO-121.md`](../../docs/manual-qa/NEO-121.md); plan: [NEO-121 implementation plan](../../docs/plans/NEO-121-implementation-plan.md).
## Encounter definitions (NEO-103) ## 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/`. **`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/`.