# NEO-139 — E7M3-07: GET faction-standing HTTP read **Linear:** [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139) **Branch:** `NEO-139-e7m3-get-faction-standing-http-read` **Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-07** **Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Pattern:** [NEO-37](NEO-37-implementation-plan.md) / [NEO-44](NEO-44-implementation-plan.md) — per-player snapshot GET + `schemaVersion`; [NEO-6](NEO-6-implementation-plan.md) — unknown player **404** via position gate **Precursors:** [NEO-135](https://linear.app/neon-sprawl/issue/NEO-135) **`Done`** — `IFactionStandingStore`; [NEO-138](https://linear.app/neon-sprawl/issue/NEO-138) **`Done`** — operator-chain rep grant **+15** via reward router **Blocks:** [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140) (quest HTTP rep projections), [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) (Godot HUD) **Client counterpart:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) — parses **`GET …/faction-standing`**; blocked until this story lands ## Goal Client-readable standing snapshot for all frozen factions. ## Kickoff clarifications **No clarifications needed.** Linear AC, [E7M3-07 backlog scope](E7M3-pre-production-backlog.md#e7m3-07--get-gameplayersidfaction-standing-http-read), and snapshot-read precedents settle: | Topic | Decision | Evidence | |-------|----------|----------| | Unknown player | **404** before building body | Linear AC “position gate precedent”; `SkillProgressionSnapshotApi`, `GigProgressionSnapshotApi`, `PositionStateApi` | | Player existence gate | **`IPositionStateStore.TryGetPosition(trimmedId)`** — not `CanWritePlayer` alone | Same trio of snapshot APIs; position row is the shared “known player” contract | | Path id handling | **Trim** path segment; echo trimmed id in JSON **`playerId`** | Skill/gig snapshot APIs (position GET additionally case-insensitive lookup — snapshot APIs use trim-only + position gate) | | Response shape | **`schemaVersion` 1**, **`playerId`**, **`factions`** array of **`{ id, standing }`** | Backlog “lists each catalog faction with current standing (default 0)”; mirrors skill progression row minimalism (display names deferred — no `GET /game/world/faction-definitions` yet) | | Faction roster | **One row per catalog faction** via **`IFactionDefinitionRegistry.GetDefinitionsInIdOrder()`** | Backlog “all frozen factions”; prototype roster = 2 ids (`PrototypeE7M3FactionCatalogRules`) | | Missing standing row | **`standing: 0`** (clamped) via **`IFactionStandingStore.TryGetStanding`** per faction | NEO-135 store contract; HTTP layer does not reimplement neutral default | | Array order | **Not part of public contract** — consumers key by **`id`** (doc on DTO) | `SkillProgressionSnapshotResponse` XML doc precedent | | Bruno scope | **Default snapshot smoke** + **operator-chain standing assertion (+15 Grid Operators)** | Backlog Bruno folder; closes NEO-138 deferral (“No standing GET until NEO-139”) | | Godot / quest projections | **Out of scope** | E7M3-08 (NEO-140), E7M3-10 (NEO-142) | ## Scope and out-of-scope **In scope (from Linear + backlog):** - **`GET /game/players/{id}/faction-standing`** — `FactionStandingApi` + DTOs in `Game/Factions/`. - Response lists each catalog faction with current standing (default **0** when no row). - Integration tests + Bruno `bruno/neon-sprawl-server/faction-standing/`. - `server/README.md` section. - Optional: extend **`Accept grid contract after operator chain.bru`** docs or add sibling Bruno that GETs standing after chain (assert **+15** on Grid Operators). **Out of scope:** - Godot HUD (NEO-142), quest HTTP rep/gate projections (NEO-140), telemetry ingest (NEO-141). - **`GET /game/world/faction-definitions`** (no backlog item; client can join display names later). - Write/mutation endpoints (standing changes stay on **`ReputationOperations`** / quest reward path). **Client counterpart:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) — standing label + gate feedback after NEO-139/NEO-140. ## Acceptance criteria checklist - [x] GET returns stable JSON for prototype player after quest rep grants. - [x] Unknown player returns **404** (position gate precedent). - [x] `dotnet test` covers default snapshot, post-rep standing, and unknown-player 404. ## Implementation reconciliation (shipped) - **API:** `FactionStandingApi.MapFactionStandingApi` — `GET /game/players/{id}/faction-standing`. - **DTOs:** `FactionStandingSnapshotResponse`, `FactionStandingRowJson` (`schemaVersion` 1). - **Gate:** `IPositionStateStore.TryGetPosition` → **404** for unknown player. - **Tests:** `FactionStandingApiTests` (5 cases: 404 unknown/whitespace-only, trim-path echo, defaults, post-rep); **821** tests green. - **Bruno:** `Get faction standing default.bru`, `unknown player.bru`, `after operator chain.bru`. - **Docs:** `server/README.md` Faction standing read section; E7.M3 module + alignment register updated. ## Technical approach ### 1. DTOs (`Game/Factions/FactionStandingSnapshotDtos.cs`) | Type | JSON | Notes | |------|------|-------| | **`FactionStandingSnapshotResponse`** | `schemaVersion`, `playerId`, `factions` | `CurrentSchemaVersion = 1` | | **`FactionStandingRowJson`** | `id`, `standing` | One row per catalog faction | Document that **`factions` array order is not contractual** — key rows by **`id`**. ### 2. `FactionStandingApi` (`Game/Factions/FactionStandingApi.cs`) ```csharp app.MapGet( "/game/players/{id}/faction-standing", (string id, IPositionStateStore positions, IFactionDefinitionRegistry factionRegistry, IFactionStandingStore standingStore) => { ... }); ``` **Flow:** 1. `trimmedId = id.Trim()`; if empty or `!positions.TryGetPosition(trimmedId, out _)` ⇒ **`Results.NotFound()`**. 2. **`BuildSnapshot(trimmedId, factionRegistry, standingStore)`**: - Iterate **`factionRegistry.GetDefinitionsInIdOrder()`**. - For each definition, **`standingStore.TryGetStanding(playerId, definition.Id)`** — on success use **`outcome.Standing`**; store deny for catalog faction should be unreachable (defense: skip/500 only if violated — prefer **`Debug.Assert`** in dev; integration tests use real catalog). 3. Return **`Results.Json(response)`**. **`BuildSnapshot`** is **`internal static`** for unit/integration reuse (mirror **`SkillProgressionSnapshotApi.BuildSnapshot`**). ### 3. Wiring | Site | Change | |------|--------| | **`Program.cs`** | **`app.MapFactionStandingApi()`** after faction standing store registration (near skill/gig snapshot maps) | ### 4. Prototype test scenarios | Scenario | Setup | Expected | |----------|-------|----------| | Fresh dev player | GET **`dev-local-1`** | **200**; **`schemaVersion` 1**; **2** factions; both **`standing: 0`** | | Unknown player | GET **`missing-player`** | **404** | | After operator-chain rep | Deliver operator-chain completion via **`RewardRouterOperations`** or HTTP quest flow helper | Grid Operators **`standing: 15`**; Rust Collective **`standing: 0`** | | Case on path | GET with trimmed id | **200**; body **`playerId`** echoes trimmed path (skill progression precedent) | Constants: **`PrototypeE7M3FactionCatalogRules.ExpectedFactionIds`**, **`PrototypeE7M3QuestFactionRules.GridContractGateFactionId`**. ### 5. Bruno (`bruno/neon-sprawl-server/faction-standing/`) | Request | seq | Role | |---------|-----|------| | **`Health after faction standing store load.bru`** | 1 | Keep existing health smoke (NEO-135) | | **`Get faction standing default.bru`** | 2 | **200**; 2 factions; all **`standing: 0`** on fresh dev player (document restart/reset) | | **`Get faction standing unknown player.bru`** | 3 | **404** | | **`Get faction standing after operator chain.bru`** | 4 | Pre-request **`operator-chain-quest-flow-helper`**; assert Grid Operators **15**, Rust Collective **0** | Update **`Accept grid contract after operator chain.bru`** docs to reference standing GET sibling at seq **4+**. ### 6. README Add **`## Faction standing read (NEO-139)`** after standing store section: - Route, sample JSON (defaults + post-chain example). - Unknown player **404**; join display names from content client-side until world API exists. - Cross-link NEO-142 client consumer. ## Files to add | Path | Purpose | |------|---------| | `server/NeonSprawl.Server/Game/Factions/FactionStandingSnapshotDtos.cs` | Response + row JSON types | | `server/NeonSprawl.Server/Game/Factions/FactionStandingApi.cs` | Route map + `BuildSnapshot` | | `server/NeonSprawl.Server.Tests/Game/Factions/FactionStandingApiTests.cs` | Integration tests (404, defaults, post-rep) | | `bruno/neon-sprawl-server/faction-standing/Get faction standing default.bru` | Default snapshot smoke | | `bruno/neon-sprawl-server/faction-standing/Get faction standing unknown player.bru` | 404 smoke | | `bruno/neon-sprawl-server/faction-standing/Get faction standing after operator chain.bru` | +15 assertion after HTTP quest flow | ## Files to modify | Path | Change | |------|--------| | `server/NeonSprawl.Server/Program.cs` | Register **`MapFactionStandingApi()`** | | `server/README.md` | Faction standing GET section + curl example | | `bruno/neon-sprawl-server/quest-progress/Accept grid contract after operator chain.bru` | Doc cross-link to standing GET Bruno | | `docs/decomposition/modules/E7_M3_FactionReputationLedger.md` | Status line: E7M3-07 plan kicked off (update to **landed** when shipped) | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M3 row note when shipped | ## Tests | File | Coverage | |------|----------| | **`FactionStandingApiTests`** | Unknown player **404**; default **200** with 2 factions at **0**; after **`RewardRouterOperations`** operator-chain delivery Grid Operators **15** | | **CI** | Existing **`dotnet test`** green; Bruno collection optional in local/CI per repo convention | Manual QA: **none** (server HTTP read; client verification deferred to NEO-142 / NEO-143). ## Implementation order 1. DTOs + **`FactionStandingApi.BuildSnapshot`**. 2. **`Program.cs`** map. 3. **`FactionStandingApiTests`** (AAA layout). 4. Bruno requests. 5. **`server/README.md`** + decomposition alignment on ship. ## Open questions / risks | Item | Agent recommendation | Status | |------|---------------------|--------| | Store read deny for catalog faction | **Should not happen** — if it does, treat as **500** or omit row (prefer **500** to surface misconfiguration) | `adopted` — use standing outcome; test happy path only | | Postgres mode Bruno | **Same assertions** on dev player after operator-chain flow; document DB persistence like skill progression Bruno | `adopted` | | Extend grid-contract accept Bruno with inline GET | **Separate Bruno file** in `faction-standing/` — keeps quest-progress folder focused | `adopted` | ## Client counterpart [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) — **`faction_standing_client.gd`** parses this route; requires NEO-140 for gate/rep quest row projections.