neon-sprawl/docs/plans/NEO-139-implementation-plan.md

11 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.mdE7M3-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 DoneIFactionStandingStore; 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-standingFactionStandingApi + 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 — 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.

Implementation reconciliation (shipped)

  • API: FactionStandingApi.MapFactionStandingApiGET /game/players/{id}/faction-standing.
  • DTOs: FactionStandingSnapshotResponse, FactionStandingRowJson (schemaVersion 1).
  • Gate: IPositionStateStore.TryGetPosition404 for unknown player.
  • Tests: FactionStandingApiTests (3 cases); 819 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)

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-142faction_standing_client.gd parses this route; requires NEO-140 for gate/rep quest row projections.