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.pull/192/head
parent
177b316efa
commit
039542aa74
|
|
@ -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<string, ContractTemplateRow>` 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.
|
||||
Loading…
Reference in New Issue