diff --git a/docs/decomposition/modules/E7_M4_ContractMissionGenerator.md b/docs/decomposition/modules/E7_M4_ContractMissionGenerator.md index 61e8ff4..bfc7d6d 100644 --- a/docs/decomposition/modules/E7_M4_ContractMissionGenerator.md +++ b/docs/decomposition/modules/E7_M4_ContractMissionGenerator.md @@ -57,6 +57,8 @@ Epic 7 **Slice 4** — `contract_issued`, `contract_complete`, reward anomalies. **Runtime stores (NEO-146):** per-player **`IContractInstanceStore`** (one active instance) + append-only **`IContractOutcomeStore`**; [server README — Contract instance store](../../../server/README.md#contract-instance-store-neo-146). +**Generator operations (NEO-147):** **`ContractGeneratorOperations.TryIssue`** — template selection, deterministic instance ids, structured deny codes; [server README — Contract generator operations](../../../server/README.md#contract-generator-operations-neo-147). + ## Source anchors - Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 7. diff --git a/docs/plans/NEO-147-implementation-plan.md b/docs/plans/NEO-147-implementation-plan.md index f706feb..e7bfdd2 100644 --- a/docs/plans/NEO-147-implementation-plan.md +++ b/docs/plans/NEO-147-implementation-plan.md @@ -62,9 +62,16 @@ Issue a validated **`active`** contract instance from **`ContractSeed`** inputs ## Acceptance criteria checklist -- [ ] Issue creates **`active`** instance with correct encounter binding. -- [ ] Second issue while active returns structured deny. -- [ ] `dotnet test` covers generator gates. +- [x] Issue creates **`active`** instance with correct encounter binding. +- [x] Second issue while active returns structured deny. +- [x] `dotnet test` covers generator gates. + +## Implementation reconciliation (shipped) + +- **Types:** `ContractGeneratorReasonCodes`, `ContractIssueOperationResult`, `ContractInstanceIds.MakeDeterministicInstanceId`. +- **Orchestrator:** `ContractGeneratorOperations.TryIssue` — band + faction template selection, deterministic id, store persist. +- **Tests:** `ContractGeneratorOperationsTests` (11 unit), `ContractGeneratorOperationsIntegrationTests` (DI), `ContractGeneratorPersistenceIntegrationTests` (`RequirePostgresFact`); **888** tests green. +- **Docs:** `server/README.md` contract generator section; `E7_M4_ContractMissionGenerator.md` NEO-147 note. ## Technical approach diff --git a/server/README.md b/server/README.md index ce67670..5b39d9e 100644 --- a/server/README.md +++ b/server/README.md @@ -282,10 +282,38 @@ Per-player contract runtime state lives in **`IContractInstanceStore`**, keyed b - **`TryMarkComplete`** — first completion returns `true`; replay returns `false` without changing **`completedAt`**. - **`TryClearInstance`** — dev fixture only; removes one instance row. -Completed instances are immutable (no status regression). Player, template, and instance ids are normalized (trim + lowercase for player/template/instance; seed bucket trim only). Template id validation against **`IContractTemplateRegistry`** is deferred to NEO-147. +Completed instances are immutable (no status regression). Player, template, and instance ids are normalized (trim + lowercase for player/template/instance; seed bucket trim only). Template id validation and selection run in **`ContractGeneratorOperations`** (NEO-147). **Storage:** in-memory singleton when **`ConnectionStrings:NeonSprawl`** is unset (seeds configured dev player only). When Postgres is configured, **`PostgresContractInstanceStore`** persists to **`contract_instance`** ([`V011__contract_instance.sql`](../db/migrations/V011__contract_instance.sql)) with a partial unique index enforcing one active row per player. Plan: [NEO-146 implementation plan](../../docs/plans/NEO-146-implementation-plan.md). +## Contract generator operations (NEO-147) + +**`ContractGeneratorOperations.TryIssue`** issues an **`active`** contract instance from seed inputs + player context. Call this orchestrator (or future HTTP wrapper) — not **`IContractInstanceStore.TryCreateActive`** directly — so template selection, faction gates, and deterministic ids stay centralized. + +**Inputs:** `playerId`, optional `templateId`, `seedBucket`, optional `zoneDifficultyBand` (defaults to **1**). + +**Template selection:** + +- **Explicit `templateId`:** registry lookup; deny **`unknown_template`** when missing; deny **`no_eligible_template`** when band or faction standing floor fails. +- **Omitted/empty `templateId`:** first template from **`GetDefinitionsInIdOrder()`** matching band + standing. +- Faction floor uses **`FactionGateOperations`** on the template **`minFactionStanding`** row. + +**Deterministic instance id:** **`ContractInstanceIds.MakeDeterministicInstanceId`** — SHA-256 of `{playerId}|{templateId}|{seedBucket}` (normalized) → **`ci_{32 lowercase hex}`**. + +**Reason codes:** + +| Code | When | +|------|------| +| **`invalid_ids`** | Empty player or seed bucket after normalize | +| **`player_not_writable`** | Player not in dev bucket / Postgres `player_position` | +| **`active_contract_exists`** | Player already has an active instance | +| **`unknown_template`** | Explicit template id not in registry | +| **`no_eligible_template`** | No template matches band + standing filters | + +**Result:** **`ContractIssueOperationResult`** — `success`, optional `reasonCode`, optional **`ContractInstanceState`** snapshot, **`encounterTemplateId`** from the selected template. + +Economy band-cap lint at issue time lands in **NEO-150** (E7M4-07). HTTP **`POST …/contracts/issue`** lands in **NEO-151** (E7M4-08). Plan: [NEO-147 implementation plan](../../docs/plans/NEO-147-implementation-plan.md). + ## Contract outcome store (NEO-146) Append-only contract completion audit rows live in **`IContractOutcomeStore`**. Each **`ContractOutcomeRow`** captures instance id, delivery idempotency key **`{playerId}:contract_complete:{contractInstanceId}`**, grant snapshot summary (items, skill XP, reputation — same shapes as **`RewardDeliveryEvent`**), and **`recordedAt`**.