# 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 **1–11** 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** - [ ] 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-144–NEO-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)