10 KiB
NEO-139 — E7M3-07: GET faction-standing HTTP read
Linear: NEO-139
Branch: NEO-139-e7m3-get-faction-standing-http-read
Backlog: E7M3-pre-production-backlog.md — E7M3-07
Module: E7_M3_FactionReputationLedger.md
Pattern: NEO-37 / NEO-44 — per-player snapshot GET + schemaVersion; NEO-6 — unknown player 404 via position gate
Precursors: NEO-135 Done — IFactionStandingStore; NEO-138 Done — operator-chain rep grant +15 via reward router
Blocks: NEO-140 (quest HTTP rep projections), NEO-142 (Godot HUD)
Client counterpart: 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, 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 inGame/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.mdsection.- Optional: extend
Accept grid contract after operator chain.brudocs 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 — 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 testcovers 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)
app.MapGet(
"/game/players/{id}/faction-standing",
(string id, IPositionStateStore positions,
IFactionDefinitionRegistry factionRegistry,
IFactionStandingStore standingStore) => { ... });
Flow:
trimmedId = id.Trim(); if empty or!positions.TryGetPosition(trimmedId, out _)⇒Results.NotFound().BuildSnapshot(trimmedId, factionRegistry, standingStore):- Iterate
factionRegistry.GetDefinitionsInIdOrder(). - For each definition,
standingStore.TryGetStanding(playerId, definition.Id)— on success useoutcome.Standing; store deny for catalog faction should be unreachable (defense: skip/500 only if violated — preferDebug.Assertin dev; integration tests use real catalog).
- Iterate
- 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
- DTOs +
FactionStandingApi.BuildSnapshot. Program.csmap.FactionStandingApiTests(AAA layout).- Bruno requests.
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 — faction_standing_client.gd parses this route; requires NEO-140 for gate/rep quest row projections.