NEO-144: add E7M4-01 contract catalog kickoff plan
Lock ContractTemplate schemas, CI gates, and file lists before server load in NEO-145.pull/184/head
parent
74b6586004
commit
53122f9f2a
|
|
@ -0,0 +1,172 @@
|
|||
# NEO-144 — E7M4-01: ContractTemplate catalog + schemas + CI
|
||||
|
||||
**Linear:** [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144)
|
||||
**Branch:** `NEO-144-e7m4-01-contracttemplate-catalog-schemas-ci`
|
||||
**Backlog:** [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) — **E7M4-01**
|
||||
**Module:** [E7_M4_ContractMissionGenerator.md](../decomposition/modules/E7_M4_ContractMissionGenerator.md)
|
||||
**Pattern:** [NEO-133](NEO-133-implementation-plan.md) (faction/quest schemas + catalog + `validate_content.py`); [NEO-124](NEO-124-implementation-plan.md) (`completionRewardBundle` reuse)
|
||||
**Precursors:** [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) **`Ready`** — **`prototype_combat_pocket`**; [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) **`Ready`** — **`quest-reward-bundle`** / grant rows; [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) **`Ready`** — faction catalog + rep grants
|
||||
**Blocks:** [NEO-145](https://linear.app/neon-sprawl/issue/NEO-145) (server contract template catalog load)
|
||||
**Client counterpart:** none (infrastructure-only); playable loop is **NEO-154** (E7M4-11)
|
||||
|
||||
## Goal
|
||||
|
||||
Lock contract content shape and CI validation before server load (NEO-145+).
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
Decisions are frozen in [E7M4-pre-production-backlog.md — Kickoff decisions](E7M4-pre-production-backlog.md#kickoff-decisions) and [E7_M4 — Prototype Slice 4 freeze](../decomposition/modules/E7_M4_ContractMissionGenerator.md#prototype-slice-4-freeze-e7m4-01). No blocking `AskQuestion` items.
|
||||
|
||||
| Topic | Question | Agent recommendation | Answer |
|
||||
|-------|----------|----------------------|--------|
|
||||
| — | — | — | **No blocking decisions** — freeze table, band caps, single-template roster, and `$ref` reuse are fully specified in backlog + module doc. |
|
||||
|
||||
**Additional defaults (no kickoff question — settled by backlog / landed code):**
|
||||
|
||||
- **JSON naming:** camelCase (`encounterTemplateId`, `zoneDifficultyBand`, `minFactionStanding`, `completionRewardBundle`, `itemGrants`, `skillXpGrants`) — matches quest/faction catalogs.
|
||||
- **`minFactionStanding` shape:** `$ref` [`faction-gate-rule.schema.json`](../../content/schemas/faction-gate-rule.schema.json) (`factionId` + `minStanding`) — prototype row **`prototype_faction_grid_operators`** / **0** (open issuance).
|
||||
- **`objectiveKind`:** required enum **`encounter_clear`** only in Slice 4 v1; pairs with **`encounterTemplateId`** (not quest-style objective graph).
|
||||
- **Catalog envelope:** **`schemaVersion`: 1**, top-level **`contractTemplates`** array — mirrors quest/faction file layout.
|
||||
- **Band caps in CI (E7M4-01):** implement **`PROTOTYPE_E7M4_BAND1_*`** constants + `_prototype_e7m4_band_cap_gate` in `validate_content.py` now (band **1**: item qty ≤ **10**, skill XP ≤ **25**, rep ≤ **10** per grant row); E7M4-07 reuses same policy at server issue time.
|
||||
- **Over-cap AC proof:** lint runs on live catalog (at-cap template passes); tampering any grant above cap fails `python scripts/validate_content.py` — no separate invalid fixture file (NEO-124 / NEO-133 precedent).
|
||||
- **Server loader / stores / HTTP / Godot:** **Out of scope** (NEO-145+).
|
||||
- **`dotnet test`:** **No server changes expected** — contract catalog is not loaded at startup until NEO-145.
|
||||
|
||||
## Scope and out-of-scope
|
||||
|
||||
**In scope (from Linear + [E7M4-01](E7M4-pre-production-backlog.md#e7m4-01--contracttemplate-catalog--schemas--ci)):**
|
||||
|
||||
- `content/schemas/contract-template.schema.json`, `content/schemas/contract-seed.schema.json` (issue/audit request shape for NEO-147+; no catalog JSON file in this story).
|
||||
- Reuse **`quest-reward-bundle`** / **`reputation-grant-row`** / **`reward-grant-row`** / **`quest-skill-xp-grant`** `$ref`s for **`completionRewardBundle`** on templates.
|
||||
- `content/contracts/prototype_contract_templates.json` — one frozen template row per module freeze table.
|
||||
- `scripts/validate_content.py`: contract schema validation; encounter / item / skill / faction cross-refs; single-template allowlist; template + bundle freeze gate; band-cap lint.
|
||||
- Designer note + CI line in [E7_M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md); **`content/README.md`** E7 Slice 4 contracts paragraph; [CT.M1](../decomposition/modules/CT_M1_ContentValidationPipeline.md) contract-catalog PR gate bullet.
|
||||
|
||||
**Out of scope (from Linear + backlog):**
|
||||
|
||||
- Server loader, `IContractTemplateRegistry`, instance store, generator, HTTP, Godot (NEO-145–NEO-154).
|
||||
- Live **`GET …/zone`** read (E4.M1); content-only **`zoneDifficultyBand`** on template + optional seed field.
|
||||
- Full E6.M4 parity map; server **`ContractEconomyValidation`** at issue time (NEO-147 / E7M4-07).
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] PR gate validates contract template JSON.
|
||||
- [ ] Exactly one prototype template id with encounter + bundle cross-refs.
|
||||
- [ ] Bundle exceeding band caps fails CI.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### 1. Prototype contract template freeze (E7M4-01)
|
||||
|
||||
| `ContractTemplate.id` | `zoneDifficultyBand` | `objectiveKind` | `encounterTemplateId` | `minFactionStanding` | `completionRewardBundle` |
|
||||
|-------------------------|----------------------|-----------------|-------------------------|----------------------|---------------------------|
|
||||
| **`prototype_contract_clear_combat_pocket`** | **1** | **`encounter_clear`** | **`prototype_combat_pocket`** | **`prototype_faction_grid_operators`** **0** | **`scrap_metal_bulk` ×5**; **`salvage`** **15** (`mission_reward`) |
|
||||
|
||||
**Band 1 caps (CI + future E7M4-07):** max **10** per item grant quantity; max **25** skill XP per grant; max **10** rep per grant row.
|
||||
|
||||
### 2. `contract-template.schema.json`
|
||||
|
||||
Draft 2020-12 object; `additionalProperties: false`. Required:
|
||||
|
||||
- **`id`** — pattern `^[a-z][a-z0-9_]*$`
|
||||
- **`displayName`** — non-empty string
|
||||
- **`zoneDifficultyBand`** — integer ≥ 1
|
||||
- **`objectiveKind`** — enum **`encounter_clear`** (Slice 4 v1 only)
|
||||
- **`encounterTemplateId`** — pattern `^[a-z][a-z0-9_]*$`
|
||||
- **`minFactionStanding`** — `$ref` faction-gate-rule (prototype uses Grid Operators **0**)
|
||||
- **`completionRewardBundle`** — `$ref` quest-reward-bundle
|
||||
|
||||
### 3. `contract-seed.schema.json`
|
||||
|
||||
Draft 2020-12 object for future **`POST …/contracts/issue`** body / audit (NEO-147+). Required: **`playerId`**, **`templateId`**, **`seedBucket`** (non-empty string — date bucket or client counter). Optional: **`zoneDifficultyBand`** (integer ≥ 1; prototype default **1** when omitted). No catalog file in E7M4-01 — schema-only so CI can `$ref` it from docs/tests later.
|
||||
|
||||
### 4. Catalog — `content/contracts/prototype_contract_templates.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"contractTemplates": [
|
||||
{
|
||||
"id": "prototype_contract_clear_combat_pocket",
|
||||
"displayName": "Clear Combat Pocket (Repeat)",
|
||||
"zoneDifficultyBand": 1,
|
||||
"objectiveKind": "encounter_clear",
|
||||
"encounterTemplateId": "prototype_combat_pocket",
|
||||
"minFactionStanding": {
|
||||
"factionId": "prototype_faction_grid_operators",
|
||||
"minStanding": 0
|
||||
},
|
||||
"completionRewardBundle": {
|
||||
"itemGrants": [{ "itemId": "scrap_metal_bulk", "quantity": 5 }],
|
||||
"skillXpGrants": [{ "skillId": "salvage", "amount": 15 }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `validate_content.py` gates
|
||||
|
||||
Add **`CONTRACTS_DIR`**, schema paths, **`PROTOTYPE_E7M4_TEMPLATE_IDS`**, **`PROTOTYPE_E7M4_TEMPLATE_FREEZE`**, **`PROTOTYPE_E7M4_BAND1_CAPS`**.
|
||||
|
||||
1. **`_contract_template_validator()`** — registry with contract-template + quest-reward-bundle + nested grant `$ref`s (same pattern as `_quest_def_validator`).
|
||||
2. **`_validate_contract_catalogs`** — load `content/contracts/*_contract_templates.json`; schema validate each row; reject duplicate **`id`**; require **`schemaVersion` 1**.
|
||||
3. **`_prototype_e7m4_template_roster_gate`** — exactly one id: **`prototype_contract_clear_combat_pocket`**.
|
||||
4. **`_prototype_e7m4_template_freeze_gate`** — row matches freeze table (band, objective kind, encounter id, min standing, bundle contents).
|
||||
5. **`_prototype_e7m4_cross_ref_gate`** — **`encounterTemplateId`** ∈ loaded encounter catalog; bundle **`itemId`** / **`skillId`** ∈ frozen item/skill catalogs; **`minFactionStanding.factionId`** ∈ faction catalog; skill XP targets list **`mission_reward`** in **`allowedXpSourceKinds`** (E7M2 rule).
|
||||
6. **`_prototype_e7m4_band_cap_gate`** — for each template, compare **`zoneDifficultyBand`** to cap table; fail when any item qty, skill XP amount, or rep amount exceeds band limits (prototype band **1** only).
|
||||
|
||||
Wire into **`main()`** after encounter + faction validation (cross-refs need encounter and faction maps) and before or after quest validation (order agnostic; prefer after faction, alongside quest bundle rules).
|
||||
|
||||
Update module docstring at top of `validate_content.py` to list contract catalogs (NEO-144).
|
||||
|
||||
### 6. Documentation
|
||||
|
||||
- **`content/README.md`:** add **`contracts/`** table row + **Prototype E7 Slice 4 — contracts (NEO-144)** paragraph (one template id, encounter cross-ref, repeat payout bundle, band-cap CI).
|
||||
- **`E7_M4_ContractMissionGenerator.md`:** add CI landed line under freeze section when complete (catalog + schemas + validate_content gates).
|
||||
- **`CT_M1_ContentValidationPipeline.md`:** bullet for contract template validation (schema, roster, freeze, cross-refs, band caps).
|
||||
|
||||
### 7. Implementation order
|
||||
|
||||
1. Schemas (`contract-template`, `contract-seed`).
|
||||
2. `prototype_contract_templates.json`.
|
||||
3. `validate_content.py` validator + gates + constants.
|
||||
4. Docs (`content/README.md`, E7_M4, CT.M1).
|
||||
5. Run `python3 scripts/validate_content.py` and `dotnet test` (expect green — no server catalog load yet).
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `docs/plans/NEO-144-implementation-plan.md` | This plan. |
|
||||
| `content/schemas/contract-template.schema.json` | `ContractTemplate` row shape with `$ref`s to bundle + faction gate rule. |
|
||||
| `content/schemas/contract-seed.schema.json` | Issue/audit request shape for NEO-147+ (schema-only in this story). |
|
||||
| `content/contracts/prototype_contract_templates.json` | One frozen template row per E7M4-01 freeze table. |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `scripts/validate_content.py` | Contract catalog load, schema registry, roster/freeze/cross-ref/band-cap gates; module docstring. |
|
||||
| `content/README.md` | Document `content/contracts/`, new schemas, Slice 4 prototype template + band-cap CI. |
|
||||
| `docs/decomposition/modules/E7_M4_ContractMissionGenerator.md` | CI/catalog landed note under freeze section. |
|
||||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for contract templates. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Layer | What |
|
||||
|-------|------|
|
||||
| CI | `python3 scripts/validate_content.py` — primary AC surface (schema, roster, freeze, cross-refs, band caps on live catalog). |
|
||||
| Manual negative | Edit template bundle to **`scrap_metal_bulk` ×11** (or **`salvage` 26**) locally → validator exits non-zero (documents over-cap AC without committed bad data). |
|
||||
| Server | **`dotnet test`** — regression only; no new `[Fact]` expected (no server contract loader until NEO-145). |
|
||||
|
||||
Manual Godot QA: **none** (content-only; capstone is NEO-154).
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
| Question / risk | Agent recommendation | Status |
|
||||
|-----------------|----------------------|--------|
|
||||
| **Band caps in E7M4-01 vs E7M4-07** | CI lint in E7M4-01; server **`TryIssue`** re-validates in E7M4-07 with shared cap constants ported to C#. | **adopted** |
|
||||
| **`contract-seed` without catalog JSON** | Schema-only in E7M4-01 per backlog; validated when HTTP DTOs land in NEO-147. | **adopted** |
|
||||
| **`minFactionStanding` optional on future templates** | Required on prototype row; schema requires field for v1 (single open template). Relax to optional when second template lands. | **adopted** |
|
||||
| **Rep grants on contract bundles** | Prototype bundle is item + skill XP only; band-cap gate still checks **`reputationGrants`** if present (forward-compatible). | **adopted** |
|
||||
Loading…
Reference in New Issue