neon-sprawl/docs/plans/NEO-138-implementation-plan.md

14 KiB

NEO-138 — E7M3-06: Extend RewardRouterOperations for reputationGrants

Linear: NEO-138
Branch: NEO-138-e7m3-reward-router-reputation-grants
Backlog: E7M3-pre-production-backlog.mdE7M3-06
Module: E7_M3_FactionReputationLedger.md
Pattern: NEO-127 — grant order + compensating rollback + TryRecord race mirror; NEO-136ReputationOperations.TryApplyDelta orchestration
Precursor: NEO-136 DoneReputationOperations.TryApplyDelta (landed on main)
Blocks: NEO-140 (HTTP quest projections for rep + gates)
Client counterpart: NEO-142 — HUD gate feedback (blocked by E7M3-07/E7M3-08)

Goal

Quest completion bundles apply reputationGrants idempotently alongside existing item and skill XP rows, using the same {playerId}:quest_complete:{questId} delivery key.

Kickoff clarifications

Topic Question Agent recommendation Answer
Bruno scope NEO-137 deferred grid-contract accept-success Bruno; backlog lists standing Bruno as optional (no GET until NEO-139). Quest-flow Bruno only — complete operator chain via HTTP quest flow, then accept grid contract success; defer standing GET assertions to NEO-139. Adopted — quest-flow Bruno

Other decisions settled by Linear AC, backlog kickoff defaults, and repo precedent (no additional questions):

Topic Decision Evidence
Grant order Items → skill XP → reputation; deny before TryRecord rolls back all prior grant types NEO-127 item→skill order; backlog “with item/skill XP rows”
Idempotency Same delivery key; TryGet replay returns stored snapshot without re-applying standing Backlog kickoff: “Rep grants ride {playerId}:quest_complete:{questId}
Rep apply path ReputationOperations.TryApplyDelta only — not standing store directly NEO-136 README + NEO-136 plan
Source attribution ReputationDeltaSourceKinds.QuestCompletion + quest id as sourceId NEO-136 adopted; server README
deltaId {idempotencyKey}:rep:{factionId} per grant row NEO-136 open-questions table
Unknown faction at runtime Deny delivery via ops passthrough unknown_faction; no store record Linear AC; startup cross-ref (NEO-134) prevents prod content errors
TryRecord race Roll back items, skill XP, and rep grants on loser (mirror NEO-127) Backlog compensating rollback policy
Rep rollback helper Inverse delta via ReputationOperations with rollback deltaId suffix (e.g. :rollback) NEO-136 “do not call standing store alone”
HTTP standing / projections Out of scope E7M3-07 (NEO-139), E7M3-08 (NEO-140)
Telemetry Comment-only reputation_delta hook on successful rep apply in router (NEO-141 consolidates) NEO-137 gate hook precedent

Scope and out-of-scope

In scope (from Linear + backlog):

  • Extend RewardRouterOperations.TryDeliverQuestCompletion to apply bundle.ReputationGrants via ReputationOperations.TryApplyDelta.
  • Extend RewardDeliveryEvent, RewardDeliveryResult, and store snapshots with rep grant lines.
  • Add router reason-code passthrough for rep denies (unknown_faction, invalid_delta, audit_append_failed, etc.).
  • Compensating rollback: rep deny after items/xp applied; rep revert on TryRecord race loss.
  • Inject IFactionStandingStore + IReputationDeltaStore through TryDeliverQuestCompletion, QuestStateOperations.TryMarkComplete, QuestObjectiveWiring, and test helpers.
  • Integration tests: operator-chain completion → +15 Grid Operators once; replay idempotent; unknown faction deny before store record.
  • Quest-flow Bruno: operator chain complete via HTTP + grid contract accept success (closes NEO-137 deferral).
  • server/README.md reward router section update.
  • E7.M3 module + alignment register updates when shipped.

Out of scope:

  • HTTP GET standing (NEO-139), quest HTTP rep projections (NEO-140), Godot HUD (NEO-142).
  • E9.M1 telemetry ingest (NEO-141 documents hooks only).
  • QuestFixtureOperations reward delivery (fixture remains progress-only; Bruno success uses organic chain completion).
  • Postgres migration for RewardDeliveryEvent shape (in-memory store only today; rep snapshot fields additive).

Client counterpart: NEO-142 — surfaces rep/gate feedback after NEO-139/NEO-140.

