10 KiB
NEO-150 — E7M4-07: Economy validation lint at issue time
Linear: NEO-150
Branch: NEO-150-e7m4-07-economy-validation-lint-at-issue-time
Backlog: E7M4-pre-production-backlog.md — E7M4-07
Module: E7_M4_ContractMissionGenerator.md
Pattern: NEO-145-implementation-plan.md (CI-parity gates + PrototypeE7M4ContractCatalogRules); NEO-147-implementation-plan.md (ContractGeneratorOperations.TryIssue orchestrator)
Precursor: NEO-147 Done — ContractGeneratorOperations.TryIssue (on main)
Blocks: 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 oneContractTemplateRowusingPrototypeE7M4ContractCatalogRuleshelpers and injected registries.- Call from
ContractGeneratorOperations.TryIssueafter template selection, beforeTryCreateActive; 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.mdcontract 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/issueHTTP (NEO-151).- Godot client deny copy (NEO-153).
- Telemetry
reward_anomalycomment 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 testcovers economy validation gates.
Technical approach
1. ContractEconomyValidation (Game/Contracts/)
Static helper (mirror MoveCommandValidation shape):
public static bool TryValidateTemplate(
ContractTemplateRow template,
IEncounterDefinitionRegistry encounterRegistry,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IFactionDefinitionRegistry factionRegistry,
out string? reasonCode)
Flow:
- Build single-row
Dictionary<string, ContractTemplateRow>for the selected template id. TryGetBandCapGateError— if non-null →reasonCode = economy_cap_exceeded, return false.TryGetCrossRefError— passknownEncounterIds,knownFactionIds,skillDefsByIdfrom registries (same inputs asContractTemplateCatalogLoader).- If cross-ref error →
reasonCode = invalid_reward_bundle, return false. - 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:
if (!ContractEconomyValidation.TryValidateTemplate(
template!, encounterRegistry, itemRegistry, skillRegistry, factionRegistry, out var economyReason))
{
return Deny(economyReason!);
}
Extend signature with:
IEncounterDefinitionRegistry encounterRegistryIItemDefinitionRegistry itemRegistryISkillDefinitionRegistry skillRegistryIFactionDefinitionRegistry 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.M4RewardParityMap/ 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.