# NEO-140 — E7M3-08: Extend quest HTTP projections for rep + gates **Linear:** [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140) **Branch:** `NEO-140-e7m3-quest-http-rep-gate-projections` **Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-08** **Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Pattern:** [NEO-129](NEO-129-implementation-plan.md) — `completionRewardSummary` nested grant lines from `RewardDeliveryEvent`; [NEO-115](NEO-115-implementation-plan.md) — world GET definition projection **Precursors:** [NEO-138](https://linear.app/neon-sprawl/issue/NEO-138) **`Done`** — `RewardDeliveryEvent.GrantedReputation` at commit time; [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139) **`Done`** — standing GET for client cross-check **Blocks:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) (Godot HUD gate + rep reward labels) **Client counterpart:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) — parses world **`factionGateRules`** and quest-progress **`completionRewardSummary.reputationGrants`**; blocked until this story lands ## Goal Client can see gate requirements and completion rep summary without local math. ## Kickoff clarifications **No clarifications needed.** Linear AC, [E7M3-08 backlog scope](E7M3-pre-production-backlog.md#e7m3-08--extend-quest-http-projections-for-rep-grants--gate-rules), and NEO-129/NEO-138 precedents settle: | Topic | Decision | Evidence | |-------|----------|----------| | **`factionGateRules` JSON shape** | **`{ factionId, minStanding }`** per row — flat mirror of content schema | `content/schemas/faction-gate-rule.schema.json`; `FactionGateRuleRow` | | **`factionGateRules` omission** | **Omit property** when quest has no rules (`JsonIgnore WhenWritingNull`) | Backlog “optional projection”; NEO-115 objective optional-field precedent | | **`factionGateRules` order** | **Preserve catalog load order** from `QuestDefRow.FactionGateRules` | Same as prerequisite ids / step order in world GET | | **Faction display names on gates** | **Not projected** — `factionId` only | NEO-139 deferred `GET /game/world/faction-definitions`; client joins standing GET or future catalog | | **`reputationGrants` in summary** | **Required array** on `completionRewardSummary` when summary is present; **empty `[]`** when delivery had no rep rows | NEO-129: `itemGrants` / `skillXpGrants` always present when summary exists | | **Rep line field names** | **`factionId`**, **`amount`** — mirror content `reputation-grant-row.schema.json` and `RewardReputationGrantApplied` | NEO-138 store snapshot; content schema | | **Rep line source** | **`RewardDeliveryEvent.GrantedReputation`** at commit time (not live standing recompute) | NEO-129 maps delivery event, not bundle definition | | **Rep line order** | **Preserve commit-time grant order** from delivery event | `MapItemGrants` / `MapSkillXpGrants` XML doc precedent | | **Schema version** | **Stay at 1** for both world quest-definitions and quest-progress | Additive optional fields; no breaking rename | | **`QuestAcceptApi`** | **No separate accept-specific changes** — shared `QuestProgressApi.MapQuestProgressRow` / `MapCompletionRewardSummary` picks up rep lines automatically | Backlog “if applicable”; accept embeds progress row via existing mapper | | **Bruno scope** | **Extend** `quest-definitions/Get quest definitions.bru` (grid-contract gate row) + **add** quest-progress GET bru asserting operator-chain **`reputationGrants`** (+15 Grid Operators) | Backlog “Bruno assertions on grid-contract quest row”; closes NEO-138 deferral on summary shape | ## Scope and out-of-scope **In scope (from Linear + backlog):** - Extend **`GET /game/world/quest-definitions`** with optional **`factionGateRules`** per quest. - Extend **`GET …/quest-progress`** **`completionRewardSummary`** with **`reputationGrants`** lines (mirror items/skill XP). - **`QuestAcceptApi`** embedded quest rows inherit updated mapper (no extra accept-only DTO work). - Integration tests + Bruno assertions on **`prototype_quest_grid_contract`** gate row and operator-chain rep summary. - `server/README.md` quest + faction sections. **Out of scope:** - Godot parse/HUD (NEO-142), telemetry ingest (NEO-141). - **`GET /game/world/faction-definitions`** (no backlog item). - Player-specific gate pass/fail on world GET (gates evaluated only on accept; standing via NEO-139 GET). - Enriching accept deny responses with failing rule detail (NEO-137 adopted: `reasonCode` only). **Client counterpart:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) — standing label + gate feedback + reward label rep lines after NEO-139/NEO-140. ## Acceptance criteria checklist - [x] Gated quest definition includes **`factionGateRules`** in world GET. - [x] Completed quest rows include rep lines in **`completionRewardSummary`** when delivered. - [x] `dotnet test` covers grid-contract gate projection and operator-chain rep summary. - [x] Bruno asserts grid-contract **`factionGateRules`** and operator-chain **`reputationGrants`**. ## Implementation reconciliation (shipped) - **World GET:** `QuestDefinitionsWorldApi` maps `QuestDefRow.FactionGateRules` → optional `factionGateRules` on `QuestDefinitionJson` (`QuestFactionGateRuleJson`: `factionId`, `minStanding`); omitted when empty. - **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. - **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 ### 1. World quest definitions — `factionGateRules` **DTOs** (`QuestDefinitionsListDtos.cs`): | Type | JSON | Notes | |------|------|-------| | **`QuestFactionGateRuleJson`** | `factionId`, `minStanding` | New grant-line type | | **`QuestDefinitionJson`** | optional **`factionGateRules`** | `JsonIgnore(WhenWritingNull)`; non-null when `QuestDefRow.FactionGateRules.Count > 0` | **Mapper** (`QuestDefinitionsWorldApi.cs`): - When mapping each `QuestDefRow`, if `d.FactionGateRules.Count > 0`, set `FactionGateRules` to mapped list; else leave null (omitted in JSON). - Preserve rule order from catalog. **Prototype assertion target:** ```json { "id": "prototype_quest_grid_contract", "factionGateRules": [ { "factionId": "prototype_faction_grid_operators", "minStanding": 15 } ] } ``` Non-gated quests (gather intro, operator chain, etc.) omit **`factionGateRules`**. ### 2. Quest progress — `completionRewardSummary.reputationGrants` **DTOs** (`QuestProgressListDtos.cs`): | Type | JSON | Notes | |------|------|-------| | **`QuestReputationGrantJson`** | `factionId`, `amount` | New line type | | **`QuestCompletionRewardSummaryJson`** | add **`reputationGrants`** | Required when summary present; empty array allowed | **Mapper** (`QuestProgressApi.cs`): - Extend `MapCompletionRewardSummary` to map `deliveryEvent.GrantedReputation` → `reputationGrants` via new `MapReputationGrants` helper (commit-time order preserved). - `QuestAcceptApi.MapResponse` unchanged — already calls `QuestProgressApi.MapQuestProgressRow`. **Prototype assertion targets:** | Quest | `reputationGrants` when completed | |-------|----------------------------------| | **`prototype_quest_gather_intro`** | `[]` | | **`prototype_quest_operator_chain`** | `[{ factionId: prototype_faction_grid_operators, amount: 15 }]` | | **`prototype_quest_grid_contract`** | `[{ factionId: prototype_faction_rust_collective, amount: 10 }]` (after hand-in flow lands in tests/Bruno) | ### 3. Docs - **`server/README.md`**: extend quest-definitions table with **`factionGateRules`**; extend quest-progress **`completionRewardSummary`** table with **`reputationGrants`** source (`GrantedReputation`). - On ship: update [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) status line and [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M3 row. ### 4. Flow (read paths only) ```mermaid sequenceDiagram participant Client participant WorldAPI as QuestDefinitionsWorldApi participant ProgressAPI as QuestProgressApi participant Registry as IQuestDefinitionRegistry participant Delivery as IRewardDeliveryStore Client->>WorldAPI: GET /game/world/quest-definitions WorldAPI->>Registry: GetDefinitionsInIdOrder() WorldAPI-->>Client: quests[] (+ factionGateRules on gated rows) Client->>ProgressAPI: GET /game/players/{id}/quest-progress ProgressAPI->>Delivery: TryGet per completed quest ProgressAPI-->>Client: quests[] (+ completionRewardSummary.reputationGrants) ``` ## Files to add | Path | Rationale | |------|-----------| | `bruno/neon-sprawl-server/quest-progress/Get quest progress after operator chain complete.bru` | Bruno spine asserting operator-chain **`completionRewardSummary.reputationGrants`** (+15 Grid Operators); mirrors gather-intro complete bru pattern | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/Quests/QuestDefinitionsListDtos.cs` | Add `QuestFactionGateRuleJson`; optional `factionGateRules` on `QuestDefinitionJson` | | `server/NeonSprawl.Server/Game/Quests/QuestDefinitionsWorldApi.cs` | Map `QuestDefRow.FactionGateRules` into world GET JSON | | `server/NeonSprawl.Server/Game/Quests/QuestProgressListDtos.cs` | Add `QuestReputationGrantJson`; extend `QuestCompletionRewardSummaryJson` | | `server/NeonSprawl.Server/Game/Quests/QuestProgressApi.cs` | Map `GrantedReputation` → `reputationGrants` in `MapCompletionRewardSummary` | | `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionsWorldApiTests.cs` | Assert grid-contract `factionGateRules`; assert non-gated quests omit property | | `server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs` | Extend operator-chain summary helper (+15 rep); extend equality helper; optional grid-contract rep assertion | | `bruno/neon-sprawl-server/quest-definitions/Get quest definitions.bru` | Assert `prototype_quest_grid_contract.factionGateRules` freeze row | | `server/README.md` | Document new projection fields on both GET routes | ## Tests | File | Coverage | |------|----------| | `QuestDefinitionsWorldApiTests.cs` | **Add/change:** grid-contract row includes `[{ factionId: prototype_faction_grid_operators, minStanding: 15 }]`; gather intro row has no `factionGateRules` in deserialized model (null / omitted) | | `QuestProgressApiTests.cs` | **Change:** `AssertOperatorChainCompletionRewardSummary` expects `reputationGrants` +15 Grid Operators; `AssertCompletionRewardSummariesEqual` compares rep lines; gather-intro summary asserts empty `reputationGrants` | | `bruno/.../quest-definitions/Get quest definitions.bru` | Grid-contract gate rule assertion | | `bruno/.../quest-progress/Get quest progress after operator chain complete.bru` | **New** — operator-chain complete → GET asserts rep grant line | Manual verification: run Bruno quest-definitions + new quest-progress bru against local server after operator-chain flow helper. ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | Existing clients ignore unknown `reputationGrants` field | **Additive field only** — `schemaVersion` stays 1; NEO-142 is first consumer | `adopted` | | Grid-contract completion rep Bruno needs full hand-in flow | ~~**Operator-chain Bruno first** (primary AC); grid-contract rep assertion in integration test using existing quest wiring helpers~~ | `adopted` — **done:** integration test + accept bru assert +10 Rust Collective | | `QuestDefinitionsWorldApiTests` missing grid-contract row coverage today | **Add grid-contract gate assertions** in same test method | `adopted` |