13 KiB
NEO-144 — E7M4-01: ContractTemplate catalog + schemas + CI
Linear: NEO-144
Branch: NEO-144-e7m4-01-contracttemplate-catalog-schemas-ci
Backlog: E7M4-pre-production-backlog.md — E7M4-01
Module: E7_M4_ContractMissionGenerator.md
Pattern: NEO-133 (faction/quest schemas + catalog + validate_content.py); NEO-124 (completionRewardBundle reuse)
Precursors: E5.M3 Ready — prototype_combat_pocket; E7.M2 Ready — quest-reward-bundle / grant rows; E7.M3 Ready — faction catalog + rep grants
Blocks: 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 and E7_M4 — Prototype Slice 4 freeze. 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. minFactionStandingshape:$reffaction-gate-rule.schema.json(factionId+minStanding) — prototype rowprototype_faction_grid_operators/ 0 (open issuance).objectiveKind: required enumencounter_clearonly in Slice 4 v1; pairs withencounterTemplateId(not quest-style objective graph).- Catalog envelope:
schemaVersion: 1, top-levelcontractTemplatesarray — mirrors quest/faction file layout. - Band caps in CI (E7M4-01): implement
PROTOTYPE_E7M4_BAND1_*constants +_prototype_e7m4_band_cap_gateinvalidate_content.pynow (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):
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$refs forcompletionRewardBundleon 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;
content/README.mdE7 Slice 4 contracts paragraph; CT.M1 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 …/zoneread (E4.M1); content-onlyzoneDifficultyBandon template + optional seed field. - Full E6.M4 parity map; server
ContractEconomyValidationat 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.
Implementation reconciliation (shipped)
- Schemas:
contract-template.schema.json,contract-seed.schema.json—$refs toquest-reward-bundle,faction-gate-rule, grant rows. - Catalog:
prototype_contract_templates.json— one frozen template per E7M4-01 freeze table. - CI:
PROTOTYPE_E7M4_*gates invalidate_content.py— roster, freeze, cross-refs, band-1 caps; contract catalog wired into PR gatemain(). - Docs:
content/README.mdE7 Slice 4 paragraph; E7_M4 CI line; CT.M1 contract-catalog bullet; E7M4-01 backlog checkboxes. - Server: no changes (823 tests green — no contract loader until NEO-145).
- Band-cap negative proof:
_prototype_e7m4_band_cap_gaterejectsscrap_metal_bulk×11 locally (not committed).
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 stringzoneDifficultyBand— integer ≥ 1objectiveKind— enumencounter_clear(Slice 4 v1 only)encounterTemplateId— pattern^[a-z][a-z0-9_]*$minFactionStanding—$reffaction-gate-rule (prototype uses Grid Operators 0)completionRewardBundle—$refquest-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
{
"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.
_contract_template_validator()— registry with contract-template + quest-reward-bundle + nested grant$refs (same pattern as_quest_def_validator)._validate_contract_catalogs— loadcontent/contracts/*_contract_templates.json; schema validate each row; reject duplicateid; requireschemaVersion1._prototype_e7m4_template_roster_gate— exactly one id:prototype_contract_clear_combat_pocket._prototype_e7m4_template_freeze_gate— row matches freeze table (band, objective kind, encounter id, min standing, bundle contents)._prototype_e7m4_cross_ref_gate—encounterTemplateId∈ loaded encounter catalog; bundleitemId/skillId∈ frozen item/skill catalogs;minFactionStanding.factionId∈ faction catalog; skill XP targets listmission_rewardinallowedXpSourceKinds(E7M2 rule)._prototype_e7m4_band_cap_gate— for each template, comparezoneDifficultyBandto 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: addcontracts/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
- Schemas (
contract-template,contract-seed). prototype_contract_templates.json.validate_content.pyvalidator + gates + constants.- Docs (
content/README.md, E7_M4, CT.M1). - Run
python3 scripts/validate_content.pyanddotnet 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 $refs 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 |