Acceptance criteria checklist

  • First-time quest completion applies rep grants exactly once per delivery key.
  • Unknown faction id in bundle denies delivery before store record.
  • dotnet test covers rep happy path, idempotent replay, unknown-faction deny, and rollback paths.

Technical approach

1. Types (Game/Rewards/)

Type Role
RewardReputationGrantApplied Snapshot row: FactionId, Amount (applied delta amount from ops outcome)
RewardDeliveryReasonCodes (extend) Passthrough: unknown_faction, invalid_delta, audit_append_failed, invalid_delta_id, invalid_source from rep ops

Extend RewardDeliveryEvent and RewardDeliveryResult with GrantedReputation (IReadOnlyList<RewardReputationGrantApplied>).

Add RewardDeliveryIds.MakeReputationDeltaId(idempotencyKey, factionId){key}:rep:{factionId}.

2. RewardRouterOperations.TryDeliverQuestCompletion

Signature additions: IFactionStandingStore standingStore, IReputationDeltaStore auditStore.

Flow (after existing skill XP loop, before TryRecord):

  1. Read reputationGrants from bundle (empty ⇒ skip).
  2. For each row, call ReputationOperations.TryApplyDelta with:
    • sourceKind = ReputationDeltaSourceKinds.QuestCompletion
    • sourceId = normalized quest id
    • deltaId = MakeReputationDeltaId(idempotencyKey, factionId)
  3. On any rep deny: CompensatingRevertAll — remove items, revert skill XP (existing helpers), revert applied rep rows via inverse TryApplyDelta with :rollback delta ids.
  4. Build RewardDeliveryEvent including GrantedReputation snapshot.
  5. On TryRecord race loss: extend existing rollback to include rep revert (same compensating helper).

Idempotent replay: unchanged — early TryGet returns stored event including rep snapshot; no standing mutation.

Empty bundle: still records empty rep array when TryRecord succeeds (mirror empty item/skill arrays).

3. Wiring

Call site Change
QuestStateOperations.TryMarkComplete Pass standing + audit stores to router
QuestObjectiveWiring Thread stores into TryMarkComplete (terminal step + bulk complete paths)
Test helpers QuestTestDependencies, QuestWiringTestDependencies, RewardRouterTestDependencies — resolve faction stores from factory

4. Prototype test scenarios

Scenario Setup Expected
Operator chain first complete Prereqs done; accept chain; TryMarkComplete Standing +15 Grid Operators; delivery event rep snapshot; audit row queryable
Idempotent replay Second TryDeliverQuestCompletion already_delivered; standing unchanged
Unknown faction Test bundle with fake factionId unknown_faction deny; no delivery store record; items/xp unchanged if rep-only bundle
Rep deny after items Mock/inject audit append failure on second rep row of multi-row bundle (unit) Items + skill + first rep rolled back
Grid contract gate Complete operator chain (applies +15); accept grid contract Accept succeeds (integration — extends NEO-137 gate success test without manual standing seed)

Constants: PrototypeE7M3QuestFactionRules.ExpectedCompletionBundles operator chain rep row; GridContractMinStanding = 15.

5. Bruno (quest-flow)

Request Role
Complete operator chain quest flow.bru (or pre-request helper in bruno-dev-fixture-helper.js) Reset player; complete gather/refine/combat intros; accept + finish operator chain via HTTP (gather/craft/encounter wiring) so completion delivers +15 rep organically
Accept grid contract after operator chain.bru POST accept prototype_quest_grid_contractaccepted: true (implies standing ≥ 15 from chain rep grant)

No standing GET assertions until NEO-139. Update deny Bruno docs to note success sibling at seq 11+.

6. Tests

RewardRouterOperationsTests (extend):

Test Covers
TryDeliverQuestCompletion_ShouldApplyReputationGrants_WhenOperatorChainBundle +15 standing; rep snapshot on event; audit row
TryDeliverQuestCompletion_ShouldNotDoubleApplyReputation_WhenAlreadyDelivered Idempotent replay
TryDeliverQuestCompletion_ShouldDenyUnknownFaction_WithoutStoreRecord Fake faction in test bundle
TryDeliverQuestCompletion_ShouldRollbackItemsAndSkillXp_WhenReputationApplyDenied Rep deny mid-bundle
Extend race-rollback tests Rep reverted on TryRecord loser

