NEO-152: Add comment-only contract telemetry hook sites.

Anchor contract_issued, contract_complete, and reward_anomaly
for E9.M1; document hooks in server README and module docs.
pull/194/head
VinPropane 2026-06-28 12:08:05 -04:00
parent 6e5ea78cc8
commit 0d089a6dff
8 changed files with 51 additions and 7 deletions

View File

@ -69,6 +69,8 @@ Epic 7 **Slice 4** — `contract_issued`, `contract_complete`, reward anomalies.
**Contract HTTP (NEO-151):** **`POST …/contracts/issue`** + **`GET …/contracts`** — issue envelope + per-player list with **`encounterTemplateId`** and **`completionRewardSummary`** on completed rows; Bruno `bruno/neon-sprawl-server/contracts/`; [server README — Contract issue HTTP (NEO-151)](../../../server/README.md#contract-issue-http-neo-151), [Per-player contract list HTTP (NEO-151)](../../../server/README.md#per-player-contract-list-http-neo-151); plan [NEO-151](../../plans/NEO-151-implementation-plan.md). **Contract HTTP (NEO-151):** **`POST …/contracts/issue`** + **`GET …/contracts`** — issue envelope + per-player list with **`encounterTemplateId`** and **`completionRewardSummary`** on completed rows; Bruno `bruno/neon-sprawl-server/contracts/`; [server README — Contract issue HTTP (NEO-151)](../../../server/README.md#contract-issue-http-neo-151), [Per-player contract list HTTP (NEO-151)](../../../server/README.md#per-player-contract-list-http-neo-151); plan [NEO-151](../../plans/NEO-151-implementation-plan.md).
**Contract telemetry hooks (NEO-152):** comment-only **`contract_issued`**, **`contract_complete`**, and **`reward_anomaly`** anchor sites in generator/completion ops + shared reward router rollback; [server README — Contract telemetry hooks (NEO-152)](../../../server/README.md#contract-telemetry-hooks-neo-152); plan [NEO-152](../../plans/NEO-152-implementation-plan.md).
## Source anchors ## Source anchors
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 7. - Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 7.

File diff suppressed because one or more lines are too long

View File

@ -307,7 +307,9 @@ Working backlog for **Epic 7 — Slice 4** ([contract generator](../decompositio
**Acceptance criteria** **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-152):** comment-only **`contract_issued`**, **`contract_complete`**, and **`reward_anomaly`** hooks; [server README — Contract telemetry hooks (NEO-152)](../../server/README.md#contract-telemetry-hooks-neo-152); plan [NEO-152](../../plans/NEO-152-implementation-plan.md).
**Client counterpart:** none (infrastructure-only). **Client counterpart:** none (infrastructure-only).

View File

@ -49,7 +49,15 @@ Anchor future E9.M1 catalog events **`contract_issued`**, **`contract_complete`*
## Acceptance criteria checklist ## Acceptance criteria checklist
- [ ] Hook sites documented in README; no behavior change beyond comments. - [x] Hook sites documented in README; no behavior change beyond comments.
## Implementation reconciliation (shipped)
- **`ContractGeneratorOperations.TryIssue`:** **`contract_issued`** hook after successful **`TryCreateActive`**; **`reward_anomaly`** stub on economy validation deny.
- **`ContractCompletionOperations.TryCompleteOnEncounterClear`:** **`contract_complete`** hook extended to NEO-121 block style (NEO-149 partial comment completed).
- **`RewardRouterOperations.TryDeliverBundle` / `CompensatingRevertAll`:** **`reward_anomaly`** stub for partial-apply rollback; NEO-130 **`reward_delivery`** comment updated for contract **`SourceKind`**.
- **Docs:** `server/README.md` **Contract telemetry hooks (NEO-152)** subsection; E7.M4 module + alignment register + E7M4 backlog updated.
- **Tests:** no new tests — comments-only; **`dotnet test`** regression green.
## Technical approach ## Technical approach

View File

@ -101,7 +101,10 @@ public static class ContractCompletionOperations
completedAt); completedAt);
// --- Telemetry hook site (NEO-152): future E9.M1 catalog event `contract_complete` --- // --- Telemetry hook site (NEO-152): future E9.M1 catalog event `contract_complete` ---
// TODO(E9.M1): catalog emit — first-time completion only; idempotent replay returns below without hook. // TODO(E9.M1): catalog emit — first-time completion only; idempotent replay returns below without hook.
// Planned payload: playerId, contractInstanceId, templateId, encounterId, completedAt, idempotencyKey. // Planned payload fields: playerId, contractInstanceId, templateId, encounterId, completedAt,
// idempotencyKey ({playerId}:contract_complete:{contractInstanceId}).
// No ingest or ILogger here (comments-only).
return Success(completedSnapshot, outcome); return Success(completedSnapshot, outcome);
} }

