neon-sprawl/docs/plans/NEO-144-implementation-plan.md

12 KiB
Raw Blame History

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.mdE7M4-01
Module: E7_M4_ContractMissionGenerator.md
Pattern: NEO-133 (faction/quest schemas + catalog + validate_content.py); NEO-124 (completionRewardBundle reuse)
Precursors: E5.M3 Readyprototype_combat_pocket; E7.M2 Readyquest-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.
  • minFactionStanding shape: $ref 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):

  • 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 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; content/README.md E7 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-145NEO-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

{
  "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 $refs (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_gateencounterTemplateId ∈ 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 $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