neon-sprawl/docs/plans/E7M4-pre-production-backlog.md

385 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# E7.M4 — Pre-production story backlog (ContractMissionGenerator)
Working backlog for **Epic 7 — Slice 4** ([contract generator](../decomposition/epics/epic_07_quest_faction.md#slice-4---contract-generator-pre-production)). Decomposition and contracts: [E7.M4 — ContractMissionGenerator](../decomposition/modules/E7_M4_ContractMissionGenerator.md).
**Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E7M3 (paired server+client)](../plans/E7M3-pre-production-backlog.md), [E7M2 (reward capstone)](../plans/E7M2-prototype-backlog.md).
**Labels (Linear):** every issue **`E7.M4`**; add **`server`** or **`client`** + **`Story`** as listed.
**Precursor (do not re-scope):** [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) **Ready** — static quest catalog + accept/complete ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)). [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) **Ready** — idempotent **`completionRewardBundle`** delivery ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)[NEO-132](https://linear.app/neon-sprawl/issue/NEO-132)). [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Ready** — faction standing + quest gates ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)[NEO-143](https://linear.app/neon-sprawl/issue/NEO-143)). [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) **Ready****`prototype_combat_pocket`** encounter clear ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)[NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)). Slice 4 **adds repeatable contract instances**; does not migrate static quests or first-time encounter loot.
**Upstream (must be landed):** E7.M1 quest state; E7.M2 **`RewardRouterOperations`** + **`IRewardDeliveryStore`**; E7.M3 **`IFactionStandingStore`**; E5.M3 encounter progress/completion. [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md) solo idempotency.
**E4.M1 note (zone difficulty):** Epic 7 Slice 4 AC keys contracts by **zone difficulty band**. Full [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) zone graph is **not** a Slice 4 blocker — prototype uses **content-only `zoneDifficultyBand`** on templates and optional **`zoneDifficultyBand`** on issue POST until E4.M1 **`GET …/zone`** lands; replace stub input with live zone read when E4.M1 Slice 1 lands.
**Prototype contract spine (frozen in E7M4-01):** **one** frozen **`ContractTemplate`** id; **one** encounter objective (**`prototype_combat_pocket`**); **one** scaled repeat payout bundle; issuance filtered by **zone difficulty band** + optional **faction standing** floor.
**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts.
| Slug | Layer | Linear |
|------|-------|--------|
| E7M4-01 | server | [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144) |
| E7M4-02 | server | [NEO-145](https://linear.app/neon-sprawl/issue/NEO-145) |
| E7M4-03 | server | [NEO-146](https://linear.app/neon-sprawl/issue/NEO-146) |
| E7M4-04 | server | [NEO-147](https://linear.app/neon-sprawl/issue/NEO-147) |
| E7M4-05 | server | [NEO-148](https://linear.app/neon-sprawl/issue/NEO-148) |
| E7M4-06 | server | [NEO-149](https://linear.app/neon-sprawl/issue/NEO-149) |
| E7M4-07 | server | [NEO-150](https://linear.app/neon-sprawl/issue/NEO-150) |
| E7M4-08 | server | [NEO-151](https://linear.app/neon-sprawl/issue/NEO-151) |
| E7M4-09 | server | [NEO-152](https://linear.app/neon-sprawl/issue/NEO-152) |
| E7M4-10 | client | [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153) |
| E7M4-11 | client | [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154) |
**Dependency graph in Linear:** E7M4-02 blocked by E7M4-01. E7M4-03 blocked by E7M4-02. E7M4-04 blocked by E7M4-03. E7M4-05 blocked by E7M4-02. E7M4-06 blocked by E7M4-04 and E7M4-05. E7M4-07 blocked by E7M4-04. E7M4-08 blocked by E7M4-06 and E7M4-07. E7M4-09 blocked by E7M4-04 and E7M4-08. E7M4-10 blocked by E7M4-08 and E7M4-09. E7M4-11 blocked by E7M4-10.
**Board order:** estimates **111** matching slug order (E7M4-01 = 1 … E7M4-11 = 11). On the Epic 7 project board, sort by **Estimate** (ascending).
---
## Story order (recommended)
| Order | Slug | Layer | Depends on |
|-------|------|-------|------------|
| 1 | **E7M4-01** | server | E7.M2 bundle schemas; E5.M3 encounter freeze |
| 2 | **E7M4-02** | server | E7M4-01 |
| 3 | **E7M4-03** | server | E7M4-02 |
| 4 | **E7M4-04** | server | E7M4-03 |
| 5 | **E7M4-05** | server | E7M4-02 |
| 6 | **E7M4-06** | server | E7M4-04, E7M4-05 |
| 7 | **E7M4-07** | server | E7M4-04 |
| 8 | **E7M4-08** | server | E7M4-06, E7M4-07 |
| 9 | **E7M4-09** | server | E7M4-04, E7M4-08 |
| 10 | **E7M4-10** | client | E7M4-08, E7M4-09 |
| 11 | **E7M4-11** | client | E7M4-10 |
**Downstream (separate modules):** [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) live zone id for issuance input; [E6.M4](../decomposition/modules/E6_M4_RewardParityEnforcer.md) full parity map; [E3.M5](../decomposition/modules/E3_M5_EconomyBalancePolicy.md) faucet/sink dashboards.
---
## Kickoff decisions (decomposition defaults)
| Topic | Decision | Rationale |
|-------|----------|-----------|
| Contract vs quest | **Separate `ContractInstance` state** — not new static `QuestDef` rows | Repeatable midgame jobs without polluting onboarding catalog |
| Template count | **One** frozen template id | Readable Slice 4 capstone; generator API still general |
| Objective kind (v1) | **`encounter_clear`** — bind to E5.M3 **`encounterId`** | Reuses combat pocket loop players already know |
| Zone difficulty | **Content `zoneDifficultyBand` on template** + optional POST override until E4.M1 | Epic AC without blocking on travel graph |
| Faction filter | **Optional `minFactionStanding` on template** — prototype **0** (open) | Standing-based template pick deferred to second template in follow-on |
| Issuance cap | **One active contract per player** (prototype) | Avoid HUD/stack complexity in Slice 4 |
| Seed / determinism | **`ContractSeed`**: `playerId` + `templateId` + `seedBucket` (date or client counter) → deterministic **`contractInstanceId`** suffix for audit | Supports daily rotation without full proc-gen |
| Completion payout | **`completionRewardBundle`** on template — deliver via extended **`RewardRouterOperations`** | Reuse E7.M2 idempotency + inventory/XP/rep apply |
| Idempotency key | **`{playerId}:contract_complete:{contractInstanceId}`** | Per-instance repeat grants; distinct from quest and encounter keys |
| Encounter loot | **Unchanged** — first-time **`prototype_combat_pocket_clear`** idempotent per player | Contract payout is **additional** repeat incentive, not double first-clear |
| Economy validation | **Prototype band caps** in content + issue-time lint (not full E6.M4) | Slice 4 AC: generated contracts validate against economy rules |
| Party mode | **Solo only** | E8.M1 not landed |
| HUD fidelity | Prototype labels / debug panels | NEO-122 / NEO-142 precedent |
### Prototype contract template freeze (E7M4-01)
| `ContractTemplate.id` | `zoneDifficultyBand` | `encounterTemplateId` | `minFactionStanding` | `completionRewardBundle` |
|-----------------------|----------------------|------------------------|----------------------|---------------------------|
| **`prototype_contract_clear_combat_pocket`** | **1** | **`prototype_combat_pocket`** | **`prototype_faction_grid_operators`** **0** | **`scrap_metal_bulk` ×5**; **`salvage`** **15** (`mission_reward`) |
**Band policy (E7M4-07):** difficulty band **1** caps: max **10** per item grant row, max **25** skill XP per grant, max **10** rep per grant (enforced at issue + CI).
**Issuance:** `POST …/contracts/issue` with optional **`zoneDifficultyBand`** (default **1** for prototype district). Server selects templates where `zoneDifficultyBand` matches and player standing ≥ `minFactionStanding`. Returns **`contractInstanceId`**, template summary, objective encounter id, status **`active`**.
**Completion:** When **`EncounterCompletionOperations`** marks **`prototype_combat_pocket`** complete for a player with matching **active** contract instance → **`ContractCompletionOperations`** delivers bundle → status **`completed`** → record **`ContractOutcome`**.
---
### E7M4-01 — ContractTemplate catalog + schemas + CI
**Goal:** Lock contract content shape and CI validation before server load.
**In scope**
- `content/schemas/contract-template.schema.json`, `content/schemas/contract-seed.schema.json` (issue request / audit shape).
- Reuse **`quest-reward-bundle`** / **`reputation-grant-row`** `$ref`s for **`completionRewardBundle`** on templates.
- `content/contracts/prototype_contract_templates.json` — one frozen template row per freeze table.
- `scripts/validate_content.py`: contract schema validation, encounter cross-ref, item/skill/faction cross-refs, band-cap lint, single-template allowlist.
- Designer note in [E7_M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) + `content/README.md`.
**Out of scope**
- Server loader, stores, operations, HTTP, Godot.
**Acceptance criteria**
- [x] PR gate validates contract template JSON.
- [x] Exactly one prototype template id with encounter + bundle cross-refs.
- [x] Bundle exceeding band caps fails CI.
**Client counterpart:** none (infrastructure-only).
---
### E7M4-02 — Server contract template catalog load (fail-fast)
**Goal:** Disk → host: startup load of `content/contracts/*_contract_templates.json` with CI-parity validation.
**In scope**
- Loader + registry types under `server/NeonSprawl.Server/Game/Contracts/`.
- **`IContractTemplateRegistry`** + DI registration.
- Cross-check **`encounterTemplateId`** vs **`IEncounterDefinitionRegistry`** at startup.
- Unit tests (AAA) for happy path and broken encounter refs.
- `server/README.md` section.
**Out of scope**
- Per-player instance store, generator, HTTP.
**Acceptance criteria**
- [x] Host exits on invalid contract catalog or broken encounter refs at startup.
- [x] Registry resolves template metadata by id.
**Client counterpart:** none (infrastructure-only).
---
### E7M4-03 — Contract instance + outcome stores
**Goal:** Durable per-player **`ContractInstance`** (active/completed) and **`ContractOutcome`** audit records.
**In scope**
- **`IContractInstanceStore`**, **`IContractOutcomeStore`** — in-memory implementations; optional Postgres migrations (NEO-116-style split).
- Instance row: `contractInstanceId`, `templateId`, `playerId`, `status` (`active` | `completed` | `expired`), `seedBucket`, `issuedAt`, optional `completedAt`.
- Outcome row: instance id, delivery idempotency key, grant snapshot summary.
- Unit tests: issue, complete, idempotent complete replay.
- `server/README.md` store sections.
**Out of scope**
- Generator logic (E7M4-04), payout apply (E7M4-05).
**Acceptance criteria**
- [ ] At most one **`active`** instance per player (prototype policy).
- [ ] Completed instances immutable; outcome append-only.
**Client counterpart:** none (infrastructure-only).
---
### E7M4-04 — `ContractGeneratorOperations.TryIssue`
**Goal:** Issue a validated contract instance from **`ContractSeed`** + player context.
**In scope**
- **`ContractGeneratorOperations.TryIssue`** in `server/NeonSprawl.Server/Game/Contracts/`.
- Inputs: `playerId`, optional `templateId`, `seedBucket`, `zoneDifficultyBand` (default 1).
- Template selection: match band + faction standing floor; fail **`no_eligible_template`** when none.
- Deny **`active_contract_exists`** when player already has active instance.
- Deterministic `contractInstanceId` from seed hash + player + template.
- Unit + integration tests (AAA): happy issue, deny paths, unknown template fail-closed.
**Out of scope**
- HTTP (E7M4-08), encounter completion wiring (E7M4-06).
**Acceptance criteria**
- [ ] Issue creates **`active`** instance with correct encounter binding.
- [ ] Second issue while active returns structured deny.
**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153) (blocked by E7M4-08).
---
### E7M4-05 — Extend `RewardRouterOperations` for contract completion bundles
**Goal:** Contract completion payouts reuse E7.M2 delivery idempotency with contract-specific keys.
**In scope**
- **`RewardRouterOperations.TryDeliverContractCompletion`** — same grant kinds as quest completion (items, skill XP, rep) with key **`{playerId}:contract_complete:{contractInstanceId}`**.
- Extend **`IRewardDeliveryStore`** / **`RewardDeliveryEvent`** for contract source kind (`contract_completion`).
- Compensating rollback policy mirrors E7.M2 quest path.
- Integration tests: first delivery, idempotent replay, unknown item/faction deny.
- `server/README.md` reward router section.
**Out of scope**
- Encounter trigger (E7M4-06), HTTP projections (E7M4-08).
**Acceptance criteria**
- [ ] First-time contract complete applies bundle exactly once per instance id.
- [ ] Replay returns **`already_delivered`** without double grant.
**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153).
---
### E7M4-06 — `ContractCompletionOperations` + encounter wiring
**Goal:** Encounter clear completes matching active contract and triggers payout.
**In scope**
- **`ContractCompletionOperations.TryCompleteOnEncounterClear`** — match **`encounterTemplateId`** on active instance.
- Wire from **`EncounterCompletionOperations`** / **`EncounterCombatWiring`** success path (after encounter completion commit, before or after encounter loot — document order: encounter loot first, contract payout second).
- Mark instance **`completed`**; record **`ContractOutcome`**; invoke **`TryDeliverContractCompletion`**.
- Integration tests: issue → encounter clear → contract completed + delivery; clear without contract unchanged; wrong encounter no-op.
- Bruno smoke: issue → encounter-progress clear → contract GET shows completed.
**Out of scope**
- Godot HUD (E7M4-10).
**Acceptance criteria**
- [ ] Active contract completes when bound encounter clears.
- [ ] Encounter first-time loot idempotency unchanged (E5.M3).
**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153), [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154).
---
### E7M4-07 — Economy validation lint at issue time
**Goal:** Slice 4 AC — generated/issued contracts validate against prototype economy caps.
**In scope**
- **`ContractEconomyValidation.TryValidateTemplate`** — band caps from content policy (item qty, skill XP, rep per grant row).
- Call from **`TryIssue`** before instance persist; deny **`economy_cap_exceeded`** fail-closed.
- Cross-ref unknown item/skill/faction ids (registry lookups).
- Unit tests: at-cap pass, over-cap deny, unknown id deny.
- Document parity stub toward E6.M4 / E3.M5 in module doc (full map deferred).
**Out of scope**
- Full **`RewardParityMap`** (E6.M4); live ops dashboards (E9.M2).
**Acceptance criteria**
- [ ] Template at freeze caps issues successfully.
- [ ] Tampered catalog row over caps fails at startup (CI) and cannot issue if slipped through.
**Client counterpart:** none (server policy).
---
### E7M4-08 — Contract issue POST + per-player contract GET
**Goal:** Client-readable contract issuance and state without local math.
**In scope**
- **`POST /game/players/{id}/contracts/issue`** — `ContractIssueApi` + DTOs.
- **`GET /game/players/{id}/contracts`** — active + recent completed instances with template summary, objective encounter id, **`completionRewardSummary`** when delivered.
- Integration tests + Bruno `bruno/neon-sprawl-server/contracts/`.
- `server/README.md` sections.
**Out of scope**
- Godot parse/HUD (E7M4-10).
**Acceptance criteria**
- [ ] POST issue returns **`active`** instance JSON.
- [ ] GET lists active contract with encounter objective id.
- [ ] GET completed row includes delivery summary after encounter clear.
**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153).
---
### E7M4-09 — Contract telemetry hook sites (comment-only)
**Goal:** Anchor future E9.M1 catalog events without ingest.
**In scope**
- Comment-only **`contract_issued`** in **`ContractGeneratorOperations.TryIssue`** (successful issue).
- Comment-only **`contract_complete`** in **`ContractCompletionOperations`** (successful complete + delivery).
- Comment-only **`reward_anomaly`** stub at economy validation deny + delivery rollback paths.
- `server/README.md` hook list.
**Out of scope**
- E9.M1 ingest, dashboards.
**Acceptance criteria**
- [x] Hook sites documented in README; no behavior change beyond comments.
**Landed (NEO-152):** comment-only **`contract_issued`**, **`contract_complete`**, and **`reward_anomaly`** hooks; [server README — Contract telemetry hooks (NEO-152)](../../server/README.md#contract-telemetry-hooks-neo-152); plan [NEO-152](../../plans/NEO-152-implementation-plan.md).
**Client counterpart:** none (infrastructure-only).
---
### E7M4-10 — Client contract issue + progress HUD (Godot)
**Goal:** Player can issue and track an active contract in Godot without Bruno.
**In scope**
- **`contract_client.gd`**, **`ContractActiveLabel`**, **`ContractIssueFeedbackLabel`** (or extend quest HUD section).
- **`POST …/contracts/issue`** on agreed binding (e.g. **Shift+C**); **`GET …/contracts`** on boot + after encounter-complete refresh (reuse **`encounter_progress_client.gd`** signal or quest HUD controller pattern).
- Surface issue denies (`active_contract_exists`, `no_eligible_template`, `economy_cap_exceeded`) with readable copy.
- GdUnit parse tests; `client/README.md` section.
- `docs/manual-qa/NEO-153.md` component checklist.
**Out of scope**
- Final contract board UI; zone picker (E4.M1).
**Acceptance criteria**
- [ ] Active contract visible with encounter objective id after issue.
- [ ] Completed contract shows reward summary line on HUD.
**Server counterpart:** [NEO-150](https://linear.app/neon-sprawl/issue/NEO-150), [NEO-152](https://linear.app/neon-sprawl/issue/NEO-152).
---
### E7M4-11 — Playable repeatable contract capstone (Godot)
**Goal:** Prove Epic 7 Slice 4 acceptance **in Godot**: issue contract, clear combat pocket, receive repeat payout; economy validation holds.
**In scope**
- **`docs/manual-qa/NEO-154.md`**: single-session capstone — extend [NEO-143](../manual-qa/NEO-143.md) or [NEO-111](../manual-qa/NEO-111.md) encounter path with **Shift+C** issue → clear pocket → contract reward **`scrap_metal_bulk` ×5** + salvage XP → re-issue after complete → second full loop.
- **`client/README.md`**: **End-to-end repeatable contract loop (NEO-154)** section; cross-links NEO-144NEO-153 and E5.M3 encounter capstone.
- Module alignment on completion: [E7_M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md), [module_dependency_register.md](../decomposition/modules/module_dependency_register.md).
**Out of scope**
- Multi-template rotation; E4.M1 zone travel; Bruno-only proof.
**Acceptance criteria**
- [ ] Human completes **`docs/manual-qa/NEO-154.md`** with server + client.
- [ ] Epic 7 Slice 4 AC: issued contract validates economy caps; repeat completion idempotent per instance.
- [ ] Re-read [epic_07 Slice 4 AC](../decomposition/epics/epic_07_quest_faction.md#slice-4---contract-generator-pre-production).
**Client counterpart:** this story **is** the Slice 4 client capstone.
---
## Decomposition complete checklist
- [x] Every player-visible AC has a **`client`** Linear issue (NEO-153, NEO-154)
- [x] No forbidden deferral language in backlog
- [x] Capstone client story exists (E7M4-11 / NEO-154)
- [x] Epic DoD updated for Godot verification (see epic_07 update)
- [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M4 row updated (decomposition pass)
- [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M4 note updated
## Related docs
- [epic_07_quest_faction.md](../decomposition/epics/epic_07_quest_faction.md)
- [E7M1-prototype-backlog.md](E7M1-prototype-backlog.md)
- [E7M2-prototype-backlog.md](E7M2-prototype-backlog.md)
- [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md)
- [E5M3-prototype-backlog.md](E5M3-prototype-backlog.md)
- [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md)
- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md)