Merge pull request #163 from ViPro-Technologies/NEO-124-e7m2-questrewardbundle-schema-quest-catalog-extension-ci
NEO-124: E7M2-01 quest completion reward bundle schema + CIpull/164/head
commit
0ae6bea327
|
|
@ -32,6 +32,8 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with **
|
|||
|
||||
**Prototype E7 Slice 1 — quests (NEO-112):** CI expects **exactly four** quest ids aligned to [E7.M1 quest freeze](../docs/decomposition/modules/E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01) — **`prototype_quest_gather_intro`**, **`prototype_quest_refine_intro`**, **`prototype_quest_combat_intro`**, **`prototype_quest_operator_chain`**. Objective **`itemId`** / **`recipeId`** / **`encounterId`** cross-refs must resolve to frozen item, recipe, and encounter catalogs; **`prerequisiteQuestIds`** must be acyclic and reference known quest ids; operator chain terminal step is **`inventory_has_item`** **`contract_handoff_token`** ×1. **Do not rename** quest `id` after ship—change **`displayName`** only. See [E7M1-prototype-backlog.md](../docs/plans/E7M1-prototype-backlog.md) and [NEO-112 plan](../docs/plans/NEO-112-implementation-plan.md).
|
||||
|
||||
**Prototype E7 Slice 2 — quest completion bundles (NEO-124):** Each of the four frozen quest ids must include **`completionRewardBundle`** with **`itemGrants`** / **`skillXpGrants`** per [E7.M2 completion freeze](../docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md#prototype-slice-2-freeze-e7m2-01). Bundle **`itemGrants[].itemId`** must resolve to the frozen item catalog; **`skillXpGrants[].skillId`** must resolve to a skill that lists **`mission_reward`** in **`allowedXpSourceKinds`**. Omit empty **`itemGrants`**. See [E7M2-prototype-backlog.md](../docs/plans/E7M2-prototype-backlog.md) and [NEO-124 plan](../docs/plans/NEO-124-implementation-plan.md).
|
||||
|
||||
**Prototype Slice 4 (NEO-45):** CI expects **exactly one** `MasteryTrack` for **`salvage`** only (`refine` / `intrusion` have no mastery rows yet). Tier 1 @ skill level **2** is a **mutually exclusive** branch pick (`scrap_efficiency` vs `bulk_haul`) with **no** perks; tier 2 @ level **4** unlocks one perk per branch path. **Do not rename** `branchId` / `perkId` after ship—change `displayName` only. See [E2.M3 — Designer note](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#designer-note-tiers-branches-and-level-gates) and [freeze table](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#prototype-slice-4-freeze--salvage-flagship-neo-45).
|
||||
|
||||
Server load path and hot-reload policy are TBD; keep files versioned here as the source of truth.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
"id": "prototype_quest_gather_intro",
|
||||
"displayName": "Intro: Salvage Run",
|
||||
"prerequisiteQuestIds": [],
|
||||
"completionRewardBundle": {
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 25 }]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "gather_intro_step_salvage",
|
||||
|
|
@ -24,6 +27,9 @@
|
|||
"id": "prototype_quest_refine_intro",
|
||||
"displayName": "Intro: Refine Stock",
|
||||
"prerequisiteQuestIds": ["prototype_quest_gather_intro"],
|
||||
"completionRewardBundle": {
|
||||
"skillXpGrants": [{ "skillId": "refine", "amount": 25 }]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "refine_intro_step_craft",
|
||||
|
|
@ -43,6 +49,9 @@
|
|||
"id": "prototype_quest_combat_intro",
|
||||
"displayName": "Intro: Clear the Pocket",
|
||||
"prerequisiteQuestIds": [],
|
||||
"completionRewardBundle": {
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 25 }]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "combat_intro_step_encounter",
|
||||
|
|
@ -65,6 +74,10 @@
|
|||
"prototype_quest_refine_intro",
|
||||
"prototype_quest_combat_intro"
|
||||
],
|
||||
"completionRewardBundle": {
|
||||
"itemGrants": [{ "itemId": "survey_drone_kit", "quantity": 1 }],
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 50 }]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "chain_step_gather",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@
|
|||
"$ref": "https://neon-sprawl.local/schemas/quest-step-def.json"
|
||||
},
|
||||
"description": "Ordered steps; prototype Slice 1 uses sequential advance only."
|
||||
},
|
||||
"completionRewardBundle": {
|
||||
"$ref": "https://neon-sprawl.local/schemas/quest-reward-bundle.json",
|
||||
"description": "Optional completion payout (E7.M2); required on all four prototype quest ids in CI."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://neon-sprawl.local/schemas/quest-reward-bundle.json",
|
||||
"title": "QuestRewardBundle",
|
||||
"description": "Composite completion rewards on QuestDef.completionRewardBundle. Grant kinds v1: itemGrants + skillXpGrants only. See docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"itemGrants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "https://neon-sprawl.local/schemas/reward-grant-row.json"
|
||||
},
|
||||
"description": "Item payout rows; omit when empty (XP-only bundles)."
|
||||
},
|
||||
"skillXpGrants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "https://neon-sprawl.local/schemas/quest-skill-xp-grant.json"
|
||||
},
|
||||
"description": "Skill XP rows applied via mission_reward on first-time completion."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["itemGrants"],
|
||||
"properties": {
|
||||
"itemGrants": {
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"required": ["skillXpGrants"],
|
||||
"properties": {
|
||||
"skillXpGrants": {
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://neon-sprawl.local/schemas/quest-skill-xp-grant.json",
|
||||
"title": "QuestSkillXpGrant",
|
||||
"description": "Single skill XP row on a quest completionRewardBundle. Apply path uses MissionRewardSkillXpGrant (sourceKind mission_reward) — see E7.M2.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["skillId", "amount"],
|
||||
"properties": {
|
||||
"skillId": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "Skill def id from content/skills (prototype Slice 1: frozen trio)."
|
||||
},
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Skill XP granted on first-time quest completion."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,8 @@ Provide a quest state machine and reward routing that ties gathering, crafting,
|
|||
- Idempotent reward delivery; replays cannot double-claim.
|
||||
- Telemetry hooks: `reward_delivery`, `unlock_granted`.
|
||||
|
||||
**Linear backlog:** [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md) — [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132); client capstone **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132). Verify quest completion rewards in **Godot**, not Bruno-only. Encounter loot ([E5.M3](../modules/E5_M3_EncounterAndRewardTables.md)) unchanged in Slice 2.
|
||||
|
||||
### Slice 3 - Faction ledger (pre-production)
|
||||
|
||||
- Scope: E7.M3 gating at least one faction line and mission set.
|
||||
|
|
@ -94,4 +96,5 @@ Provide a quest state machine and reward routing that ties gathering, crafting,
|
|||
## Definition of Done
|
||||
|
||||
- Prototype quest counts and integrated chain meet master plan minimums **in the Godot client** (Slice 1 capstone [NEO-123](../../manual-qa/NEO-123.md)), not Bruno-only.
|
||||
- Slice 2 quest **completion** rewards (items + skill XP) visible in Godot with idempotent delivery (capstone [NEO-132](../../manual-qa/NEO-132.md)), not Bruno-only.
|
||||
- Pre-production adds faction + contracts with validation.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard
|
|||
|
||||
**Quest catalogs ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)):** the same script validates each row in `content/quests/*_quests.json` against [`content/schemas/quest-def.schema.json`](../../../content/schemas/quest-def.schema.json) (steps via [`quest-step-def.schema.json`](../../../content/schemas/quest-step-def.schema.json), objectives via [`quest-objective-def.schema.json`](../../../content/schemas/quest-objective-def.schema.json)), rejects **duplicate `id`** across files and duplicate step/objective ids within a quest, cross-checks objective **`itemId`** / **`recipeId`** / **`encounterId`** against frozen item, recipe, and encounter catalogs, enforces **acyclic `prerequisiteQuestIds`**, and (E7 Slice 1) enforces the **frozen four-quest** id set with operator chain terminal **`inventory_has_item`** **`contract_handoff_token`** ×1.
|
||||
|
||||
**Quest completion bundles ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)):** optional **`completionRewardBundle`** on each quest row via [`quest-reward-bundle.schema.json`](../../../content/schemas/quest-reward-bundle.schema.json) (`itemGrants` reuses [`reward-grant-row.schema.json`](../../../content/schemas/reward-grant-row.schema.json); `skillXpGrants` via [`quest-skill-xp-grant.schema.json`](../../../content/schemas/quest-skill-xp-grant.schema.json)). E7 Slice 2 requires bundles on all four frozen quest ids with exact freeze-table contents; cross-checks **`itemGrants[].itemId`** and **`skillXpGrants[].skillId`**; each skill XP target must allow **`mission_reward`** in **`allowedXpSourceKinds`**.
|
||||
|
||||
**Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception.
|
||||
|
||||
## Source anchors
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
| **Module ID** | E7.M2 |
|
||||
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
||||
| **Stage target** | Prototype |
|
||||
| **Status** | Planned (see [dependency register](module_dependency_register.md)). **NEO-43 prep:** server `MissionRewardSkillXpGrant` for scripted skill XP from bundles (`mission_reward`); full router + `QuestRewardBundle` apply still TBD. |
|
||||
| **Status** | Planned — Slice 2 backlog [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md): **E7M2-01** [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132). **NEO-43 prep landed:** `MissionRewardSkillXpGrant` for bundle skill XP (`mission_reward`). Upstream: E7.M1 **Ready**, E2.M2 grant stack, E3.M3 inventory. |
|
||||
|
||||
## Purpose
|
||||
|
||||
|
|
@ -38,7 +38,22 @@ Route quest and encounter outputs to XP, items, unlocks, and flags with **idempo
|
|||
|
||||
## Related implementation slices
|
||||
|
||||
Epic 7 **Slice 2** — `reward_delivery`, `unlock_granted`; no double-claim on replay.
|
||||
Epic 7 **Slice 2** — `reward_delivery`, `unlock_granted`; no double-claim on replay. Backlog: [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md).
|
||||
|
||||
## Prototype Slice 2 freeze (E7M2-01)
|
||||
|
||||
**`completionRewardBundle`** on each frozen [E7.M1 quest id](E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01). Grant kinds v1: **`itemGrants`** + **`skillXpGrants`** only (unlock/flag rows deferred). Skill XP apply uses **`mission_reward`** via **`MissionRewardSkillXpGrant`** — not stored in content rows.
|
||||
|
||||
| Quest id | Item grants (`itemGrants`) | Skill XP (`skillXpGrants`) |
|
||||
|----------|----------------------------|----------------------------|
|
||||
| `prototype_quest_gather_intro` | — | `salvage` **25** |
|
||||
| `prototype_quest_refine_intro` | — | `refine` **25** |
|
||||
| `prototype_quest_combat_intro` | — | `salvage` **25** |
|
||||
| `prototype_quest_operator_chain` | `survey_drone_kit` ×1 | `salvage` **50** |
|
||||
|
||||
**Idempotency:** `{playerId}:quest_complete:{questId}`. **Encounter loot:** [E5.M3](E5_M3_EncounterAndRewardTables.md) direct grants unchanged in Slice 2.
|
||||
|
||||
**CI enforcement ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)):** [`scripts/validate_content.py`](../../../scripts/validate_content.py) requires **`completionRewardBundle`** on all four frozen quest ids with exact freeze-table contents; cross-checks **`itemGrants[].itemId`** against the frozen item catalog and **`skillXpGrants[].skillId`** against skills that allow **`mission_reward`** in **`allowedXpSourceKinds`**.
|
||||
|
||||
## Source anchors
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -111,6 +111,8 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|
|||
|
||||
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) **landed**; label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. **E7M1-12 / NEO-123** client capstone landed — [`NEO-123` manual QA](../../manual-qa/NEO-123.md), [client README — End-to-end onboarding quest loop (NEO-123)](../../../client/README.md#end-to-end-onboarding-quest-loop-neo-123), plan [NEO-123](../../plans/NEO-123-implementation-plan.md). Epic 7 Slice 1 client capstone complete.
|
||||
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned |
|
||||
|
||||
**E7.M2 note:** Epic 7 **Slice 2** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132); label **`E7.M2`**. See [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md), [E7_M2_RewardAndUnlockRouter.md](E7_M2_RewardAndUnlockRouter.md). Upstream: E7.M1 **Ready**, E2.M2 grant stack ([NEO-43](https://linear.app/neon-sprawl/issue/NEO-43)), E3.M3 inventory **Ready**. Client capstone **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132). Register row stays **Planned** until capstone lands.
|
||||
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
|
||||
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco
|
|||
| 11 | **E7M1-11** | client | E7M1-08, E7M1-09 |
|
||||
| 12 | **E7M1-12** | client | E7M1-11 |
|
||||
|
||||
**Downstream (separate modules):** [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) idempotent **`QuestRewardBundle`** apply + consume **`EncounterCompleteEvent`**; [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) faction ledger (pre-production); [E8.M1](../decomposition/modules/E8_M1_PartyAndMatchAssembly.md) party-credit quests.
|
||||
**Downstream (separate modules):** [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) Slice 2 backlog [E7M2-prototype-backlog.md](E7M2-prototype-backlog.md) ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)–[NEO-132](https://linear.app/neon-sprawl/issue/NEO-132)); [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) faction ledger (pre-production); [E8.M1](../decomposition/modules/E8_M1_PartyAndMatchAssembly.md) party-credit quests.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,314 @@
|
|||
# E7.M2 — Prototype story backlog (RewardAndUnlockRouter)
|
||||
|
||||
Working backlog for **Epic 7 — Slice 2** ([reward and unlock routing](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing)). Decomposition and contracts: [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md).
|
||||
|
||||
**Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E7M1 (paired server+client)](../plans/E7M1-prototype-backlog.md), [E5M3 (encounter loot capstone)](../plans/E5M3-prototype-backlog.md).
|
||||
|
||||
**Labels (Linear):** every issue **`E7.M2`**; add **`server`** or **`client`** + **`Story`** as listed.
|
||||
|
||||
**Precursor (do not re-scope):** [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) Slice 1 **Ready** — quest state + objectives only; no duplicate completion grants on replay ([NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)). [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounter loot stays on **`EncounterCompletionOperations`** + **`rewardGrantSummary`** on encounter-progress GET — **not** migrated through E7.M2 in Slice 2. **`EncounterCompleteEvent`** ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)) remains a future consumer hook. **NEO-43** [`MissionRewardSkillXpGrant`](../../server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpGrant.cs) is the skill-XP apply path for bundle rows with **`sourceKind: mission_reward`**.
|
||||
|
||||
**Upstream (must be landed):** [E7.M1](E7_M1_QuestStateMachine.md) **Ready** ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)–[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)); [E2.M2](E2_M2_XpAwardAndLevelEngine.md) grant stack ([NEO-38](https://linear.app/neon-sprawl/issue/NEO-38), [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43)); [E3.M3](E3_M3_ItemizationAndInventorySchema.md) inventory ([NEO-54](https://linear.app/neon-sprawl/issue/NEO-54)–[NEO-55](https://linear.app/neon-sprawl/issue/NEO-55)). [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) quest HUD for client reward surfacing.
|
||||
|
||||
**Prototype reward spine (frozen in E7M2-01):** **`completionRewardBundle`** on each of the **four** frozen [E7.M1 quests](E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01) — **quest completion only** (not per-step). Grant kinds v1: **`itemGrants`** + **`skillXpGrants`** only. **Solo only** — per-character idempotent keys ([quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md)).
|
||||
|
||||
**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts.
|
||||
|
||||
| Slug | Layer | Linear |
|
||||
|------|-------|--------|
|
||||
| E7M2-01 | server | [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) |
|
||||
| E7M2-02 | server | [NEO-125](https://linear.app/neon-sprawl/issue/NEO-125) |
|
||||
| E7M2-03 | server | [NEO-126](https://linear.app/neon-sprawl/issue/NEO-126) |
|
||||
| E7M2-04 | server | [NEO-127](https://linear.app/neon-sprawl/issue/NEO-127) |
|
||||
| E7M2-05 | server | [NEO-128](https://linear.app/neon-sprawl/issue/NEO-128) |
|
||||
| E7M2-06 | server | [NEO-129](https://linear.app/neon-sprawl/issue/NEO-129) |
|
||||
| E7M2-07 | server | [NEO-130](https://linear.app/neon-sprawl/issue/NEO-130) |
|
||||
| E7M2-08 | client | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) |
|
||||
| E7M2-09 | client | [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132) |
|
||||
|
||||
**Dependency graph in Linear:** E7M2-02 blocked by E7M2-01. E7M2-03 blocked by E7M2-01. E7M2-04 blocked by E7M2-02 and E7M2-03. E7M2-05 blocked by E7M2-04. E7M2-06 blocked by E7M2-05. E7M2-07 blocked by E7M2-05 (may parallel E7M2-06). E7M2-08 blocked by E7M2-06. E7M2-09 blocked by E7M2-08.
|
||||
|
||||
**Board order:** estimates **1–9** matching slug order (E7M2-01 = 1 … E7M2-09 = 9). On the Epic 7 project board, sort by **Estimate** (ascending).
|
||||
|
||||
---
|
||||
|
||||
## Story order (recommended)
|
||||
|
||||
| Order | Slug | Layer | Depends on |
|
||||
|-------|------|-------|------------|
|
||||
| 1 | **E7M2-01** | server | E7.M1 quest catalog frozen |
|
||||
| 2 | **E7M2-02** | server | E7M2-01 |
|
||||
| 3 | **E7M2-03** | server | E7M2-01 |
|
||||
| 4 | **E7M2-04** | server | E7M2-02, E7M2-03 |
|
||||
| 5 | **E7M2-05** | server | E7M2-04 |
|
||||
| 6 | **E7M2-06** | server | E7M2-05 |
|
||||
| 7 | **E7M2-07** | server | E7M2-05 |
|
||||
| 8 | **E7M2-08** | client | E7M2-06 |
|
||||
| 9 | **E7M2-09** | client | E7M2-08 |
|
||||
|
||||
**Downstream (separate modules):** [E6.M4](../decomposition/modules/E6_M4_RewardParityEnforcer.md) lint against bundles; [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) reputation grants (pre-production); E5.M3 **`EncounterCompleteEvent`** full router consumer (optional follow-on).
|
||||
|
||||
---
|
||||
|
||||
## Kickoff decisions (decomposition defaults)
|
||||
|
||||
| Topic | Decision | Rationale |
|
||||
|-------|----------|-----------|
|
||||
| Reward trigger | **Quest completion only** (not per-step) | Slice 2 AC is idempotent **completion** delivery; step bundles add scope without prototype need |
|
||||
| Grant kinds (v1) | **`itemGrants`** + **`skillXpGrants`** (`mission_reward` at apply) | [NEO-43](NEO-43-implementation-plan.md) prep; unlock/flag grants deferred |
|
||||
| Unlock / world gates | **Deferred** to pre-production | Epic mentions unlocks; no prototype gate content yet — comment-only **`unlock_granted`** hook |
|
||||
| Encounter loot | **Unchanged** — E5.M3 direct grants | Avoid breaking [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) / [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); `EncounterCompleteEvent` consumer is follow-on |
|
||||
| Bundle attachment | **`completionRewardBundle`** on each frozen **four** quests | Readable capstone; CI enforces shape on all E7.M1 ids |
|
||||
| Idempotency key | **`{playerId}:quest_complete:{questId}`** | [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md); once per character per quest |
|
||||
| Delivery store | **In-memory** **`IRewardDeliveryStore`** (+ optional Postgres migration) | Matches encounter/quest prototype stores |
|
||||
| Fail-closed | **Quest complete rolls back** if bundle apply fails after mark (inventory full deny) | Mirror [NEO-105](NEO-105-implementation-plan.md) encounter completion compensating rollback |
|
||||
| HTTP read | **Extend** **`GET …/quest-progress`** with **`completionRewardSummary`** per completed quest | Mirror encounter **`rewardGrantSummary`** ([NEO-108](NEO-108-implementation-plan.md)); no new top-level route in Slice 2 |
|
||||
| Party mode | **Solo only** | E8.M1 not landed |
|
||||
| HUD fidelity | Prototype labels / debug panels | NEO-122 / NEO-110 precedent |
|
||||
|
||||
### Prototype completion bundles (E7M2-01 freeze — tunable display only)
|
||||
|
||||
| Quest id | Item grants | Skill XP grants |
|
||||
|----------|-------------|-----------------|
|
||||
| `prototype_quest_gather_intro` | — | **`salvage`** **25** (`mission_reward`) |
|
||||
| `prototype_quest_refine_intro` | — | **`refine`** **25** (`mission_reward`) |
|
||||
| `prototype_quest_combat_intro` | — | **`salvage`** **25** (`mission_reward`) |
|
||||
| `prototype_quest_operator_chain` | **`survey_drone_kit` ×1** | **`salvage`** **50** (`mission_reward`) |
|
||||
|
||||
Combat intro bundle is **skill XP only** — encounter loot already granted **`contract_handoff_token`** via E5.M3 (no double item payout on same milestone).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-01 — QuestRewardBundle schema + quest catalog extension + CI
|
||||
|
||||
**Goal:** Lock **`completionRewardBundle`** content shape and CI validation on the four frozen quests before server load.
|
||||
|
||||
**In scope**
|
||||
|
||||
- `content/schemas/quest-reward-bundle.schema.json`, `content/schemas/quest-skill-xp-grant.schema.json` (reuse or align with existing grant row patterns).
|
||||
- Extend `content/schemas/quest-def.schema.json` with optional **`completionRewardBundle`**.
|
||||
- Update `content/quests/prototype_quests.json` — bundles per freeze table above.
|
||||
- `scripts/validate_content.py`: bundle schema validation, item id cross-refs, skill id cross-refs, `mission_reward` allowlist alignment with `prototype_skills.json`.
|
||||
- Designer note in [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) + `content/README.md`.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Server loader, delivery store, router, HTTP, Godot.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [x] PR gate validates quest JSON including optional completion bundles.
|
||||
- [x] All four prototype quests include a **`completionRewardBundle`** row matching freeze table.
|
||||
- [x] Invalid item/skill refs fail CI.
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-02 — Server quest catalog load: reward bundle validation
|
||||
|
||||
**Goal:** Fail-fast startup validation of **`completionRewardBundle`** with CI parity.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Extend `server/NeonSprawl.Server/Game/Quests/` catalog loader / registry types.
|
||||
- Cross-check bundle grants against item + skill registries at startup.
|
||||
- Unit tests (AAA) for happy path and broken bundle refs.
|
||||
- `server/README.md` section.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Delivery apply, HTTP projection changes.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Host exits on invalid bundle refs at startup.
|
||||
- [ ] `dotnet test` covers loader gates.
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-03 — Reward delivery store (`IRewardDeliveryStore`)
|
||||
|
||||
**Goal:** Durable idempotency record for **`RewardDeliveryEvent`** per player + quest completion.
|
||||
|
||||
**In scope**
|
||||
|
||||
- `IRewardDeliveryStore`, in-memory implementation, optional Postgres migration (NEO-29-style split).
|
||||
- Idempotency key **`{playerId}:quest_complete:{questId}`**; store granted item + skill XP summary for audit/read projection.
|
||||
- Unit tests: first delivery applies; replay returns existing event without double-apply.
|
||||
- `server/README.md` store section.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Router apply logic (E7M2-04), quest-state wiring (E7M2-05).
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] `TryRecord` idempotent on same key.
|
||||
- [ ] Store readable summary for HTTP projection.
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-04 — `RewardRouterOperations` (apply `QuestRewardBundle`)
|
||||
|
||||
**Goal:** Server-internal apply for item + skill XP completion bundles with structured outcomes.
|
||||
|
||||
**In scope**
|
||||
|
||||
- `RewardRouterOperations.TryDeliverQuestCompletion` in `server/NeonSprawl.Server/Game/Quests/` (or `Game/Rewards/`).
|
||||
- Item grants via **`PlayerInventoryOperations`**; skill XP via **`MissionRewardSkillXpGrant`**.
|
||||
- Deny reason codes (inventory full, unknown item/skill, xp deny).
|
||||
- Unit + integration tests (AAA): happy path, inventory deny, idempotent replay via store.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Quest state transition hooks (E7M2-05), HTTP DTOs (E7M2-06).
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Bundle rows apply atomically where possible; compensating rollback on partial failure before store record.
|
||||
- [ ] Skill XP uses **`mission_reward`** source kind only.
|
||||
|
||||
**Client counterpart:** none (server engine).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-05 — Wire router into `QuestStateOperations.TryMarkComplete`
|
||||
|
||||
**Goal:** First-time quest completion triggers bundle delivery exactly once.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Call **`RewardRouterOperations`** from **`TryMarkComplete`** success path (after state commit or transactional equivalent per fail-closed decision).
|
||||
- Skip delivery on idempotent complete replay (already `Completed` status).
|
||||
- Integration tests: complete gather intro → inventory/skills reflect bundle; replay → no duplicate grants.
|
||||
- Bruno smoke in `bruno/neon-sprawl-server/quest-progress/` (optional subfolder).
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Encounter completion path; step-complete bundles.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Completing a quest via objective wiring delivers bundle once.
|
||||
- [ ] Idempotent complete does not double-grant items or XP.
|
||||
|
||||
**Client counterpart:** [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — HUD surfacing (blocked by E7M2-06).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-06 — Extend `GET …/quest-progress` with `completionRewardSummary`
|
||||
|
||||
**Goal:** Client-readable grant summary on completed quest rows (mirror encounter progress).
|
||||
|
||||
**In scope**
|
||||
|
||||
- Extend `QuestProgressApi` DTOs + OpenAPI-stable JSON: per completed quest optional **`completionRewardSummary`** (item lines + skill XP lines).
|
||||
- Populate from **`IRewardDeliveryStore`** / delivery event snapshot.
|
||||
- Integration tests + Bruno assertions on gather intro complete path.
|
||||
- `server/README.md` quest-progress section update.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Godot HUD (E7M2-08).
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Completed quest rows include summary when delivery recorded.
|
||||
- [ ] `not_started` / `active` rows omit summary.
|
||||
|
||||
**Client counterpart:** [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-07 — Reward telemetry hook sites (comment-only)
|
||||
|
||||
**Goal:** Anchor future E9.M1 catalog events without ingest.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Comment-only **`reward_delivery`** in **`RewardRouterOperations`** (first-time success).
|
||||
- Comment-only **`unlock_granted`** stub (no runtime unlock apply in Slice 2).
|
||||
- `server/README.md` hook list.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- E9.M1 ingest, unlock engine.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Hook sites documented in README; no behavior change beyond comments.
|
||||
|
||||
**Client counterpart:** none (infrastructure-only).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-08 — Client quest completion reward HUD (Godot)
|
||||
|
||||
**Goal:** Player sees quest completion rewards in Godot without Bruno.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Extend **`quest_progress_client.gd`** parse for **`completionRewardSummary`**.
|
||||
- **`QuestRewardDeliveryLabel`** (or extend **`QuestProgressLabel`** / **`quest_hud_controller.gd`**) — compact grant lines when quest transitions to **`completed`**.
|
||||
- Refresh on quest-progress GET after objective events (reuse NEO-122 triggers).
|
||||
- GdUnit parse tests; `client/README.md` section.
|
||||
- `docs/manual-qa/NEO-131.md` component checklist.
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Final quest journal UI; reward VFX.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Completing a quest shows readable reward copy on HUD.
|
||||
- [ ] Failed GET surfaces visible error (NEO-122 pattern).
|
||||
|
||||
**Server counterpart:** [NEO-129](https://linear.app/neon-sprawl/issue/NEO-129).
|
||||
|
||||
---
|
||||
|
||||
### E7M2-09 — Playable quest reward delivery capstone (Godot)
|
||||
|
||||
**Goal:** Prove Epic 7 Slice 2 acceptance **in Godot**: quest completion grants items + skill XP **once**; idempotent on replay.
|
||||
|
||||
**In scope**
|
||||
|
||||
- **`docs/manual-qa/NEO-132.md`**: single-session capstone — extend [NEO-123](NEO-123.md) flow or shorter reward-focused path; verify **`completionRewardSummary`** + inventory/skills; Godot restart + duplicate complete attempt unchanged.
|
||||
- **`client/README.md`**: **End-to-end quest reward loop** section; cross-links NEO-124–NEO-131.
|
||||
- Module alignment on completion: [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md), [module_dependency_register.md](../decomposition/modules/module_dependency_register.md).
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Encounter loot migration; faction reputation; Bruno-only proof.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Human completes **`docs/manual-qa/NEO-132.md`** with server + client.
|
||||
- [ ] Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim.
|
||||
- [ ] Re-read [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing).
|
||||
|
||||
**Client counterpart:** this story **is** the Slice 2 client capstone.
|
||||
|
||||
---
|
||||
|
||||
## Decomposition complete checklist
|
||||
|
||||
- [x] Every player-visible AC has a **`client`** Linear issue (NEO-131, NEO-132)
|
||||
- [x] No forbidden deferral language in backlog
|
||||
- [x] Capstone client story exists (E7M2-09 / NEO-132)
|
||||
- [x] Epic DoD updated for Godot verification (see epic_07 update)
|
||||
- [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M2 row updated (decomposition pass)
|
||||
- [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M2 note updated
|
||||
|
||||
## Related docs
|
||||
|
||||
- [epic_07_quest_faction.md](../decomposition/epics/epic_07_quest_faction.md)
|
||||
- [E7M1-prototype-backlog.md](E7M1-prototype-backlog.md)
|
||||
- [NEO-43 implementation plan](NEO-43-implementation-plan.md)
|
||||
- [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md)
|
||||
- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md)
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
# NEO-124 — Implementation plan
|
||||
|
||||
## Story reference
|
||||
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| **Key** | NEO-124 |
|
||||
| **Title** | E7M2-01: QuestRewardBundle schema + quest catalog extension + CI |
|
||||
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-124/e7m2-01-questrewardbundle-schema-quest-catalog-extension-ci |
|
||||
| **Module** | [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-01** |
|
||||
| **Branch** | `NEO-124-e7m2-questrewardbundle-schema-quest-catalog-extension-ci` |
|
||||
| **Precursor** | [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) **Ready** ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)–[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)) · [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43) **`MissionRewardSkillXpGrant`** prep · frozen item + skill catalogs |
|
||||
| **Pattern** | [NEO-112](NEO-112-implementation-plan.md) — extend row JSON Schema + catalog + `scripts/validate_content.py` gate |
|
||||
| **Blocks** | [NEO-125](https://linear.app/neon-sprawl/issue/NEO-125) (server catalog load), [NEO-126](https://linear.app/neon-sprawl/issue/NEO-126) (delivery store) |
|
||||
| **Client counterpart** | none — infrastructure-only ([E7M2-prototype-backlog](E7M2-prototype-backlog.md#e7m2-01--questrewardbundle-schema--quest-catalog-extension--ci)) |
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
| Topic | Question | Agent recommendation | Answer |
|
||||
|--------|----------|----------------------|--------|
|
||||
| **Bundle array keys** | `item_grants` / `skill_xp_grants` (backlog prose) vs camelCase? | **`itemGrants` + `skillXpGrants`** — matches repo JSON (`fixedGrants`, `prerequisiteQuestIds`, `completionRewardBundle`). | **Adopted** — camelCase |
|
||||
| **Skill XP `sourceKind` in content** | Explicit `sourceKind: mission_reward` per row vs omit? | **Omit** — v1 Slice 2 is `mission_reward` only; CI checks target skill **`allowedXpSourceKinds`** includes `mission_reward`; apply path hardcodes via **`MissionRewardSkillXpGrant`** (NEO-43). | **Adopted** — omit from content |
|
||||
|
||||
**Additional defaults (no kickoff question — settled by backlog / precedent):**
|
||||
|
||||
- **Item grant rows:** `$ref` existing [`reward-grant-row.schema.json`](../../content/schemas/reward-grant-row.schema.json) (`itemId` + `quantity`) — same shape as E5.M3 `fixedGrants`.
|
||||
- **Bundle required on all four quests:** CI gate enforces presence + exact freeze-table contents (not optional omission on prototype ids).
|
||||
- **Empty `itemGrants`:** omit property when no item rows (XP-only quests); schema allows optional `itemGrants` with `minItems: 0` when present.
|
||||
- **At least one grant:** bundle must contain ≥1 item or skill XP row (`skillXpGrants` non-empty for all four frozen quests per freeze table).
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** Lock **`completionRewardBundle`** content shape and CI validation on the four frozen E7.M1 quests before server load (NEO-125+).
|
||||
|
||||
**In scope (from Linear + [E7M2-01](E7M2-prototype-backlog.md#e7m2-01--questrewardbundle-schema--quest-catalog-extension--ci)):**
|
||||
|
||||
- `content/schemas/quest-reward-bundle.schema.json`, `content/schemas/quest-skill-xp-grant.schema.json`.
|
||||
- Extend [`quest-def.schema.json`](../../content/schemas/quest-def.schema.json) with optional **`completionRewardBundle`** (`$ref` bundle schema).
|
||||
- Update [`prototype_quests.json`](../../content/quests/prototype_quests.json) — bundles per freeze table (below).
|
||||
- [`scripts/validate_content.py`](../../scripts/validate_content.py): bundle schema validation via extended quest validator registry; item id cross-refs; skill id cross-refs; **`mission_reward`** allowlist alignment (skill row must list `mission_reward` in **`allowedXpSourceKinds`**); E7M2 prototype bundle freeze gate.
|
||||
- Designer note in [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) (camelCase array keys + CI line) + [`content/README.md`](../../content/README.md) E7 Slice 2 paragraph + [CT.M1](../decomposition/modules/CT_M1_ContentValidationPipeline.md) quest-bundle PR gate paragraph.
|
||||
|
||||
**Out of scope (from Linear):**
|
||||
|
||||
- Server loader, delivery store, router, HTTP, Godot (NEO-125+).
|
||||
- **`sourceKind`** field in content rows; unlock/flag grant kinds.
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [x] PR gate validates quest JSON including optional completion bundles.
|
||||
- [x] All four prototype quests include **`completionRewardBundle`** matching freeze table.
|
||||
- [x] Invalid item/skill refs fail CI.
|
||||
|
||||
## Implementation reconciliation (shipped)
|
||||
|
||||
- **Schemas:** [`quest-skill-xp-grant.schema.json`](../../content/schemas/quest-skill-xp-grant.schema.json), [`quest-reward-bundle.schema.json`](../../content/schemas/quest-reward-bundle.schema.json); [`quest-def.schema.json`](../../content/schemas/quest-def.schema.json) extended with optional **`completionRewardBundle`**.
|
||||
- **Catalog:** [`prototype_quests.json`](../../content/quests/prototype_quests.json) — four **`completionRewardBundle`** rows per E7.M2 freeze table (`itemGrants` / `skillXpGrants` camelCase).
|
||||
- **CI:** `PROTOTYPE_E7M2_COMPLETION_BUNDLES`, presence/freeze/cross-ref gates in [`validate_content.py`](../../scripts/validate_content.py); **`mission_reward`** allowlist check via skill **`allowedXpSourceKinds`**.
|
||||
- **Docs:** E7.M2 module CI line + camelCase keys; `content/README.md` E7 Slice 2 paragraph; CT.M1 quest-bundle bullet; E7M2-01 backlog checkboxes; alignment register E7.M2 row.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### 1. `quest-skill-xp-grant.schema.json`
|
||||
|
||||
Draft 2020-12 object; `additionalProperties: false`. Required: **`skillId`** (pattern `^[a-z][a-z0-9_]*$`), **`amount`** (integer ≥ 1). No **`sourceKind`** — apply path uses **`MissionRewardSkillXpGrant`** (NEO-43).
|
||||
|
||||
### 2. `quest-reward-bundle.schema.json`
|
||||
|
||||
Draft 2020-12 object; `additionalProperties: false`. Optional: **`itemGrants`** (array of `$ref` reward-grant-row), **`skillXpGrants`** (array of `$ref` quest-skill-xp-grant). At least one array must be present with ≥1 row (enforce in schema via `anyOf` / `minItems` or post-schema CI helper). Omit **`itemGrants`** when empty (XP-only quests).
|
||||
|
||||
### 3. Extend `quest-def.schema.json`
|
||||
|
||||
Add optional property **`completionRewardBundle`** → `$ref` quest-reward-bundle. Remaining required fields unchanged.
|
||||
|
||||
### 4. Extend `_quest_def_validator()` registry
|
||||
|
||||
Register **`quest-reward-bundle.schema.json`** and **`quest-skill-xp-grant.schema.json`** (and existing **`reward-grant-row.schema.json`**) in the Draft 2020-12 `$ref` registry alongside step/objective schemas.
|
||||
|
||||
### 5. Catalog — `content/quests/prototype_quests.json`
|
||||
|
||||
Add **`completionRewardBundle`** to each of the four rows:
|
||||
|
||||
| Quest id | `itemGrants` | `skillXpGrants` |
|
||||
|----------|--------------|-----------------|
|
||||
| `prototype_quest_gather_intro` | *(omit)* | `salvage` **25** |
|
||||
| `prototype_quest_refine_intro` | *(omit)* | `refine` **25** |
|
||||
| `prototype_quest_combat_intro` | *(omit)* | `salvage` **25** |
|
||||
| `prototype_quest_operator_chain` | `survey_drone_kit` ×1 | `salvage` **50** |
|
||||
|
||||
Example (gather intro):
|
||||
|
||||
```json
|
||||
"completionRewardBundle": {
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 25 }]
|
||||
}
|
||||
```
|
||||
|
||||
Example (operator chain):
|
||||
|
||||
```json
|
||||
"completionRewardBundle": {
|
||||
"itemGrants": [{ "itemId": "survey_drone_kit", "quantity": 1 }],
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 50 }]
|
||||
}
|
||||
```
|
||||
|
||||
### 6. `validate_content.py` constants + gates
|
||||
|
||||
| Constant | Purpose |
|
||||
|----------|---------|
|
||||
| `PROTOTYPE_E7M2_COMPLETION_BUNDLES` | Map quest id → normalized `{ "itemGrants": [...], "skillXpGrants": [...] }` matching freeze table (keep in sync with E7.M2 module doc + future NEO-125 loader) |
|
||||
| `PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND` | `"mission_reward"` — used in cross-ref gate only |
|
||||
|
||||
**New validators (run after quest schema pass, alongside existing E7M1 gates):**
|
||||
|
||||
- **`_prototype_e7m2_completion_bundle_presence_gate`:** every id in `PROTOTYPE_E7M1_QUEST_IDS` must have **`completionRewardBundle`** object.
|
||||
- **`_prototype_e7m2_completion_bundle_freeze_gate`:** bundle contents must exactly match `PROTOTYPE_E7M2_COMPLETION_BUNDLES` (sorted grant rows for stable compare).
|
||||
- **`_prototype_e7m2_completion_bundle_cross_ref_gate`:** each **`itemGrants[].itemId`** ∈ frozen item ids; each **`skillXpGrants[].skillId`** ∈ frozen skill ids; each target skill’s **`allowedXpSourceKinds`** must include **`mission_reward`** (load skill rows from validated skill catalog or reuse existing skill id map if available in `main()` scope).
|
||||
|
||||
Extend module docstring (quest catalogs bullet) and startup schema file existence checks for new schema paths.
|
||||
|
||||
### 7. Docs
|
||||
|
||||
- **`E7_M2_RewardAndUnlockRouter.md`** — update freeze prose to **`itemGrants` / `skillXpGrants`** (camelCase); add CI enforcement line under prototype freeze.
|
||||
- **`content/README.md`** — **Prototype E7 Slice 2 — quest completion bundles (NEO-124)** paragraph: four quests require bundles; cross-ref rules; link E7M2 backlog + this plan.
|
||||
- **`CT_M1_ContentValidationPipeline.md`** — extend quest catalog bullet with completion bundle validation.
|
||||
- **`E7M2-prototype-backlog.md`** — E7M2-01 checkboxes when implementation completes.
|
||||
- **`documentation_and_implementation_alignment.md`** — E7.M2 row note NEO-124 catalog in progress / landed.
|
||||
|
||||
### 8. No server/C#/client changes in this story.
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `content/schemas/quest-skill-xp-grant.schema.json` | Single skill XP row on a quest completion bundle (`skillId`, `amount`). |
|
||||
| `content/schemas/quest-reward-bundle.schema.json` | Composite bundle (`itemGrants`, `skillXpGrants`) attached to `QuestDef`. |
|
||||
| `docs/plans/NEO-124-implementation-plan.md` | This plan. |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `content/schemas/quest-def.schema.json` | Add optional **`completionRewardBundle`** property referencing bundle schema. |
|
||||
| `content/quests/prototype_quests.json` | Add **`completionRewardBundle`** to all four frozen quest rows per freeze table. |
|
||||
| `scripts/validate_content.py` | New schema paths; extended quest `$ref` registry; E7M2 bundle presence/freeze/cross-ref gates; docstring + schema existence checks. |
|
||||
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | camelCase grant keys + CI enforcement note under prototype freeze. |
|
||||
| `content/README.md` | E7 Slice 2 quest completion bundle paragraph (NEO-124). |
|
||||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for quest completion bundles. |
|
||||
| `docs/plans/E7M2-prototype-backlog.md` | Mark E7M2-01 AC complete when shipped. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M2 alignment row — NEO-124 catalog landed. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Target | Coverage |
|
||||
|--------|----------|
|
||||
| **No new automated test files** | Content-only slice; regression surface is **`scripts/validate_content.py`** invoked by PR gate (NEO-112 / NEO-100 precedent). |
|
||||
| **Manual verification** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — must pass with four bundles. Spot-check failures: typo **`itemId`**, unknown **`skillId`**, skill without **`mission_reward`** in **`allowedXpSourceKinds`**, missing **`completionRewardBundle`** on a quest row, wrong XP amount vs freeze table. |
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
| Question or risk | Agent recommendation | Status |
|
||||
|------------------|----------------------|--------|
|
||||
| Constants drift vs future NEO-125 C# loader | Sync comment on `PROTOTYPE_E7M2_COMPLETION_BUNDLES` pointing at E7.M2 module freeze + future server rules class. | **adopted** |
|
||||
| Backlog prose still says `item_grants` / `skill_xp_grants` | Update E7M2 backlog + E7.M2 module to camelCase when implementing (kickoff decision). | **adopted** |
|
||||
| Bundle compare stability (grant row order) | Normalize/sort grant rows in freeze gate before compare. | **adopted** |
|
||||
|
||||
None beyond the above.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# Code review — NEO-124 (E7M2-01)
|
||||
|
||||
**Date:** 2026-06-07
|
||||
**Scope:** Branch `NEO-124-e7m2-questrewardbundle-schema-quest-catalog-extension-ci` vs `origin/main` — commits `85ead01` … `c8e7449`
|
||||
**Base:** `origin/main`
|
||||
**Follow-up:** Alignment register E7.M2 References column repaired; negative CI spot-checks run (see Verification).
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approve with nits** — blocking documentation corruption fixed; implementation matches plan.
|
||||
|
||||
## Summary
|
||||
|
||||
NEO-124 locks the **`completionRewardBundle`** content contract for Epic 7 Slice 2: two new JSON Schemas (`quest-reward-bundle`, `quest-skill-xp-grant`), optional **`completionRewardBundle`** on `quest-def`, four frozen quest rows in `prototype_quests.json`, and three CI gates in `validate_content.py` (presence, freeze-table match with normalized sort, item/skill cross-refs + **`mission_reward`** allowlist). The work follows the NEO-112 content-only pattern, extends the quest `$ref` registry correctly, and documents the freeze in E7.M2, CT.M1, `content/README.md`, and the implementation plan. `python3 scripts/validate_content.py` passes locally. Risk is low for runtime (no server/client changes), but the corrupted alignment-table row should be fixed before merge.
|
||||
|
||||
## Documentation checked
|
||||
|
||||
| Path | Result |
|
||||
|------|--------|
|
||||
| `docs/plans/NEO-124-implementation-plan.md` | **Matches** — kickoff decisions (camelCase keys, omit `sourceKind`), acceptance criteria checked, reconciliation accurate. |
|
||||
| `docs/plans/E7M2-prototype-backlog.md` (E7M2-01) | **Matches** — AC checkboxes complete; infrastructure-only client counterpart noted. |
|
||||
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | **Matches** — Slice 2 freeze table, camelCase keys, CI enforcement line. |
|
||||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | **Matches** — quest completion bundle PR-gate bullet added. |
|
||||
| `content/README.md` | **Matches** — E7 Slice 2 paragraph with cross-ref rules and links. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | ~~**Conflicts** — E7.M2 row **References** column corrupted~~ **Matches** — References cell repaired (follow-up). |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E7.M2 note between table rows (same pattern as E7.M1); row stays **Planned** until NEO-132. |
|
||||
| `docs/decomposition/epics/epic_07_quest_faction.md` | **Matches** — Slice 2 backlog link + Godot DoD line for NEO-132. |
|
||||
| Full-stack epic decomposition | **N/A** — E7M2-01 is infrastructure-only; no client counterpart required. |
|
||||
|
||||
**Register / tracking:** E7.M2 correctly remains **Planned** until capstone NEO-132; alignment row content is otherwise appropriate once corruption is fixed.
|
||||
|
||||
## Blocking issues
|
||||
|
||||
1. ~~**Corrupted E7.M2 row in `documentation_and_implementation_alignment.md`** — line 62 ends the References column with a stray fragment:~~ **Done.** Replaced References cell with clean link list per review suggestion; table renders as one row.
|
||||
|
||||
## Suggestions
|
||||
|
||||
1. ~~**Spot-check negative CI paths before merge** — Plan lists manual failure cases; run at least one each (typo `itemId`, wrong XP amount, skill missing `mission_reward`) to confirm gate messages are actionable.~~ **Done.** Ran three temporary mutations (restored after each): operator chain item typo → freeze gate exit 1; gather intro amount **24** → freeze gate exit 1; salvage without `mission_reward` → allowlist cross-ref exit 1. Note: item typo is caught by freeze gate before item cross-ref (gate order: presence → freeze → cross-ref).
|
||||
|
||||
2. **NEO-125 sync comment** — `PROTOTYPE_E7M2_COMPLETION_BUNDLES` already points at E7.M2 freeze; when server loader lands, mirror the constant in C# with an explicit cross-link comment (plan risk row already tracks this).
|
||||
|
||||
## Nits
|
||||
|
||||
- Nit: `_prototype_e7m2_completion_bundle_cross_ref_gate` skips malformed grant dicts silently (same pattern as other E7M1 gates); schema validation already rejects them — consistent, no change needed.
|
||||
- Nit: `quest-reward-bundle.schema.json` `anyOf` allows `itemGrants: []` when `skillXpGrants` is non-empty; catalog correctly omits empty `itemGrants` per kickoff decision.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
python3 scripts/validate_content.py
|
||||
```
|
||||
|
||||
Optional negative checks (expect non-zero exit + stderr message):
|
||||
|
||||
- Temporarily typo `survey_drone_kit` → confirm item cross-ref error.
|
||||
- Temporarily set gather intro `amount` to **24** → confirm freeze gate error.
|
||||
- Temporarily remove `mission_reward` from a skill's `allowedXpSourceKinds` → confirm allowlist error.
|
||||
|
||||
After fixing blocking issue **1**, re-read the E7.M2 alignment row in a Markdown preview to confirm the table renders as one row.
|
||||
|
||||
**Follow-up verification:** `python3 scripts/validate_content.py` passes on clean tree; negative spot-checks above executed in follow-up (see Suggestions **1**).
|
||||
|
|
@ -14,7 +14,8 @@ Validates:
|
|||
- npc behavior catalogs: content/npc-behaviors/*_npc_behaviors.json rows vs content/schemas/npc-behavior-def.schema.json (NEO-87)
|
||||
- reward table catalogs: content/reward-tables/*_reward_tables.json rows vs content/schemas/reward-table.schema.json (NEO-100)
|
||||
- encounter catalogs: content/encounters/*_encounters.json rows vs content/schemas/encounter-def.schema.json (NEO-100)
|
||||
- quest catalogs: content/quests/*_quests.json rows vs content/schemas/quest-def.schema.json (NEO-112)
|
||||
- quest catalogs: content/quests/*_quests.json rows vs content/schemas/quest-def.schema.json (NEO-112);
|
||||
optional completionRewardBundle via quest-reward-bundle.schema.json (NEO-124)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -43,6 +44,8 @@ ENCOUNTER_DEF_SCHEMA = REPO_ROOT / "content/schemas/encounter-def.schema.json"
|
|||
QUEST_OBJECTIVE_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-objective-def.schema.json"
|
||||
QUEST_STEP_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-step-def.schema.json"
|
||||
QUEST_DEF_SCHEMA = REPO_ROOT / "content/schemas/quest-def.schema.json"
|
||||
QUEST_REWARD_BUNDLE_SCHEMA = REPO_ROOT / "content/schemas/quest-reward-bundle.schema.json"
|
||||
QUEST_SKILL_XP_GRANT_SCHEMA = REPO_ROOT / "content/schemas/quest-skill-xp-grant.schema.json"
|
||||
SKILLS_DIR = REPO_ROOT / "content/skills"
|
||||
MASTERY_DIR = REPO_ROOT / "content/mastery"
|
||||
ITEMS_DIR = REPO_ROOT / "content/items"
|
||||
|
|
@ -170,6 +173,56 @@ PROTOTYPE_E7M1_QUEST_IDS = frozenset(
|
|||
PROTOTYPE_E7M1_CHAIN_QUEST_ID = "prototype_quest_operator_chain"
|
||||
PROTOTYPE_E7M1_CHAIN_TERMINAL_ITEM_ID = "contract_handoff_token"
|
||||
|
||||
# Epic 7 Slice 2 prototype lock (NEO-124): completionRewardBundle on four frozen quests.
|
||||
# Keep in sync with E7.M2 freeze table and future NEO-125 server loader.
|
||||
PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND = "mission_reward"
|
||||
PROTOTYPE_E7M2_COMPLETION_BUNDLES: dict[str, dict] = {
|
||||
"prototype_quest_gather_intro": {
|
||||
"itemGrants": [],
|
||||
"skillXpGrants": [{"skillId": "salvage", "amount": 25}],
|
||||
},
|
||||
"prototype_quest_refine_intro": {
|
||||
"itemGrants": [],
|
||||
"skillXpGrants": [{"skillId": "refine", "amount": 25}],
|
||||
},
|
||||
"prototype_quest_combat_intro": {
|
||||
"itemGrants": [],
|
||||
"skillXpGrants": [{"skillId": "salvage", "amount": 25}],
|
||||
},
|
||||
"prototype_quest_operator_chain": {
|
||||
"itemGrants": [{"itemId": "survey_drone_kit", "quantity": 1}],
|
||||
"skillXpGrants": [{"skillId": "salvage", "amount": 50}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_completion_bundle(bundle: dict) -> dict:
|
||||
"""Normalize grant rows for stable freeze-table comparison."""
|
||||
item_grants = bundle.get("itemGrants")
|
||||
skill_xp_grants = bundle.get("skillXpGrants")
|
||||
return {
|
||||
"itemGrants": sorted(
|
||||
[
|
||||
{"itemId": grant["itemId"], "quantity": grant["quantity"]}
|
||||
for grant in (item_grants if isinstance(item_grants, list) else [])
|
||||
if isinstance(grant, dict)
|
||||
and isinstance(grant.get("itemId"), str)
|
||||
and isinstance(grant.get("quantity"), int)
|
||||
],
|
||||
key=lambda grant: (grant["itemId"], grant["quantity"]),
|
||||
),
|
||||
"skillXpGrants": sorted(
|
||||
[
|
||||
{"skillId": grant["skillId"], "amount": grant["amount"]}
|
||||
for grant in (skill_xp_grants if isinstance(skill_xp_grants, list) else [])
|
||||
if isinstance(grant, dict)
|
||||
and isinstance(grant.get("skillId"), str)
|
||||
and isinstance(grant.get("amount"), int)
|
||||
],
|
||||
key=lambda grant: (grant["skillId"], grant["amount"]),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _prototype_slice1_gate(seen_ids: dict[str, str], id_to_category: dict[str, str]) -> str | None:
|
||||
"""Return a human-readable error if Slice 1 contract fails, else None."""
|
||||
|
|
@ -842,15 +895,21 @@ def _prototype_e5m3_encounter_cross_ref_gate(id_to_row: dict[str, dict]) -> str
|
|||
|
||||
|
||||
def _quest_def_validator() -> Draft202012Validator:
|
||||
"""Build a validator that resolves quest-def $ref to step and objective schemas."""
|
||||
"""Build a validator that resolves quest-def $ref to step, objective, and bundle schemas."""
|
||||
objective_schema = json.loads(QUEST_OBJECTIVE_DEF_SCHEMA.read_text(encoding="utf-8"))
|
||||
step_schema = json.loads(QUEST_STEP_DEF_SCHEMA.read_text(encoding="utf-8"))
|
||||
def_schema = json.loads(QUEST_DEF_SCHEMA.read_text(encoding="utf-8"))
|
||||
bundle_schema = json.loads(QUEST_REWARD_BUNDLE_SCHEMA.read_text(encoding="utf-8"))
|
||||
skill_xp_grant_schema = json.loads(QUEST_SKILL_XP_GRANT_SCHEMA.read_text(encoding="utf-8"))
|
||||
grant_row_schema = json.loads(REWARD_GRANT_ROW_SCHEMA.read_text(encoding="utf-8"))
|
||||
registry = Registry().with_resources(
|
||||
[
|
||||
(def_schema["$id"], Resource.from_contents(def_schema)),
|
||||
(step_schema["$id"], Resource.from_contents(step_schema)),
|
||||
(objective_schema["$id"], Resource.from_contents(objective_schema)),
|
||||
(bundle_schema["$id"], Resource.from_contents(bundle_schema)),
|
||||
(skill_xp_grant_schema["$id"], Resource.from_contents(skill_xp_grant_schema)),
|
||||
(grant_row_schema["$id"], Resource.from_contents(grant_row_schema)),
|
||||
]
|
||||
)
|
||||
return Draft202012Validator(def_schema, registry=registry)
|
||||
|
|
@ -1071,6 +1130,82 @@ def _prototype_e7m1_chain_terminal_gate(id_to_row: dict[str, dict]) -> str | Non
|
|||
return None
|
||||
|
||||
|
||||
def _prototype_e7m2_completion_bundle_presence_gate(id_to_row: dict[str, dict]) -> str | None:
|
||||
"""Return a human-readable error if a frozen quest lacks completionRewardBundle, else None."""
|
||||
for qid in sorted(PROTOTYPE_E7M1_QUEST_IDS):
|
||||
row = id_to_row.get(qid)
|
||||
if not isinstance(row, dict):
|
||||
return f"error: missing quest {qid!r}"
|
||||
bundle = row.get("completionRewardBundle")
|
||||
if not isinstance(bundle, dict):
|
||||
return f"error: quest {qid!r} must include completionRewardBundle object"
|
||||
return None
|
||||
|
||||
|
||||
def _prototype_e7m2_completion_bundle_freeze_gate(id_to_row: dict[str, dict]) -> str | None:
|
||||
"""Return a human-readable error if bundle contents diverge from E7M2 freeze table, else None."""
|
||||
for qid, expected in PROTOTYPE_E7M2_COMPLETION_BUNDLES.items():
|
||||
row = id_to_row.get(qid)
|
||||
if not isinstance(row, dict):
|
||||
return f"error: missing quest {qid!r}"
|
||||
bundle = row.get("completionRewardBundle")
|
||||
if not isinstance(bundle, dict):
|
||||
return f"error: quest {qid!r} must include completionRewardBundle object"
|
||||
actual = _normalize_completion_bundle(bundle)
|
||||
expected_normalized = _normalize_completion_bundle(expected)
|
||||
if actual != expected_normalized:
|
||||
return (
|
||||
f"error: quest {qid!r} completionRewardBundle must match E7M2 freeze table "
|
||||
f"(expected {expected_normalized!r}, got {actual!r})"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _prototype_e7m2_completion_bundle_cross_ref_gate(
|
||||
id_to_row: dict[str, dict],
|
||||
skill_id_to_allowed_kinds: dict[str, list[str]],
|
||||
) -> str | None:
|
||||
"""Return a human-readable error if bundle refs fail cross-checks, else None."""
|
||||
for qid in sorted(PROTOTYPE_E7M1_QUEST_IDS):
|
||||
row = id_to_row.get(qid)
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
bundle = row.get("completionRewardBundle")
|
||||
if not isinstance(bundle, dict):
|
||||
continue
|
||||
item_grants = bundle.get("itemGrants")
|
||||
if isinstance(item_grants, list):
|
||||
for gi, grant in enumerate(item_grants):
|
||||
if not isinstance(grant, dict):
|
||||
continue
|
||||
item_id = grant.get("itemId")
|
||||
if isinstance(item_id, str) and item_id not in PROTOTYPE_SLICE1_ITEM_IDS:
|
||||
return (
|
||||
f"error: quest {qid!r} completionRewardBundle.itemGrants[{gi}]: itemId "
|
||||
f"{item_id!r} is not in the frozen prototype item catalog"
|
||||
)
|
||||
skill_xp_grants = bundle.get("skillXpGrants")
|
||||
if isinstance(skill_xp_grants, list):
|
||||
for gi, grant in enumerate(skill_xp_grants):
|
||||
if not isinstance(grant, dict):
|
||||
continue
|
||||
skill_id = grant.get("skillId")
|
||||
if isinstance(skill_id, str) and skill_id not in PROTOTYPE_SLICE1_SKILL_IDS:
|
||||
return (
|
||||
f"error: quest {qid!r} completionRewardBundle.skillXpGrants[{gi}]: skillId "
|
||||
f"{skill_id!r} is not in the frozen prototype skill catalog"
|
||||
)
|
||||
allowed_kinds = skill_id_to_allowed_kinds.get(skill_id, [])
|
||||
if PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND not in allowed_kinds:
|
||||
return (
|
||||
f"error: quest {qid!r} completionRewardBundle.skillXpGrants[{gi}]: skillId "
|
||||
f"{skill_id!r} must allow sourceKind "
|
||||
f"{PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND!r} in allowedXpSourceKinds "
|
||||
f"(got {allowed_kinds!r})"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None:
|
||||
"""Return a human-readable error if Slice 4 contract fails, else None."""
|
||||
if len(track_skill_ids) != 1:
|
||||
|
|
@ -1415,6 +1550,12 @@ def main() -> int:
|
|||
if not QUEST_DEF_SCHEMA.is_file():
|
||||
print(f"error: missing schema {QUEST_DEF_SCHEMA}", file=sys.stderr)
|
||||
return 1
|
||||
if not QUEST_REWARD_BUNDLE_SCHEMA.is_file():
|
||||
print(f"error: missing schema {QUEST_REWARD_BUNDLE_SCHEMA}", file=sys.stderr)
|
||||
return 1
|
||||
if not QUEST_SKILL_XP_GRANT_SCHEMA.is_file():
|
||||
print(f"error: missing schema {QUEST_SKILL_XP_GRANT_SCHEMA}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
skill_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8"))
|
||||
skill_validator = Draft202012Validator(skill_schema)
|
||||
|
|
@ -1540,6 +1681,7 @@ def main() -> int:
|
|||
|
||||
seen_ids: dict[str, str] = {}
|
||||
id_to_category: dict[str, str] = {}
|
||||
skill_id_to_allowed_kinds: dict[str, list[str]] = {}
|
||||
errors = 0
|
||||
|
||||
for path in skill_files:
|
||||
|
|
@ -1579,6 +1721,11 @@ def main() -> int:
|
|||
cat = row.get("category")
|
||||
if isinstance(cat, str):
|
||||
id_to_category[sid] = cat
|
||||
allowed_kinds = row.get("allowedXpSourceKinds")
|
||||
if isinstance(allowed_kinds, list):
|
||||
skill_id_to_allowed_kinds[sid] = [
|
||||
kind for kind in allowed_kinds if isinstance(kind, str)
|
||||
]
|
||||
|
||||
for path in curve_files:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
|
@ -1831,6 +1978,24 @@ def main() -> int:
|
|||
print(e7m1_chain_terminal_err, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
e7m2_bundle_presence_err = _prototype_e7m2_completion_bundle_presence_gate(quest_id_to_row)
|
||||
if e7m2_bundle_presence_err:
|
||||
print(e7m2_bundle_presence_err, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
e7m2_bundle_freeze_err = _prototype_e7m2_completion_bundle_freeze_gate(quest_id_to_row)
|
||||
if e7m2_bundle_freeze_err:
|
||||
print(e7m2_bundle_freeze_err, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
e7m2_bundle_cross_ref_err = _prototype_e7m2_completion_bundle_cross_ref_gate(
|
||||
quest_id_to_row,
|
||||
skill_id_to_allowed_kinds,
|
||||
)
|
||||
if e7m2_bundle_cross_ref_err:
|
||||
print(e7m2_bundle_cross_ref_err, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(
|
||||
"content OK: "
|
||||
f"{len(skill_files)} skill catalog file(s), "
|
||||
|
|
|
|||
Loading…
Reference in New Issue