From 039542aa749cf2dfdb81bb65534253db50d79b98 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 28 Jun 2026 09:10:59 -0400 Subject: [PATCH] NEO-150: Add E7M4-07 economy validation kickoff plan Document issue-time band-cap and cross-ref validation approach, deny reason codes, file lists, and tests before implementation. --- docs/plans/NEO-150-implementation-plan.md | 163 ++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/plans/NEO-150-implementation-plan.md diff --git a/docs/plans/NEO-150-implementation-plan.md b/docs/plans/NEO-150-implementation-plan.md new file mode 100644 index 0000000..8c5afdc --- /dev/null +++ b/docs/plans/NEO-150-implementation-plan.md @@ -0,0 +1,163 @@ +# NEO-150 — E7M4-07: Economy validation lint at issue time + +**Linear:** [NEO-150](https://linear.app/neon-sprawl/issue/NEO-150) +**Branch:** `NEO-150-e7m4-07-economy-validation-lint-at-issue-time` +**Backlog:** [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) — **E7M4-07** +**Module:** [E7_M4_ContractMissionGenerator.md](../decomposition/modules/E7_M4_ContractMissionGenerator.md) +**Pattern:** [NEO-145-implementation-plan.md](NEO-145-implementation-plan.md) (CI-parity gates + `PrototypeE7M4ContractCatalogRules`); [NEO-147-implementation-plan.md](NEO-147-implementation-plan.md) (`ContractGeneratorOperations.TryIssue` orchestrator) +**Precursor:** [NEO-147](https://linear.app/neon-sprawl/issue/NEO-147) **`Done`** — `ContractGeneratorOperations.TryIssue` (**on `main`**) +**Blocks:** [NEO-151](https://linear.app/neon-sprawl/issue/NEO-151) (E7M4-08 contract issue HTTP) +**Client counterpart:** none (server policy); readable deny copy lands in **NEO-153** (E7M4-10) + +## Goal + +Slice 4 AC — issued contracts validate against **prototype economy caps** and **reward bundle cross-refs** at issue time (defense in depth after CI + startup catalog load). + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|-------|----------|---------------------|--------| +| Deny reason codes | Single vs split codes for cap vs cross-ref failures? | **Split:** `economy_cap_exceeded` for band-cap violations; `invalid_reward_bundle` for unknown item/skill/faction refs or disallowed skill XP source — keeps E7M4-10 HUD copy precise and matches backlog unit-test split (over-cap vs unknown id). | **Adopted** — user chose separate codes | +| Validation reuse | New logic vs delegate to `PrototypeE7M4ContractCatalogRules`? | **Delegate** — `ContractEconomyValidation.TryValidateTemplate` calls existing `TryGetBandCapGateError` + `TryGetCrossRefError` on a single-row map; same constants as CI/loader (`BandCapsByDifficultyBand`). | **Adopted** — repo precedent (NEO-145) | +| Cross-ref scope at issue | Bundle-only vs full loader parity? | **Full loader parity** for the selected template (encounter, faction gate, bundle item/skill/faction refs, `mission_reward` allowlist) via live registries — matches backlog “registry lookups” and “slipped through” AC. | **Adopted** — backlog in-scope list | +| TryIssue signature | Add registry parameters vs hide inside validation service? | **Add registry parameters** to `TryIssue` (encounter, item, skill, faction registries + skill defs for allowlist) — mirror `ContractCompletionOperations` / loader cross-ref deps; static orchestrator pattern unchanged. | **Adopted** — NEO-147 orchestrator precedent | +| Startup vs issue-time | Re-validate when catalog already passed load? | **Yes** — issue-time lint is fail-closed defense; production path always passes; unit tests inject tampered in-memory templates to prove deny. | **Adopted** — Linear AC “cannot issue if slipped through” | +| HTTP / Godot / telemetry | In scope? | **Out of scope** — NEO-151 HTTP, NEO-153 client HUD, NEO-152 comment hooks. | **Adopted** — backlog | + +## Scope and out-of-scope + +**In scope (from Linear + backlog):** + +- **`ContractEconomyValidation.TryValidateTemplate`** — band caps + cross-ref for one `ContractTemplateRow` using `PrototypeE7M4ContractCatalogRules` helpers and injected registries. +- Call from **`ContractGeneratorOperations.TryIssue`** after template selection, **before** `TryCreateActive`; fail-closed deny. +- **`ContractGeneratorReasonCodes`:** `economy_cap_exceeded`, `invalid_reward_bundle`. +- Unit tests: at-cap pass (prototype freeze), over-cap deny, unknown bundle id deny. +- Confirm startup/CI path unchanged: tampered catalog still fails `ContractTemplateCatalogLoader` / `validate_content.py` (existing NEO-144/NEO-145 tests). +- `server/README.md` contract generator section — economy validation reason codes + issue-time gate. +- **E7_M4** module doc — economy validation note + parity stub toward E6.M4 / E3.M5 (full map deferred). + +**Out of scope (from Linear + backlog):** + +- Full **`RewardParityMap`** (E6.M4); live ops dashboards (E9.M2 / E3.M5). +- **`POST …/contracts/issue`** HTTP (NEO-151). +- Godot client deny copy (NEO-153). +- Telemetry **`reward_anomaly`** comment hooks (NEO-152). + +## Acceptance criteria checklist + +- [ ] Template at freeze caps issues successfully. +- [ ] Tampered catalog row over caps fails at startup (CI) and cannot issue if slipped through. +- [ ] `dotnet test` covers economy validation gates. + +## Technical approach + +### 1. `ContractEconomyValidation` (`Game/Contracts/`) + +Static helper (mirror `MoveCommandValidation` shape): + +```csharp +public static bool TryValidateTemplate( + ContractTemplateRow template, + IEncounterDefinitionRegistry encounterRegistry, + IItemDefinitionRegistry itemRegistry, + ISkillDefinitionRegistry skillRegistry, + IFactionDefinitionRegistry factionRegistry, + out string? reasonCode) +``` + +**Flow:** + +1. Build single-row `Dictionary` for the selected template id. +2. **`TryGetBandCapGateError`** — if non-null → `reasonCode = economy_cap_exceeded`, return false. +3. **`TryGetCrossRefError`** — pass `knownEncounterIds`, `knownFactionIds`, `skillDefsById` from registries (same inputs as `ContractTemplateCatalogLoader`). +4. If cross-ref error → `reasonCode = invalid_reward_bundle`, return false. +5. Otherwise return true. + +Internal detail strings from gate helpers stay server-side; orchestrator exposes only stable reason codes. + +### 2. `ContractGeneratorOperations.TryIssue` + +After successful `TrySelectTemplate`, before `MakeDeterministicInstanceId` / `TryCreateActive`: + +```csharp +if (!ContractEconomyValidation.TryValidateTemplate( + template!, encounterRegistry, itemRegistry, skillRegistry, factionRegistry, out var economyReason)) +{ + return Deny(economyReason!); +} +``` + +Extend signature with: + +- `IEncounterDefinitionRegistry encounterRegistry` +- `IItemDefinitionRegistry itemRegistry` +- `ISkillDefinitionRegistry skillRegistry` +- `IFactionDefinitionRegistry factionRegistry` + +Update all call sites (generator tests, completion tests, encounter tests, quest fixture test, integration tests). + +### 3. Reason codes + +| Code | When | +|------|------| +| **`economy_cap_exceeded`** | Item qty, skill XP, or rep grant exceeds band policy | +| **`invalid_reward_bundle`** | Unknown encounter/item/skill/faction ref or skill missing `mission_reward` allowlist | + +### 4. Docs + +- **`server/README.md`** — extend contract generator reason-code table; note issue-time economy lint (NEO-150). +- **`E7_M4_ContractMissionGenerator.md`** — economy validation at issue + stub: “Prototype band caps enforce Slice 4 policy; full E6.M4 `RewardParityMap` / E3.M5 balance dashboards deferred.” +- **`documentation_and_implementation_alignment.md`** — E7.M4 row note after implementation. + +### 5. Parity stub (E6.M4 / E3.M5) + +One short subsection under E7.M4 module doc — no new E6.M4 implementation; documents that issue-time lint is **prototype band caps only**, not equivalent-power parity. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/plans/NEO-150-implementation-plan.md` | This plan | +| `server/NeonSprawl.Server/Game/Contracts/ContractEconomyValidation.cs` | `TryValidateTemplate` — band caps + cross-ref for one template | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractEconomyValidationTests.cs` | AAA unit tests for validation helper (at-cap, over-cap, unknown ref) | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Game/Contracts/ContractGeneratorReasonCodes.cs` | Add `EconomyCapExceeded`, `InvalidRewardBundle` constants | +| `server/NeonSprawl.Server/Game/Contracts/ContractGeneratorOperations.cs` | Invoke `TryValidateTemplate` before persist; extend `TryIssue` signature | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsTests.cs` | Wire registries into helper; add over-cap + unknown-ref deny tests; update `TryIssue` calls | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsIntegrationTests.cs` | Pass registries from DI | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorPersistenceIntegrationTests.cs` | Pass registries from DI | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractCompletionOperationsTests.cs` | Update `TryIssue` call sites | +| `server/NeonSprawl.Server.Tests/Game/Contracts/ContractCompletionOperationsIntegrationTests.cs` | Update `TryIssue` call sites | +| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCompletionOperationsTests.cs` | Update `TryIssue` call sites | +| `server/NeonSprawl.Server.Tests/Game/Quests/QuestFixtureApiTests.cs` | Update `TryIssue` call sites | +| `server/README.md` | Document economy validation at issue + reason codes | +| `docs/decomposition/modules/E7_M4_ContractMissionGenerator.md` | NEO-150 economy validation + E6.M4/E3.M5 parity stub | + +## Tests + +| File | What it covers | +|------|----------------| +| `ContractEconomyValidationTests` | At-cap pass (prototype freeze row); over-cap deny → `economy_cap_exceeded`; unknown item id → `invalid_reward_bundle` | +| `ContractGeneratorOperationsTests` | End-to-end: tampered template in in-memory registry denies issue with correct reason codes; prototype happy path still passes | +| `ContractGeneratorOperationsIntegrationTests` | DI path with real catalog still issues successfully | +| `ContractTemplateCatalogLoaderTests` (existing) | Startup fail on over-cap catalog — AC “fails at startup (CI)”; no change expected | +| CI | `python3 scripts/validate_content.py` unchanged; `dotnet test` green | + +Manual Godot QA: **none** (server policy; capstone NEO-154). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| Duplicate validation vs startup load | Accept small cost — issue-time lint is explicit Slice 4 AC defense-in-depth | `adopted` | +| `TryIssue` signature churn | Update all call sites in this story; NEO-151 HTTP wrapper inherits extended signature | `adopted` | +| Client deny copy for `invalid_reward_bundle` | Defer readable copy to NEO-153; server ships stable code now | `deferred` | +| Telemetry `reward_anomaly` at economy deny | Defer comment-only hooks to NEO-152 | `deferred` | + +## Client counterpart + +None (server policy). **NEO-153** will surface `economy_cap_exceeded` and `invalid_reward_bundle` after **NEO-151** HTTP lands.