View File

@ -67,6 +67,11 @@ public static class ContractGeneratorOperations
factionRegistry, factionRegistry,
out var economyReason)) out var economyReason))
{ {
// --- Telemetry hook site (NEO-152): future E9.M1 catalog event `reward_anomaly` ---
// TODO(E9.M1): catalog emit — economy validation deny at issue (defense-in-depth; startup load should prevent most).
// Planned payload fields: playerId, templateId, reasonCode (economy_cap_exceeded | invalid_reward_bundle).
// No ingest or ILogger here (comments-only).
return Deny(economyReason!); return Deny(economyReason!);
} }
@ -87,6 +92,11 @@ public static class ContractGeneratorOperations
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
out var snapshot)) out var snapshot))
{ {
// --- Telemetry hook site (NEO-152): future E9.M1 catalog event `contract_issued` ---
// TODO(E9.M1): catalog emit — first-time active instance persist only (deny paths excluded).
// Planned payload fields: playerId, contractInstanceId, templateId, seedBucket, encounterTemplateId, issuedAt.
// No ingest or ILogger here (comments-only).
return Success(snapshot, template.EncounterTemplateId); return Success(snapshot, template.EncounterTemplateId);
} }

View File

@ -12,7 +12,8 @@ namespace NeonSprawl.Server.Game.Rewards;
/// Applies quest and contract <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127, NEO-148). /// Applies quest and contract <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127, NEO-148).
/// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128). /// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128).
/// Reputation grants: <see cref="ReputationOperations"/> (NEO-138). /// Reputation grants: <see cref="ReputationOperations"/> (NEO-138).
/// E9.M1 telemetry hook sites: <see cref="TryDeliverQuestCompletion"/> (NEO-130). /// E9.M1 telemetry hook sites: <see cref="TryDeliverQuestCompletion"/> (NEO-130);
/// shared bundle rollback <c>reward_anomaly</c> (NEO-152).
/// </summary> /// </summary>
public static class RewardRouterOperations public static class RewardRouterOperations
{ {
@ -335,9 +336,9 @@ public static class RewardRouterOperations
if (deliveryStore.TryRecord(deliveryEvent)) if (deliveryStore.TryRecord(deliveryEvent))
{ {
// --- Telemetry hook site (NEO-130): future E9.M1 catalog event `reward_delivery` --- // --- Telemetry hook site (NEO-130): future E9.M1 catalog event `reward_delivery` ---
// TODO(E9.M1): catalog emit — once per first-time quest completion delivery (TryRecord success). // TODO(E9.M1): catalog emit — once per first-time quest or contract completion delivery (TryRecord success).
// Not on TryGet idempotent replay, deny paths, or TryRecord race-loser rollback paths. // Not on TryGet idempotent replay, deny paths, or TryRecord race-loser rollback paths.
// Planned payload fields: playerId, questId, deliveredAt, grantedItems (itemId, quantity), // Planned payload fields: playerId, sourceKind, sourceId, deliveredAt, grantedItems (itemId, quantity),
// grantedSkillXp (skillId, amount), grantedReputation (factionId, amount), idempotencyKey. // grantedSkillXp (skillId, amount), grantedReputation (factionId, amount), idempotencyKey.
// No ingest or ILogger here (comments-only). // No ingest or ILogger here (comments-only).
@ -371,6 +372,11 @@ public static class RewardRouterOperations
return Deny(RewardDeliveryReasonCodes.AlreadyDelivered); return Deny(RewardDeliveryReasonCodes.AlreadyDelivered);
} }
// --- Telemetry hook site (NEO-152): future E9.M1 catalog event `reward_anomaly` (stub) ---
// TODO(E9.M1): catalog emit — partial grant apply then compensating revert (skill XP, reputation, TryRecord race).
// E9.M1 filters by DeliveryTarget.SourceKind (contract_completion vs quest_completion).
// Planned payload fields: playerId, sourceKind, sourceId, reasonCode, partialAppliedGrants summary.
// Not on early denies before any grant apply or idempotent TryGet replay. No ingest or ILogger (comments-only).
private static void CompensatingRevertAll( private static void CompensatingRevertAll(
string playerId, string playerId,
DeliveryTarget target, DeliveryTarget target,

View File

@ -372,6 +372,19 @@ Unknown player ids return **404** (position gate). Bruno: `bruno/neon-sprawl-ser
Plan: [NEO-149 implementation plan](../../docs/plans/NEO-149-implementation-plan.md). Contract HTTP GET/issue: [Per-player contract list HTTP (NEO-151)](#per-player-contract-list-http-neo-151). Plan: [NEO-149 implementation plan](../../docs/plans/NEO-149-implementation-plan.md). Contract HTTP GET/issue: [Per-player contract list HTTP (NEO-151)](#per-player-contract-list-http-neo-151).
### Contract telemetry hooks (NEO-152)
Comment-only hook sites for future E9.M1 catalog events ([Epic 7 Slice 4](../../docs/decomposition/modules/E7_M4_ContractMissionGenerator.md#related-implementation-slices)). **`TODO(E9.M1)`** — no production ingest. **`ContractIssueApi`** and **`ContractListApi`** delegate to ops layers only (no duplicate API-layer hooks). Contract completion payouts emit **`reward_delivery`** via shared **`RewardRouterOperations.TryDeliverBundle`** — see [Reward telemetry hooks (NEO-130)](#reward-telemetry-hooks-neo-130) cross-link (`SourceKind = contract_completion`).
| Event | Anchor | When |
|-------|--------|------|
| **`contract_issued`** | **`ContractGeneratorOperations.TryIssue`** | After successful **`TryCreateActive`** (active instance persisted). |
| **`contract_complete`** | **`ContractCompletionOperations.TryCompleteOnEncounterClear`** | After successful deliver-then-mark (instance marked complete + outcome appended). |
| **`reward_anomaly`** | **`ContractGeneratorOperations.TryIssue`** (economy deny); **`RewardRouterOperations.CompensatingRevertAll`** (via **`TryDeliverBundle`**) | Economy cap / bundle cross-ref deny at issue; compensating revert after partial delivery apply. |
| **`reward_delivery`** *(cross-link)* | **`RewardRouterOperations.TryDeliverBundle`** | NEO-130 — first-time **`TryRecord`** success; contract path uses **`SourceKind = contract_completion`**. |
Plan: [NEO-152 implementation plan](../../docs/plans/NEO-152-implementation-plan.md).
## Contract outcome store (NEO-146) ## Contract outcome store (NEO-146)
Append-only contract completion audit rows live in **`IContractOutcomeStore`**. Each **`ContractOutcomeRow`** captures instance id, delivery idempotency key **`{playerId}:contract_complete:{contractInstanceId}`**, grant snapshot summary (items, skill XP, reputation — same shapes as **`RewardDeliveryEvent`**), and **`recordedAt`**. Append-only contract completion audit rows live in **`IContractOutcomeStore`**. Each **`ContractOutcomeRow`** captures instance id, delivery idempotency key **`{playerId}:contract_complete:{contractInstanceId}`**, grant snapshot summary (items, skill XP, reputation — same shapes as **`RewardDeliveryEvent`**), and **`recordedAt`**.