Files
sow-tools/internal/topdata/CLASS_FEAT_GLOBAL_INJECTS_CONTRACT.md
T

3.3 KiB

Class Feat Global Injection Contract

Status Snapshot

Current state as of 2026-06-05:

  • Preferred class feat injection policy is authored in topdata/data/classes/feats/global.json.
  • global.json rows flow through the ordinary class feat expansion pipeline, including successor expansion, masterfeat expansion, class-skill filtering, reference resolution, deduplication, and final 2DA emission.
  • Legacy topdata.class_feat_injections YAML still loads only as a compatibility path when no applicable class feat global.json exists.
  • Toolkit hardcoded defaults are compatibility-only and must not be the normal source of project policy.

Objective

Native class feat generation must produce deterministic, legacy-equivalent effective output while keeping project policy in topdata authoring files.

Requirements

  • For each class skill, inject the configured skill-focus masterfeat rows from classes/feats/global.json using FeatIndex.filter = "classskills".
  • Inject feat:literate only when the class table does not already include feat:illiterate.
  • Inject the shared combat/menu feat rows declared in the manifest unless a class already authors the equivalent FeatIndex.
  • Explicit class feat rows take precedence over injected rows.
  • Injections must be deterministic for identical inputs.

Manifest Shape

{
  "position": "prepend",
  "injections": [
    {
      "row": {
        "FeatIndex": { "id": "feat:literate" },
        "GrantedOnLevel": 1,
        "List": 3,
        "OnMenu": 0
      },
      "unless_present": [
        { "field": "FeatIndex", "id": "feat:illiterate" }
      ]
    },
    {
      "row": {
        "FeatIndex": {
          "id": "masterfeats:skill_focus",
          "filter": "classskills"
        },
        "GrantedOnLevel": -1,
        "List": 1,
        "OnMenu": 0
      }
    }
  ]
}

position defaults to append. The module uses prepend for class feat injections to keep generated policy rows before per-class authored rows.

Global Injection Grammar

Each injection entry may use only row, require_present, any_present, and unless_present. when_present is invalid and rejected; it is not an alias.

{
  "row": {
    "FeatIndex": { "id": "feat:example" }
  },
  "require_present": [{ "field": "FeatIndex", "id": "feat:required" }],
  "any_present": [
    { "field": "FeatIndex", "id": "masterfeats:metamagic" },
    { "field": "FeatIndex", "id": "masterfeats:combat" }
  ],
  "unless_present": [{ "field": "FeatIndex", "id": "feat:example" }]
}
  • require_present means every listed reference must exist.
  • any_present means at least one listed reference must exist, and the array must not be empty.
  • unless_present means none of the listed references may exist.
  • Every present group on an entry must pass.
  • Conditions observe the current rows in injection order, so earlier injections can affect later conditions.

Acceptance Criteria

  • Class feat output contains the same effective shared feats as the prior YAML policy.
  • Literacy injection is skipped for illiterate classes.
  • Skill-focus masterfeat rows expand only for class skills.
  • YAML compatibility continues to work for consumers that have not migrated.
  • A project with classes/feats/global.json does not also receive YAML or hardcoded default class feat injections.