# CT.M1 — ContentValidationPipeline

## Summary

| Field | Value |
|--------|--------|
| **Module ID** | CT.M1 |
| **Track** | [Content tooling — internal authoring](../tooling/internal_authoring.md) (cross-cutting; not E1–E9) |
| **Stage target** | Prototype |
| **Status** | Planned (see [dependency register](module_dependency_register.md)) |

## Purpose

Own **content schema artifacts**, **automated validation** in CI and locally, and (when wired) **server-side smoke loading** so file-backed catalogs cannot drift from enforced contracts.

## Responsibilities

- Publish and version **JSON Schema** (or agreed equivalent) for catalogs consumed by **E2.M1**, **E3.M2–M3**, **E4.M1**, **E7.M1**.
- Run **structural** and **cross-reference** validation before merge; expose the same rules to **CT.M2**.
- Optional: **C#** deserialize test or boot check aligned with `ContentCatalogRegistry` (or successor).

## Key contracts

| Contract | Role |
|----------|------|
| Content schema files | `$id` + versioning per [contracts.md](contracts.md) |
| Validation CLI | Stable exit codes and machine-readable errors for CI and studio |

## Module dependencies

- **Soft:** Gameplay modules above must define or stabilize the **shapes** being validated; pipeline can ship **minimal** schemas first.

## Dependents (by design)

- **CT.M2** — AuthoringStudioApplication
- **CT.M3** — ContentReferenceAndBundleWorkflows

## Related implementation slices

Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hardening.

## CI (prototype)

**Skill catalogs:** the repo **PR gate** workflow ([`.github/workflows/pr-gate.yml`](../../../.github/workflows/pr-gate.yml)) runs [`scripts/validate_content.py`](../../../scripts/validate_content.py) on every push and pull request. It validates each object in `content/skills/*.json` under the top-level `skills` array against [`content/schemas/skill-def.schema.json`](../../../content/schemas/skill-def.schema.json) (Python **jsonschema**, Draft 2020-12), rejects **duplicate `id`** across files, and (Slice 1 / [NEO-33](https://linear.app/neon-sprawl/issue/NEO-33)) enforces the **frozen prototype trio** ids plus **gather + tech + (process or make)** category coverage.

**Mastery catalogs ([NEO-45](https://linear.app/neon-sprawl/issue/NEO-45)):** the same script validates `content/mastery/*_mastery.json` against [`content/schemas/mastery-catalog.schema.json`](../../../content/schemas/mastery-catalog.schema.json), cross-checks track `skillId` against loaded `SkillDef` ids, rejects **duplicate `perkId`**, enforces **branch integrity** (tier branch sets match; `perkIds` reference defined perks), and (Slice 4) requires **exactly one** track for **`salvage`** only.

**Item catalogs ([NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)):** the same script validates each row in `content/items/*_items.json` against [`content/schemas/item-def.schema.json`](../../../content/schemas/item-def.schema.json), rejects **duplicate `id`** across files, and (Slice 1) enforces the **frozen six-item** id set plus **exactly one row per `prototypeRole`** (`material` through `equip_stub`).

**Resource node catalogs ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)):** the same script validates `content/resource-nodes/*_resource_nodes.json` rows against [`content/schemas/resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json) and `*_resource_yields.json` rows against [`content/schemas/resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); rejects **duplicate `nodeDefId`**; (Slice 2) enforces the **frozen four-node** id set, **one row per `gatherLens`**, exactly **one yield row per node**, yield **`itemId`** cross-check against item catalogs, and **only `scrap_metal_bulk`** as yield item in prototype.

**Recipe catalogs ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)):** the same script validates each row in `content/recipes/*_recipes.json` against [`content/schemas/recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) (I/O rows via [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json)), rejects **duplicate `id`** across files, cross-checks every input/output **`itemId`** against item catalogs and every **`requiredSkillId`** against skill catalogs, and (Slice 3) enforces the **frozen eight-recipe** id set, at least one **`process`** and one **`make`** row, and **`requiredSkillId: refine`** on every row.

**Ability catalogs ([NEO-76](https://linear.app/neon-sprawl/issue/NEO-76)):** the same script validates each row in `content/abilities/*_abilities.json` against [`content/schemas/ability-def.schema.json`](../../../content/schemas/ability-def.schema.json), rejects **duplicate `id`** across files, and (E5 Slice 1) enforces the **frozen four-ability** id set (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`). Extend the script when additional catalogs and schemas ship.

**NPC behavior catalogs ([NEO-87](https://linear.app/neon-sprawl/issue/NEO-87)):** the same script validates each row in `content/npc-behaviors/*_npc_behaviors.json` against [`content/schemas/npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json), rejects **duplicate `id`** across files, requires **`leashRadius` > `aggroRadius`** per row, and (E5 Slice 2) enforces the **frozen three-behavior** id set (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`).

**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

- Plan: [`internal-content-authoring.md`](../../plans/internal-content-authoring.md)
- [Module dependency register](module_dependency_register.md)
