Files
sow-tools/internal/topdata/PORTRAIT_METADATA_CONTRACT.md
T
archvillainette dc93feb176 Topdata Migration (#2)
Native topdata migration summary

- Replaced the legacy 2dabuilder runtime path with the native topdata builder.
- Native build, validate, compare, and convert flows now cover the canonical topdata pipeline.
- compare-topdata is now a native self-check; legacy reference-builder runtime usage was removed.
- Parts generation follows the native asset-scan contract and uses sow-assets via project asset resolution.
- Generated feat families, class-feat injects, masterfeat/successor expansion, and dataset-derived feat generation were brought to parity and regression-covered.
- Remaining mirrored datasets were migrated into native-owned canonical data while preserving lock IDs.
- normalize-topdata bridge behavior and migration-era ownership markers were removed.
- Documentation was rewritten around the native workflow and active contracts were cleaned up.
- Final hardening pass removed stale validator assumptions and cleaned ignored/generated artifacts for PR readiness.

Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/2
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-04-09 07:14:01 +00:00

3.7 KiB

Portrait Metadata Contract

Objective

Define the canonical behavior for portrait metadata embedded in topdata rows, enabling cross-dataset portrait injection into the portraits.2da table.


Portrait Metadata Shape

Portrait metadata is embedded in row meta.portrait:

{
  "meta": {
    "portrait": {
      "resref": "nw2book1_",
      "sex": 4,
      "race": 10,
      "inanimate_type": 4,
      "plot": 0,
      "low_gore": "****"
    }
  }
}

Required Fields

  • resref — non-empty string identifying the portrait base resource reference

Optional Fields

  • sex — numeric or string value mapped to portraits.2da Sex column
  • race — numeric or string value mapped to portraits.2da Race column
  • inanimate_type — numeric or string value mapped to portraits.2da InanimateType column
  • plot — numeric or string value mapped to portraits.2da Plot column
  • low_gore — numeric or string value mapped to portraits.2da LowGore column

Field names are case-insensitive with hyphen/underscore normalization:

  • inanimate_type = inanimate-type = InanimateType

Merge Behavior

Identity Matching

Portrait entries are matched by resref. When any row in any dataset carries meta.portrait.resref:

  1. If a portraits.2da row with matching BaseResRef already exists → merge into that row
  2. If no matching row exists → create a new row with auto-generated identity portraits:auto:<resref>

Field Mapping

Portrait metadata fields map to portraits.2da columns:

Metadata Key 2DA Column
resref BaseResRef
sex Sex
race Race
inanimate_type InanimateType
plot Plot
low_gore LowGore

Override Precedence

  • First contributor wins for each column
  • If a column already has a non-null value, subsequent contributions are rejected if they differ
  • Identical values (after normalization) are silently accepted
  • Conflicting values produce a build error

Null-Like Values

Missing fields and nullValue ("****") are treated as unset. They do not override existing values and are not considered conflicts.


Lock Data

Auto-generated portrait rows are tracked in lock data with key pattern:

portraits:auto:<resref>

For example: portraits:auto:nw2book1_


Cross-Dataset Injection

Any dataset may contribute portrait metadata. The build pipeline:

  1. Collects all datasets
  2. Runs mergePortraitMetadata() after dataset collection
  3. Injects portrait rows into the portraits.2da dataset
  4. Updates lock data if new auto-generated rows were created

This enables non-portrait datasets (e.g., placeables, genericdoors) to define portrait associations without modifying the portraits dataset directly.


Validation Rules

  1. resref is required — missing resref produces a parse error
  2. Unknown metadata fields in meta.portrait produce a parse error
  3. Unknown top-level keys in meta produce a parse error (allowed keys: family, wiki, portrait)
  4. Conflicting portrait field values from different contributors produce a build error
  5. Portrait metadata is excluded from emitted 2DA output — it exists only at authoring/build time

Acceptance Criteria

Implementation is correct only if all of the following are true:

  1. Portrait metadata is parsed from meta.portrait with case-insensitive field names
  2. Rows with portrait metadata inject into portraits.2da by resref matching
  3. Auto-generated rows receive portraits:auto:<resref> lock keys
  4. First-contributor-wins precedence is enforced per column
  5. Conflicting values produce build errors with clear diagnostic messages
  6. Portrait metadata does not leak into emitted 2DA column output
  7. Lock data is persisted when new auto-generated rows are created