NEO-144: add E7M4 Slice 4 pre-production backlog

Land NEO-144–NEO-154 story order, freeze table, and cross-link from E7M3 downstream.
pull/184/head
VinPropane 2026-06-20 19:15:56 -04:00
parent 53122f9f2a
commit 4238038e11
2 changed files with 383 additions and 1 deletions

View File

@ -50,7 +50,7 @@ Working backlog for **Epic 7 — Slice 3** ([faction ledger](../decomposition/ep
| 10 | **E7M3-10** | client | E7M3-07, E7M3-08 |
| 11 | **E7M3-11** | client | E7M3-10 |
**Downstream (separate modules):** [E7.M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) contract generator; [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) **`TravelRule`** faction gates; [E8.M2](../decomposition/modules/E8_M2_GuildCorpProgressionState.md) guild/corp rep coupling.
**Downstream (separate modules):** [E7.M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) contract generator — [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) ([NEO-144](https://linear.app/neon-sprawl/issue/NEO-144)[NEO-154](https://linear.app/neon-sprawl/issue/NEO-154)); [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) **`TravelRule`** faction gates; [E8.M2](../decomposition/modules/E8_M2_GuildCorpProgressionState.md) guild/corp rep coupling.
---

View File

@ -0,0 +1,382 @@
# 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**
- [ ] PR gate validates contract template JSON.
- [ ] Exactly one prototype template id with encounter + bundle cross-refs.
- [ ] 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**
- [ ] Host exits on invalid contract catalog or broken encounter refs at startup.
- [ ] 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**
- [ ] Hook sites documented in README; no behavior change beyond comments.
**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)