From 53122f9f2a99214e83b804ee96465ff42d08de0a Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sat, 20 Jun 2026 19:15:43 -0400 Subject: [PATCH] NEO-144: add E7M4-01 contract catalog kickoff plan Lock ContractTemplate schemas, CI gates, and file lists before server load in NEO-145. --- docs/plans/NEO-144-implementation-plan.md | 172 ++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/plans/NEO-144-implementation-plan.md diff --git a/docs/plans/NEO-144-implementation-plan.md b/docs/plans/NEO-144-implementation-plan.md new file mode 100644 index 0000000..bc6eb49 --- /dev/null +++ b/docs/plans/NEO-144-implementation-plan.md @@ -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** |