NEO-139: kickoff implementation plan for faction-standing GET API.
parent
e405d43d43
commit
5fc780c8ab
|
|
@ -0,0 +1,171 @@
|
|||
# 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
|
||||
|
||||
- [ ] GET returns stable JSON for prototype player after quest rep grants.
|
||||
- [ ] Unknown player returns **404** (position gate precedent).
|
||||
- [ ] `dotnet test` covers default snapshot, post-rep standing, and unknown-player 404.
|
||||
|
||||
## 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.
|
||||
Loading…
Reference in New Issue