# NEO-39 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-39 |
| **Title** | Data-driven skill level curve + CI validation |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-39/data-driven-skill-level-curve-ci-validation |
| **Module** | [E2.M2 — XpAwardAndLevelEngine](../decomposition/modules/E2_M2_XpAwardAndLevelEngine.md) |
| **Blocked by** | [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) (grant path + progression store) |

## Kickoff clarifications

| Topic | Question | Resolution |
|--------|----------|------------|
| **Curve shape** | Which JSON structure should this story ship? | **User accepted recommendation:** use a simple ordered thresholds array (`array_thresholds`) for level resolution. |
| **Startup failure behavior** | What if level-curve content is missing/invalid at boot? | **User accepted recommendation:** **fail fast** at startup; no fallback defaults in this slice. |
| **Scope target** | Where should curve loading be wired in this story? | **User accepted recommendation:** **strict ticket scope**; implement only what is required to satisfy NEO-39 acceptance criteria. |

## Goal, scope, and out-of-scope

**Goal:** Replace `SkillLevelCurvePlaceholder` inline threshold logic with data-driven `LevelCurve` content under `content/`, validate that content in CI via `scripts/validate_content.py`, and load the curve at server boot so XP-to-level resolution can be tuned through data without code changes.

**In scope (from Linear + kickoff decisions):**

- Add `LevelCurve` content JSON and JSON Schema using an ordered threshold-array shape.
- Extend `scripts/validate_content.py` so CI fails on invalid curve files/schema mismatches.
- Load and validate level curve content during server startup; fail startup if required curve content is invalid or missing.
- Switch progression level resolution from `SkillLevelCurvePlaceholder` constants to loaded curve data.
- Add tests for curve loading and level boundary behavior.

**Out of scope (from Linear):**

- Hot reload / dev file watcher for content.
- E2.M4 pacing profiles and broader balancing systems.
- Additional progression API shape changes unrelated to curve source replacement.

## Acceptance criteria checklist

- [x] Curve content + schema exist; CI fails on invalid curve files.
- [x] Server loads curves at startup; XP/level resolution uses file data (no code change required to tune thresholds).
- [x] Tests cover curve load and level boundary behavior.

## Technical approach

1. Add a new curve catalog file under `content/skills/` with a top-level level-threshold array (prototype-global curve for this slice).
2. Add a dedicated JSON Schema under `content/schemas/` for level-curve rows and enforce strictly increasing XP thresholds.
3. Extend `scripts/validate_content.py` to validate both existing skill-def catalogs and the new level-curve catalog(s), failing with clear diagnostics and non-zero exit code on any schema error.
4. Introduce a server-side level-curve provider/loader that reads validated curve content at boot and registers the resolved thresholds in DI.
5. Replace `SkillLevelCurvePlaceholder.LevelFromTotalXp` usage with a resolver backed by loaded content data; remove placeholder-only assumptions from runtime path.
6. Keep behavior deterministic at boundaries (exact threshold hit levels up; below threshold does not); encode this in tests.
7. Preserve strict startup behavior: missing or invalid required curve content prevents server startup for prototype safety.

## Files to add

| Path | Purpose |
|------|---------|
| `content/skills/prototype_level_curve.json` | Data source for prototype level thresholds used by runtime level resolution. |
| `content/schemas/level-curve.schema.json` | JSON Schema for curve file structure and row constraints. |
| `server/NeonSprawl.Server/Game/Skills/ISkillLevelCurve.cs` | Interface for XP→level resolution from loaded content-backed thresholds. |
| `server/NeonSprawl.Server/Game/Skills/ContentBackedSkillLevelCurve.cs` | Runtime resolver mapping total XP to level from loaded thresholds. |
| `server/NeonSprawl.Server.Tests/Game/Skills/ContentBackedSkillLevelCurveTests.cs` | Boundary-focused tests for threshold mapping behavior. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `scripts/validate_content.py` | Add level-curve catalog/schema validation and CI-fail diagnostics for invalid curve content. |
| `server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs` | Ensure progression snapshots resolve `level` via loaded curve provider instead of placeholder constants. |
| `server/NeonSprawl.Server/Game/Skills/SkillDefinitionCatalogLoader.cs` | Restrict skill catalog discovery to `*_skills.json` so level-curve JSON can coexist in `content/skills/`. |
| `server/db/migrations/V003__player_skill_progression.sql` | Update comment to reflect data-driven level derivation in runtime. |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionCatalogLoaderTests.cs` | Update fixture file names to `*_skills.json` pattern used by loader. |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionRegistryTests.cs` | Update fixture file name to `prototype_skills.json` for loader parity. |
| `server/NeonSprawl.Server/Program.cs` | Register boot-time curve loading and DI wiring for content-backed level resolver. |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs` | Update/extend assertions to reflect data-driven level thresholds and boundary crossings. |
| `server/README.md` | Document where level curves live, boot-load/fail-fast behavior, and validation command. |
| `docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md` | Update implementation snapshot after NEO-39 decisions/landing details. |

## Tests

| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Skills/ContentBackedSkillLevelCurveTests.cs` | Unit tests for XP threshold boundaries (below first threshold, exact threshold, between thresholds, high-XP upper ranges). |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs` | Integration tests asserting grant responses use data-driven level resolution and expected `levelUps` at threshold boundaries. |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionSnapshotApiTests.cs` | Snapshot endpoint reflects data-driven levels (including unchanged level below next threshold). |
| `scripts/validate_content.py` (invoked in CI) | Validation failure behavior for malformed level-curve content/schema mismatch. |

If script-level tests are not present in this repository, verification for validator changes will be covered by running the validation script against good and intentionally bad fixtures during implementation and documenting results in the plan Decisions section.

## Decisions

- Adopted `*_skills.json` and `*_level_curve.json` file naming split under `content/skills/` so both catalogs can coexist without ambiguous loader behavior.
- Kept startup fail-fast for level-curve load: missing/invalid `content/skills/prototype_level_curve.json` or missing schema now throws during boot.
- Verification outcomes:
  - `python3 scripts/validate_content.py` passes with skill + level-curve catalogs.
  - `dotnet test server/NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj --no-restore` passes (`132` tests).

## Open questions / risks

- **Schema strictness trade-off:** very strict schema constraints reduce bad data risk but can make iterative tuning slower; keep constraints targeted to structural correctness and monotonic thresholds.
- **Boot coupling:** fail-fast startup improves safety but increases sensitivity to content mistakes; error messages in validator/boot path must be precise.
- **None** otherwise.
