12 KiB
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 · Epic 5 Slice 3 · backlog E5M3-01 |
| Branch | NEO-100-e5m3-encounter-reward-catalog-schemas-ci |
| Precursor | E5.M2 Ready (three NPC instance ids) · E3.M3 Ready (frozen item catalog incl. contract_handoff_token) |
| Pattern | NEO-87 — row JSON Schema + catalog envelope + scripts/validate_content.py gate |
| Blocks | 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, E5.M3 freeze table, 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):
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 rowprototype_combat_pocket.content/reward-tables/prototype_reward_tables.json— one rowprototype_combat_pocket_clear.scripts/validate_content.py— schema validation, duplicateid, one-encounter / one-table allowlists, cross-refitemId→ frozen item catalog,requiredNpcInstanceIds→ three E5.M2 NPC instance ids,rewardTableId→ reward table ids.- Designer note / CI rules line in E5_M3 + CT.M1 PR gate paragraph.
documentation_and_implementation_alignment.mdE5.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 / 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
idfails CI. - Stable id list documented in module doc freeze box.
Implementation reconciliation (shipped)
- Schemas:
encounter-def.schema.json,reward-table.schema.json,reward-grant-row.schema.json. - Catalogs:
prototype_encounters.json,prototype_reward_tables.json. - CI:
PROTOTYPE_E5M3_ENCOUNTER_IDS,PROTOTYPE_E5M3_REWARD_TABLE_IDS,PROTOTYPE_E5M2_NPC_INSTANCE_IDS,PROTOTYPE_E5M3_REWARD_GRANTSgates invalidate_content.py. - Docs: E5.M3 CI enforcement line, CT.M1, alignment register, E5M3-01 backlog checkboxes.
Technical approach
-
reward-grant-row.schema.json: Mirrorrecipe-io-row.schema.json— requireditemId(pattern^[a-z][a-z0-9_]*$),quantityinteger ≥ 1. -
reward-table.schema.json: Draft 2020-12 object;additionalProperties: false. Required:id,displayName,fixedGrants(non-empty array of$refreward-grant-row).idpattern same as other catalogs. -
encounter-def.schema.json: Required:id,displayName,completionCriteria,requiredNpcInstanceIds,rewardTableId.completionCriteria: object with requiredkindenumdefeat_all_targetsonly (prototype).requiredNpcInstanceIds: array of 3 strings (pattern),minItems/maxItems: 3.rewardTableId: string pattern. -
Catalog files:
content/reward-tables/prototype_reward_tables.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{ "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" } ] }requiredNpcInstanceIdsorder matches E5.M3 freeze table (melee → ranged → elite); CI enforces set equality withPROTOTYPE_E5M2_NPC_INSTANCE_IDS, not order-sensitive beyond schema min/max 3. -
validate_content.pyconstants (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" } -
validate_content.pyvalidators:- Add schema paths + dirs:
ENCOUNTER_DEF_SCHEMA,REWARD_TABLE_SCHEMA,REWARD_GRANT_ROW_SCHEMA,ENCOUNTERS_DIR,REWARD_TABLES_DIR. _validate_reward_table_catalogs: envelopeschemaVersion: 1, top-levelrewardTablesarray; validate each row; trackseen_ids; reject duplicates across files._prototype_e5m3_reward_table_gate: ids must equalPROTOTYPE_E5M3_REWARD_TABLE_IDS._prototype_e5m3_reward_grant_item_gate: eachfixedGrants[].itemId∈PROTOTYPE_SLICE1_ITEM_IDS; quantities ≥ 1 (schema) — spot-checkscrap_metal_bulk×10 andcontract_handoff_token×1 in gate or integration-style assert in docstring test notes._validate_encounter_catalogs: envelopeschemaVersion: 1, top-levelencountersarray; validate rows; duplicateidrejection._prototype_e5m3_encounter_gate: ids must equalPROTOTYPE_E5M3_ENCOUNTER_IDS._prototype_e5m3_encounter_cross_ref_gate: each row'srequiredNpcInstanceIdsset must equalPROTOTYPE_E5M2_NPC_INSTANCE_IDS;rewardTableIdmust exist in reward-tableseen_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.
- Add schema paths + dirs:
-
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 forencounters/andreward-tables/already present; verify paragraph matches shipped constants after implementation.E5M3-prototype-backlog.md— E5M3-01 checkboxes when implementation completes.
-
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 |