From 599d87bd0d191045843fb1316bfa656fd66c0902 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 15 Jun 2026 22:03:33 -0400 Subject: [PATCH] NEO-138: Add E7M3-06 implementation plan for reputationGrants router wiring. --- docs/plans/NEO-138-implementation-plan.md | 198 ++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/plans/NEO-138-implementation-plan.md diff --git a/docs/plans/NEO-138-implementation-plan.md b/docs/plans/NEO-138-implementation-plan.md new file mode 100644 index 0000000..f728756 --- /dev/null +++ b/docs/plans/NEO-138-implementation-plan.md @@ -0,0 +1,198 @@ +# NEO-138 — E7M3-06: Extend RewardRouterOperations for reputationGrants + +**Linear:** [NEO-138](https://linear.app/neon-sprawl/issue/NEO-138) +**Branch:** `NEO-138-e7m3-reward-router-reputation-grants` +**Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-06** +**Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) +**Pattern:** [NEO-127](NEO-127-implementation-plan.md) — grant order + compensating rollback + `TryRecord` race mirror; [NEO-136](NEO-136-implementation-plan.md) — `ReputationOperations.TryApplyDelta` orchestration +**Precursor:** [NEO-136](https://linear.app/neon-sprawl/issue/NEO-136) **`Done`** — `ReputationOperations.TryApplyDelta` (**landed on `main`**) +**Blocks:** [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140) (HTTP quest projections for rep + gates) +**Client counterpart:** [NEO-142](https://linear.app/neon-sprawl/issue/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](https://linear.app/neon-sprawl/issue/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`). + +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_contract`** ⇒ **`accepted: 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.