# 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`: ```json { "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:` ### 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: ``` 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:` 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