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>
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.2daSexcolumnrace— numeric or string value mapped to portraits.2daRacecolumninanimate_type— numeric or string value mapped to portraits.2daInanimateTypecolumnplot— numeric or string value mapped to portraits.2daPlotcolumnlow_gore— numeric or string value mapped to portraits.2daLowGorecolumn
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:
- If a portraits.2da row with matching
BaseResRefalready exists → merge into that row - 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:
- Collects all datasets
- Runs
mergePortraitMetadata()after dataset collection - Injects portrait rows into the portraits.2da dataset
- 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
resrefis required — missing resref produces a parse error- Unknown metadata fields in
meta.portraitproduce a parse error - Unknown top-level keys in
metaproduce a parse error (allowed keys:family,wiki,portrait) - Conflicting portrait field values from different contributors produce a build error
- 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:
- Portrait metadata is parsed from
meta.portraitwith case-insensitive field names - Rows with portrait metadata inject into portraits.2da by
resrefmatching - Auto-generated rows receive
portraits:auto:<resref>lock keys - First-contributor-wins precedence is enforced per column
- Conflicting values produce build errors with clear diagnostic messages
- Portrait metadata does not leak into emitted 2DA column output
- Lock data is persisted when new auto-generated rows are created