QuestStateOperationsTests (extend):

Test Covers
Extend TryMarkComplete_ShouldDeliverOperatorChainBundle_WhenFirstCompletion Assert GrantedReputation + standing 15
TryAccept_ShouldAcceptGridContract_WhenOperatorChainCompletedWithRepGrant End-to-end: complete chain via ops (no manual SeedGridStanding)

Manual QA: none — server engine; Bruno quest-flow smoke replaces player-visible checklist until NEO-142.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Rewards/RewardReputationGrantApplied.cs Rep grant snapshot row on delivery event
bruno/neon-sprawl-server/quest-progress/Accept grid contract after operator chain.bru Grid contract accept success after organic chain completion
bruno/neon-sprawl-server/quest-progress/scripts/operator-chain-quest-flow-helper.js (or extend bruno-dev-fixture-helper.js) HTTP helper to complete operator chain quest flow for Bruno pre-requests

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Rewards/RewardRouterOperations.cs Apply rep grants; compensating rollback; inject faction stores
server/NeonSprawl.Server/Game/Rewards/RewardDeliveryEvent.cs Add GrantedReputation field
server/NeonSprawl.Server/Game/Rewards/RewardDeliveryResult.cs Add GrantedReputation field
server/NeonSprawl.Server/Game/Rewards/RewardDeliveryReasonCodes.cs Rep deny passthrough constants
server/NeonSprawl.Server/Game/Rewards/RewardDeliveryIds.cs MakeReputationDeltaId helper
server/NeonSprawl.Server/Game/Rewards/InMemoryRewardDeliveryStore.cs Clone GrantedReputation array on record
server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs Pass standing + audit stores to router in TryMarkComplete
server/NeonSprawl.Server/Game/Quests/QuestObjectiveWiring.cs Thread stores into TryMarkComplete call sites
server/NeonSprawl.Server.Tests/Game/Rewards/RewardRouterOperationsTests.cs Rep grant AAA tests; extend Deliver helper + deps
server/NeonSprawl.Server.Tests/Game/Quests/QuestStateOperationsTests.cs Operator chain rep assertions; grid contract accept without manual seed
server/NeonSprawl.Server.Tests/Game/Quests/QuestObjectiveWiringTests.cs Update TryMarkComplete helper signature
server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs Update TryMarkComplete helper signature
server/NeonSprawl.Server.Tests/Game/Quests/QuestAcceptApiTests.cs Update any TryMarkComplete call sites if present
server/README.md Document rep grant apply order, rollback, reason codes, Bruno seq
docs/decomposition/modules/E7_M3_FactionReputationLedger.md Status: E7M3-06 in progress / landed
docs/decomposition/modules/documentation_and_implementation_alignment.md Register NEO-138 plan + shipped router extension
bruno/neon-sprawl-server/quest-progress/Accept grid contract faction gate deny.bru Docs: success sibling request added in NEO-138

Tests

File Action Coverage
server/NeonSprawl.Server.Tests/Game/Rewards/RewardRouterOperationsTests.cs Change Rep happy path, idempotent replay, unknown faction deny, rep-failure rollback, race rep rollback
server/NeonSprawl.Server.Tests/Game/Quests/QuestStateOperationsTests.cs Change Operator chain +15 standing; grid contract accept after chain completion
server/NeonSprawl.Server.Tests/Game/Quests/QuestObjectiveWiringTests.cs Change Helper signature only (compile fix)
server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs Change Helper signature only (compile fix)

No new Postgres integration tests — rep persistence covered by NEO-135/NEO-136; router tests use in-memory stores.

Open questions / risks

Question / risk Agent recommendation Status
Operator chain Bruno weight Encapsulate in shared JS helper; reuse craft/gather/encounter patterns from existing spines adopted
Rollback audit rows on compensating rep revert Prototype: inverse TryApplyDelta with rollback deltaId; no delete of failed-forward audit row if append succeeded then race — mirror skill XP revert policy adopted
completionRewardSummary rep fields Defer to NEO-140 — Bruno success asserts accept only, not summary shape adopted
Concurrent rep + standing reads during gate eval Out of scope — accept gate reads standing before chain completion rep in separate requests deferred

NEO-140 (HTTP quest projections) and NEO-141 (telemetry README) follow this router extension landing.