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

4.0 KiB

Generic Family Expansion Contract

Status Snapshot

Current state as of 2026-05-13:

  • Implemented and actively used by native feat generation.
  • family_expansion.go, native.go, and topdata_test.go cover the current underscore-family interpretation, generated family metadata, and reapplication behavior.
  • Remaining work is mostly consumer expansion: use the same primitive in more datasets only when the authored data actually benefits from it.

Topdata now treats underscore expansion as a structural rule:

  • parent_child means child is an expansion of parent
  • underscores are for family structure, not simulated spaces
  • existing canonical keys still win when lock or TLK state already established them

This is a global interpretation rule, not a wiki-only convention.

Identity

  • weaponspecialization_club
    • parent: weaponspecialization
    • child: club
  • gnome_rock
    • parent: gnome
    • child: rock
  • toughness_10
    • parent: toughness
    • child: 10

Standalone keys without an underscore are treated as a parent identity with no child.

Generated Family Files

Canonical generated families declare:

{
  "family": "weapon_focus",
  "family_key": "weaponfocus",
  "template": "masterfeats:weaponfocus",
  "name_prefix": "Weapon Focus",
  "label_prefix": "FEAT_WEAPON_FOCUS",
  "constant_prefix": "FEAT_WEAPON_FOCUS",
  "legacy_family_keys": ["epic_weapon_focus"],
  "default_fields": {
    "DESCRIPTION": {
      "tlk": {
        "key": "masterfeats:weaponfocus.description",
        "text": "..."
      }
    },
    "MINATTACKBONUS": "1",
    "TOOLSCATEGORIES": "1"
  },
  "apply_after_modules": true,
  "identity_source": "child_source_value",
  "auto_prereq_fields": {
    "PREREQFEAT1": "weaponfocus"
  },
  "child_source": {
    "dataset": "baseitems",
    "column": "WeaponFocusFeat"
  }
}

Rules:

  • family is an authored label and is not a hardcoded switch key
  • family_key is the structural parent identity used in generated child keys
  • template is optional for the primitive in general, but required by current masterfeat-backed feat families
  • name_prefix, label_prefix, and constant_prefix define how new child rows are named when they are not already authored
  • template_fields lists template-backed fields copied from the referenced template row when authored; it is optional if the family owns its shared values directly
  • default_fields applies authored shared/default values to every generated family row and can be used to move family-wide shared values out of the template row
  • apply_after_modules: true re-applies the generated family after normal module files, so authored family-shared values remain authoritative even for legacy explicitly authored child rows
  • child_ref_field binds the generated child row back to the source row key, e.g. REQSKILL
  • identity_source: "child_source_value" tells the builder to reuse IDs/keys from the source column when that column already carries feat identity
  • legacy_family_keys declares previous generated-family prefixes that should donate existing locked row IDs to the current family when a family is renamed
  • auto_prereq_fields binds prerequisite fields to other authored families by family_key, without code changes
  • allow_existing_only limits expansion to already-authored child rows when a family is intentionally partial
  • child_source.dataset identifies the canonical dataset driving expansion
  • child_source.column is used when expansion is gated by a non-null source field
  • child_source.predicate is used when expansion depends on a named rule such as accessibility

Row Metadata

Generated rows can carry:

{
  "meta": {
    "family": {
      "parent": "weaponfocus",
      "child": "club",
      "source": "baseitems:club",
      "template": "masterfeats:weaponfocus"
    }
  }
}

This metadata is builder-owned and does not affect emitted 2DA columns. It preserves family structure for later phases without enabling wiki generation yet.