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>
126 lines
3.7 KiB
Markdown
126 lines
3.7 KiB
Markdown
# 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:<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
|