NEO-100: Kickoff plan for encounter + reward catalog schemas and CI

Document frozen prototype_combat_pocket spine, schema shapes, validate_content
gates, and file lists before E5M3-01 implementation.
pull/139/head
VinPropane 2026-05-30 19:33:27 -04:00
parent cb86645524
commit 4f16c848db
1 changed files with 166 additions and 0 deletions

View File

@ -0,0 +1,166 @@
# NEO-100 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-100 |
| **Title** | E5M3-01: Prototype EncounterDef + RewardTable catalog + schemas + CI |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-100/e5m3-01-prototype-encounterdef-rewardtable-catalog-schemas-ci |
| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-01** |
| **Branch** | `NEO-100-e5m3-encounter-reward-catalog-schemas-ci` |
| **Precursor** | [E5.M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) **Ready** (three NPC instance ids) · [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) **Ready** (frozen item catalog incl. **`contract_handoff_token`**) |
| **Pattern** | [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate |
| **Blocks** | [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) — server encounter + reward catalog load |
## Kickoff clarifications
**No clarifications needed.** Scope, frozen ids, loot grants, completion rule, and cross-ref targets are fully specified in [E5M3 kickoff decisions](E5M3-prototype-backlog.md#kickoff-decisions-decomposition-defaults), [E5.M3 freeze table](../decomposition/modules/E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01), and `content/README.md` (NEO-100 paragraph already present from E5M3 decomposition). Catalog envelope and CI constant naming follow NEO-87 / NEO-76 precedent without ambiguity.
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| — | — | — | **No blocking decisions** (see above). |
## Goal, scope, and out-of-scope
**Goal:** Lock content shape and CI validation for **one** frozen `EncounterDef` + **one** frozen `RewardTable` before server load (NEO-101+).
**In scope (from Linear + [E5M3-01](E5M3-prototype-backlog.md#e5m3-01--prototype-encounterdef--rewardtable-catalog--schemas--ci)):**
- `content/schemas/encounter-def.schema.json`, `content/schemas/reward-table.schema.json`, `content/schemas/reward-grant-row.schema.json`.
- `content/encounters/prototype_encounters.json` — one row **`prototype_combat_pocket`**.
- `content/reward-tables/prototype_reward_tables.json` — one row **`prototype_combat_pocket_clear`**.
- `scripts/validate_content.py` — schema validation, duplicate `id`, one-encounter / one-table allowlists, cross-ref **`itemId`** → frozen item catalog, **`requiredNpcInstanceIds`** → three E5.M2 NPC instance ids, **`rewardTableId`** → reward table ids.
- Designer note / CI rules line in [E5_M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) + **CT.M1** PR gate paragraph.
- `documentation_and_implementation_alignment.md` E5.M3 row → note NEO-100 catalog in progress / landed.
**Out of scope (from Linear):**
- Server loader, runtime engine, HTTP, Godot (NEO-101+).
- **`IEncounterProgressStore`**, completion events, inventory grant (E5M3-04+).
- **Client counterpart:** none — server-only content + CI; player-visible work starts at E5M3-08+ / [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111).
## Acceptance criteria checklist
- [ ] PR gate validates encounter + reward-table JSON against schema.
- [ ] Exactly one prototype encounter id and one reward table id; duplicate `id` fails CI.
- [ ] Stable id list documented in module doc freeze box.
## Technical approach
1. **`reward-grant-row.schema.json`:** Mirror [`recipe-io-row.schema.json`](../../content/schemas/recipe-io-row.schema.json) — required **`itemId`** (pattern `^[a-z][a-z0-9_]*$`), **`quantity`** integer ≥ 1.
2. **`reward-table.schema.json`:** Draft 2020-12 object; `additionalProperties: false`. Required: **`id`**, **`displayName`**, **`fixedGrants`** (non-empty array of `$ref` reward-grant-row). `id` pattern same as other catalogs.
3. **`encounter-def.schema.json`:** Required: **`id`**, **`displayName`**, **`completionCriteria`**, **`requiredNpcInstanceIds`**, **`rewardTableId`**. **`completionCriteria`:** object with required **`kind`** enum **`defeat_all_targets`** only (prototype). **`requiredNpcInstanceIds`:** array of 3 strings (pattern), **`minItems`/`maxItems`: 3**. **`rewardTableId`:** string pattern.
4. **Catalog files:**
**`content/reward-tables/prototype_reward_tables.json`**
```json
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Prototype Combat Pocket Clear",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
```
**`content/encounters/prototype_encounters.json`**
```json
{
"schemaVersion": 1,
"encounters": [
{
"id": "prototype_combat_pocket",
"displayName": "Prototype Combat Pocket",
"completionCriteria": { "kind": "defeat_all_targets" },
"requiredNpcInstanceIds": [
"prototype_npc_melee",
"prototype_npc_ranged",
"prototype_npc_elite"
],
"rewardTableId": "prototype_combat_pocket_clear"
}
]
}
```
**`requiredNpcInstanceIds`** order matches [E5.M3 freeze table](../decomposition/modules/E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01) (melee → ranged → elite); CI enforces **set equality** with `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`, not order-sensitive beyond schema min/max 3.
5. **`validate_content.py` constants (keep in sync with E5.M3 module doc + future NEO-101 loader):**
| Constant | Value |
|----------|--------|
| `PROTOTYPE_E5M3_ENCOUNTER_IDS` | `{ "prototype_combat_pocket" }` |
| `PROTOTYPE_E5M3_REWARD_TABLE_IDS` | `{ "prototype_combat_pocket_clear" }` |
| `PROTOTYPE_E5M2_NPC_INSTANCE_IDS` | `{ "prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite" }` |
6. **`validate_content.py` validators:**
- Add schema paths + dirs: `ENCOUNTER_DEF_SCHEMA`, `REWARD_TABLE_SCHEMA`, `REWARD_GRANT_ROW_SCHEMA`, `ENCOUNTERS_DIR`, `REWARD_TABLES_DIR`.
- **`_validate_reward_table_catalogs`:** envelope `schemaVersion: 1`, top-level **`rewardTables`** array; validate each row; track `seen_ids`; reject duplicates across files.
- **`_prototype_e5m3_reward_table_gate`:** ids must equal `PROTOTYPE_E5M3_REWARD_TABLE_IDS`.
- **`_prototype_e5m3_reward_grant_item_gate`:** each `fixedGrants[].itemId``PROTOTYPE_SLICE1_ITEM_IDS`; quantities ≥ 1 (schema) — spot-check **`scrap_metal_bulk` ×10** and **`contract_handoff_token` ×1** in gate or integration-style assert in docstring test notes.
- **`_validate_encounter_catalogs`:** envelope `schemaVersion: 1`, top-level **`encounters`** array; validate rows; duplicate `id` rejection.
- **`_prototype_e5m3_encounter_gate`:** ids must equal `PROTOTYPE_E5M3_ENCOUNTER_IDS`.
- **`_prototype_e5m3_encounter_cross_ref_gate`:** each row's **`requiredNpcInstanceIds`** set must equal `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`; **`rewardTableId`** must exist in reward-table `seen_ids`.
- Run **reward tables before encounters** in `main()` (encounters reference reward table ids).
- Extend module docstring, startup schema file checks, success summary with encounter + reward-table file/id counts.
7. **Docs:**
- `E5_M3_EncounterAndRewardTables.md` — add CI enforcement line under freeze table (ids + cross-ref rules); link to this plan when complete.
- `CT_M1_ContentValidationPipeline.md` — encounter + reward-table PR gate paragraph.
- `documentation_and_implementation_alignment.md` — E5.M3 row notes NEO-100 catalog when complete.
- `content/README.md` — table rows for `encounters/` and `reward-tables/` already present; verify paragraph matches shipped constants after implementation.
- `E5M3-prototype-backlog.md` — E5M3-01 checkboxes when implementation completes.
8. **No server/C#/client changes** in this story.
## Files to add
| Path | Purpose |
|------|---------|
| `content/schemas/reward-grant-row.schema.json` | JSON Schema for a single fixed-grant row on a `RewardTable`. |
| `content/schemas/reward-table.schema.json` | JSON Schema for a single `RewardTable` row (uses reward-grant-row). |
| `content/schemas/encounter-def.schema.json` | JSON Schema for a single `EncounterDef` row. |
| `content/reward-tables/prototype_reward_tables.json` | Prototype one-table catalog (`schemaVersion` + `rewardTables` array). |
| `content/encounters/prototype_encounters.json` | Prototype one-encounter catalog (`schemaVersion` + `encounters` array). |
| `docs/plans/NEO-100-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `scripts/validate_content.py` | Load/validate encounter + reward-table catalogs; duplicate `id`; E5M3 one-id gates; NPC instance + item + rewardTableId cross-refs; success summary. |
| `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | CI enforcement note under freeze table; link to plan when complete. |
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for encounter + reward-table catalog validation. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 / NEO-100 status after catalog lands. |
## Tests
| Item | Coverage |
|------|----------|
| **CI / PR gate** | `.github/workflows/pr-gate.yml` runs `python scripts/validate_content.py` — extended script is the primary regression signal. |
| **Manual negative cases** | From repo root after implementation: (1) duplicate encounter or reward-table `id` → non-zero exit; (2) remove frozen id → gate error; (3) unknown `itemId` in `fixedGrants` → cross-ref error; (4) wrong NPC instance id set → cross-ref error; (5) broken `rewardTableId` on encounter → cross-ref error; (6) empty `displayName` or invalid `completionCriteria.kind` → schema error. |
| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports encounter + reward-table catalogs + existing catalogs OK. |
No dedicated pytest module in-repo today (same as NEO-87 / NEO-76).
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| NPC instance ids are server-registry constants, not content rows | **`PROTOTYPE_E5M2_NPC_INSTANCE_IDS` frozenset in CI** — mirror server `PrototypeNpcRegistry`; document sync obligation in module doc (same pattern as behavior ids without a content file). | **adopted** |
| `content/README.md` pre-documents NEO-100 before catalogs land | **Keep paragraph; verify constants match after ship** — decomposition already merged. | **adopted** |
| Cursor markdown `#anchor` deep links | Unrelated to this story; GitHub + stable ids remain source of truth. | **deferred** |