ASSET_DROPIN_CONTRACT.md

assets/ASSET_DROPIN_CONTRACT.md

DR-2 — The Asset-Path Drop-In Contract

CANON SUBORDINATION — this document is PROPOSAL-TIER: it serves canon and never outranks it.
The canon: the CVD · the T1 foundation docs · the T0 registries (registries/) · the spine
(docs/spine/CH_*.md) · the region pages (_source/02_Tier_2_Region_Pages/). Authority order: docs/DOC_MAP.md § 0.
Canon served (scanned from this document's own citations — widen it by hand where it is thin):
T0_Character_Index · T0_Realm_Ruleset_Registry · T0_SFX_Registry · T0_Scene_Spec_Registry · (+4 more t0)
READ THAT CANON FIRST — open it and derive from it before you build anything from this document.
If this document disagrees with canon, CANON WINS and this document is the defect — fix the
document, never the canon. Nothing here is applied until it is ratified into canon.

Status: PROPOSAL-TIER (LANE-CANON). Closes DR-2, "the one absent piece" of the 5090 handshake

(docs/PRE_5090_BUILD_PLAN_VOL2.md §B, row DR-2; audited by the W11 handshake-close wave). Provenance:

the scattered instances already exist (T0.3 ue_sound_asset_path, the cinematic ue_asset_path, the AG7.5

mesh state machine, the T0.13 cross-repo writeback manifest); a grep docs/ for a UNIFIED contract returned

zero. This document is that single contract. Every current-state claim below carries a file:line or a

registry-column citation.

This is ENGINE infrastructure that consumes CANON as data (see Section 5). It invents no world canon; it

declares how a finished asset attaches to a canon id that already exists.

1. The invariant

An asset is addressed by its CANON ID, never by a content path.

voice_id, scene_id/scene_anchor_id, a GameplayCue tag, a UMG widget binding. The id is a queryable

UPROPERTY on the staged actor (DR-1 contract) and the RowName of its registry row.

resolver knows the path. The resolver always degrades to a declared placeholder, never to a hard null.

imported to the same path the placeholder occupied (path takeover), or the resolved path is re-pointed in

the id's registry column (manifest re-point). Either way: zero code edits, zero Blueprint edits, zero

re-placement of the staged actor. That is the drop-in guarantee, and it is the whole reason the post-5090

generative passes are pure fill and never rework. Ship-hardening note (critic): a Blueprint

hard-asset reference or a cooked build can bypass a string-path resolver — manifest-re-point

asset directories must be declared always-cook, and the W11 audit gains a BP-hard-ref tooth

(zero Blueprint hard references to generable asset classes) before ship (docs/PRE_5090_BUILD_PLAN_VOL2.md §B exit condition;

docs/ROADMAP_POST_5090_TO_SHIP.md P2.0).

6-D adds the third; rides the commit that adds the columns, and is unrecoverable after the freeze).

presentation_tier_ref is the MASTERY-VARIANT axis: which authored variant of an ability/weapon

presentation plays. generation_tier is the FIDELITY/BUDGET class: which asset-quality ceiling a

surface is generated against (open item 6). decay_stage is the CONDITION axis: which authored

condition of a PLACE renders — not a quality ceiling, not a mastery variant (item 6-D). A bare

tier column or parameter is banned on this contract's schema surface — five live senses already

collide (PresentationTier in code = mastery; the realm charter's tier = fidelity; QA

tier_applicability = footage maturity; the twelve mastery levels = progression;

readability_tier = inscription mastery).

Current violations — the honest migration list. The DataTable/Map id-rail is already clean (34 of the 56

/Game/ string literals in Source/Humanity point at /Game/Data/Registries/DT_* or /Game/Maps/* — that

is the resolver's own input, correctly id-keyed). The ASSET side is not yet under any resolver:

/Engine/BasicShapes/Cube|Cylinder and /Game/Characters/Mannequins/Meshes/SKM_Manny_Simple directly in

actor constructors: HumanityBossCharacter.cpp:92,112,125, HumanityCharacter.cpp:118,130,

HumanityEnemyCharacter.cpp:77,85, HumanitySiteMarker.cpp:38, HumanityGatherNode.cpp:42,

HumanityVrilRechargeZone.cpp:41, plus the Manny skeletal-mesh and anim content paths at

HumanityBossCharacter.cpp:126,128,133. No actor reads mesh_id_ref to choose a mesh (grep of

Source/Humanity/Private for mesh_id_ref as a mesh selector = 0). The mesh class has no id→path resolver

at all.

has a real ResolveSound(FName) with placeholder fallback (HumanityAudioSubsystem.cpp:55-75), but the

cue→path table is six hardcoded /Game/Audio/SFX/SW_* literals (HumanityAudioSubsystem.cpp:32-38) plus

the optional attenuation (:42). It never reads DT_Sfx.ue_sound_asset_path. The column exists; the

resolver ignores it.

So the migration is not "invent a resolver from nothing" — it is "generalize the one audio resolver to read

the id column, and give the mesh classes the same resolver." Roughly 15 mesh-side and 6 audio-side hardcoded (critic-refreshed live count ≈ 21)

asset references move under the contract (Section 6).

2. Per-asset-class contract table

Each row: the canon id source, where the placeholder lives today, where the final asset drops, how the swap

happens, and the QA tooth that proves it.

Asset classCanon id source (registry.column)Placeholder home todayFinal-asset drop pathSwap mechanismVerification tooth
Named-NPC skeletal (MetaHuman)T0_Character_Index.character_id / mesh_id_ref (+ cinematic ue_asset_path)SKM_Manny_Simple via ConstructorHelpers (HumanityCharacter.cpp:130)/Game/Characters/NPC/<char_id> (MetaHuman in-engine)Manifest re-point (ue_asset_path; MetaHuman lands off-convention)DataValidation id resolves; capture read by image critic (P3)
Boss / creature / familiar / antagonist meshmesh_id_ref on Boss/Creature/Familiar/Antagonist assetgen block/Engine/BasicShapes/Cube (HumanityBossCharacter.cpp:92)/Game/Meshes/<class>/SM_<mesh_id_ref> (AG7.5 import)Path takeover (import to convention path)mesh_id_ref resolves (QT-8); material-identity + capture diff (P3)
Weapon meshT0_Weapon_Registry.mesh_id_ref (WeaponRegistryRow.h:27)none built (weapons unrendered)/Game/Meshes/Weapon/SM_<mesh_id_ref>Path takeoverDataValidation resolves; capture diff
Vril-site / prop / environment meshT0_Vril_Site_Registry.mesh_id_refCylinder via ConstructorHelpers (HumanityVrilRechargeZone.cpp:41)/Game/Meshes/Env/SM_<mesh_id_ref>Path takeoverimported-mesh visual QA (P3); resolver non-placeholder
Material / substratesubstrate_material_ref_path (assetgen block)M_SiteMarker / biome materials (Content/Terrain/)/Game/Materials/<class>/M_<id>Path takeovermaterial-identity check; capture diff
SFXT0_SFX_Registry.sfx_idue_sound_asset_path/Game/Audio/SFX/SW_*_placeholder + _sonniss (HumanityAudioSubsystem.cpp:32-38)/Game/Audio/SFX/SFX_<sfx_id>Path takeover (idempotent import)cue-fired telemetry non-zero (QT-10); muted/wired fixture (QT-AU)
Voice line (VO)T0_Voice_Registry.voice_idue_sound_asset_pathnone built (CN-4 slot)/Game/Audio/Voice/<cat>/V_<voice_id>Path takeovercue-fired telemetry; §17.12 gender-lock check (QT-9)
Music / themeT0_Theme_Registry.<theme_id>ue_sound_asset_pathAU-5 placeholder stems/Game/Audio/Cues/<cat>/A_<theme_id>Path takeoveradaptive graph plays real stems; benchmark gate
VFX (Niagara)GameplayCue tag + the closed cue-parameter set (open item 7: element, presentation_tier_ref, status class, counter class, phase)placeholder cue notifies (C1.16)Niagara system bound to the same cue tag, branching per-tier on the parameter set — one system per family, never one per variantPath takeover at the cue bindingevery cue fires its effect (QT-6); shape channel independent; per-tier variant identifiable (doctrine §5.4 D10)
Cinematic sceneT0_Scene_Spec_Registry.scene_id/scene_anchor_idscene_spec_refscene-anchor runtime placeholder (DT_SceneAnchor)/Game/Cinematics/Scenes/<scene_id> (Level Sequence)Manifest re-point (scene_spec_ref)framed scene plays for FORK_CH02_A; critics read shots
HUD art skinUMG widget binding (authored, not generated)bound UMG overlay, placeholder art (P2.4)skinned art in the same widgetPath takeover at the widgetlegibility audit (QA-12); before/after captures

Eleven asset classes — note HUD art WIDENS DR-2's enumerated Vol2 surface list (a disclosed charter

extension, open item 5; ratify or strike at the P0 handshake). Nine ride the five-state generation

machine (Section 3); VFX and HUD art are

hand-authored (Niagara, UMG) and ride only the id + placeholder + swap discipline, with generation_status

marked N/A (open item 5).

3. The five-state lifecycle

The state machine is the ratified AG7.5 enum, stored in the assetgen columns already declared in

docs/registry_extensions.json (generation_status, generation_prompt_hash, last_generated_timestamp,

regeneration_trigger) — the five-state asset enum ratified at AG7.5 (docs/PRE_5090_BUILD_PLAN.md:800) and Vol2 DR-2

(docs/PRE_5090_BUILD_PLAN_VOL2.md:314). (Critic-corrected: the spine/book pipelines run a DIFFERENT

seven/eight-state spine_status machine — no shared enum is claimed.) The human-readable rungs

(placeholder → prompted → generated → reviewed → live) map onto the ratified enum values; the review and

live rungs are process gates and observable outcomes, not extra stored states.

Human runggeneration_status valueWho writes the transitionRecorded in
placeholderpendingthe staging tool (DR-1..DR-6 placement)generation_status=pending; placeholder sits at the convention path
promptedin_progressthe 5090 pipeline (prompt composer)generation_status=in_progress; generation_prompt_hash set
generatedcompletethe 5090 pipeline (import + apply_*_writeback)generation_status=complete; ue_asset_path + last_generated_timestamp written
reviewedcomplete + human sign-offthe QA loop (QA-13) / director / Josh backstopQA bug-ledger + director_in_loop_required; not a new enum value
livecomplete + resolver returns non-placeholderthe resolver (runtime, automatic)observable: the resolver no longer falls to the placeholder
(rejected)rejectedthe QA loop / directorgeneration_status=rejected; the id stays on its placeholder
(re-gen)regeneration_requireda regeneration_trigger (prompt-hash change, canon edit, source correction)generation_status=regeneration_required; loops back to in_progress

Two disciplines this encodes. First, "live" is never a stored claim — it is the resolver actually returning

the real asset, so a stale complete on a missing file is caught the moment the resolver degrades to the

placeholder. Second, the cross-repo writeback obeys the one-writer-per-repo rule: the game repo is read-only

against canon, so the pipeline writes results back through the T0.13 manifest and the canon-side applier,

which refreshes docs/fidelity_baseline.json in the same commit (docs/BUILD_PLAN_END_TO_END.md:139).

4. The DR-1 audit hook (W11)

W11 (HANDSHAKE close) proves the contract holds across every staged actor and cue

(docs/PRE_5090_BUILD_PLAN_VOL2.md MASTER WAVE QUEUE, W11 row). Per staged actor / cue it asserts:

the recorded re-import-renames-a-row / chunk_id-collapse orphan class is exactly what this catches). The

proven precedent is AHumanityVrilRechargeZone::FindForVrilSite keying on VrilSiteId

(HumanityVrilRechargeZone.h:99).

never a crash-null.

non-empty and loads; if pending, the placeholder loads.

(pendingin_progresscomplete → live via the resolver) with zero code or Blueprint diff. That

single round-trip is the handshake exit condition for DR-2.

5. Engine-vs-canon split

Per the standing engine-reuse directive (game #2 inherits the machinery, not the world):

conventions, the five-state machine mechanics, the cross-repo writeback manifest + applier, the DR-1

stable-id audit and DataValidation, and the swap-verification teeth (QT-8 data integrity, QT-10 audio

event-fire, QT-AU muted/wired fixture). None of it encodes a single Humanity fact.

which sfx, which voice, which scene; the actual ids, prompts, and prompt hashes; the §17.1 reference-built

vs generatable constraints; the reveal-window gates. Game #2 supplies its own registries and inherits the

resolver, the conventions, and the audit unchanged.

6. Migration plan

Ordered small batches, each with its own test, each gated by run_gates.py green (unpiped) before it lands.

mesh-bearing registries in docs/registry_extensions.json and docs/fidelity_baseline.json

(added_columns + extension_hashes). Zero code; the mechanism is proven across ten registries already.

Test: harness/check_registry_extensions.py green + the fidelity gate green in the same commit.

DONE 2026-08-02 — Batch 0 is CLOSED, gates 36/36 green, exit 0. Three legs landed together

because they are one commit by ruling: the schema (17 columns across 12 registries, including the

C-12 generation_licence_ref on every enrolled registry, the item-6/6-D reservation columns having

already landed at 28n), the STATE (843 generation_status cells written through the sanctioned

writer harness/apply_generation_lifecycle_backfill.py — 826 empty cells took the enum's own

initial state pending per §3 row 1, since a placeholder at the convention path IS pending), and

the TOOTH (GATE 36 harness/check_generation_lifecycle.py). One correction the migration plan did

not anticipate and that every later Batch-0-shaped pass should inherit: **a schema add that lands a

status column EMPTY re-creates the exact defect Batch 0 exists to close.** T0_Scene_Spec_Registry

therefore received generation_status populated in the same pass, not empty. regeneration_trigger,

last_generated_timestamp and generation_licence_ref do land empty — those are honestly empty

until something generates; a status column is not.

unit-tested headless. Test: a known id resolves to its placeholder; a re-pointed id resolves to the final

path; a missing id returns the declared placeholder, never null.

DT_Sfx read keyed on sfx_idue_sound_asset_path, keeping the placeholder fallback path already in

ResolveSound. Test: QT-10 cue-fired telemetry stays non-zero; the QT-AU muted-build must-not-fire /

wired-build must-fire fixture still passes; the six hardcoded audio literals are gone (grep = 0 outside

the resolver).

mesh_id_ref through the resolver (convention path), with the BasicShape / Manny bind demoted to the

declared placeholder fallback. Test: DataValidation shows mesh_id_ref resolves for every staged actor; a

capture shows the placeholder still renders when no final asset exists; the ten ConstructorHelpers binds now

route through the resolver.

via DT_SceneAnchor, so this batch is mostly confirming the manifest-re-point path. Test: a framed scene

plays for FORK_CH02_A and image critics read the shots.

before/after captures; the three open HUD polish records (BUG-0006..8) closed or triaged.

Honest sizing: Batches 0-2 are the near-term spine (schema + resolver + the audio proof that round-trips the

state machine — this is the DR-2 handshake exit). Batch 3 is the largest (every mesh-bearing actor). Batches

4-5 are confirmation and polish. None requires the 5090; all are pre-hardware pre-wire.

7. Ratified items and binding pre-freeze clauses — ✅ ITEMS 1-5 RATIFIED EARLY (Josh, seventeenth sitting 2026-07-28: "ratify all five as written"; the P0-handshake ratification is DONE ahead of schedule — each item's recommendation below is now the RULE, including item 5's HUD-art surface widening) · items 6-7 RULED at the fourteenth sitting

1. Mesh resolved-path column. The assetgen block carries mesh_id_ref but no resolved-path column; audio has

ue_sound_asset_path and named-NPC has the cinematic ue_asset_path, but boss/creature/familiar/vril/

weapon meshes have only the id. Recommendation: ratify the id→convention path as the default

(/Game/Meshes/<class>/SM_<mesh_id_ref>, resolver-derived, zero column) AND add an optional

ue_asset_path override column for off-convention/licensed/MetaHuman assets. The audio lane already proves

the override-column pattern; one gate-safe column via Batch 0.

2. Reviewed vs live as enum values. The task's five rungs include a distinct "reviewed" and "live"; the

ratified enum has neither. Recommendation: do NOT expand the enum. Record the aesthetic-taste review as a

process gate (QA-13 ledger + director_in_loop_required); treat "live" as the observable resolver outcome.

The five stored states stay pending/in_progress/complete/rejected/regeneration_required.

3. Default swap mechanism. Two mechanisms are valid (path takeover, manifest re-point). Recommendation: make

path takeover the default — the audio import is already idempotent to a fixed path

(Tools/import_audio_assets.py, replace_existing=True) — and reserve manifest re-point for licensed and

MetaHuman assets that must live off-convention. A one-line ratification stops pipelines improvising.

4. The §17.1 reference-built guard. Real living-heritage architecture and forms are NEVER raw-generated

(docs/ROADMAP_POST_5090_TO_SHIP.md P2.2). Recommendation: the resolver's in_progresscomplete

transition for a care-gated id is blocked until the capture-rights / authenticity audit signs off — a

mechanical gate keyed on the care-tier column, sitting beside the existing §17.1 collective-care validator

(QT-9). Director confirms the gate wiring.

5. VFX and HUD status semantics. These two classes are hand-authored (Niagara, UMG), not generated, yet they

still ride the id + placeholder + swap invariant. Recommendation: state that the SWAP guarantee applies to

them (placeholder Niagara → real Niagara at the same cue tag; placeholder UMG art → skinned art in the same

widget) while generation_status is marked N/A — the token is now named: not_applicable (Batch 0,

2026-08-02). It is a SENTINEL, not a sixth state: item 2's five stored STATES are untouched, and the live

T0_Voice_Registry already used the same token in two columns before this contract named it. It is not

free to write. GATE 36 (harness/check_generation_lifecycle.py) tooth L5 permits it only where the

registry declares a machine-checkable predicate over live columns (the thirteen cultural_register voice

rows, via voice_clone_status) or a declared lifecycle redirection whose OWNER ROW must exist and be

enum-valid (tooth L6) — and it enforces the converse, so a class cannot be half marked. The column's

vocabulary is therefore closed at five states plus one justified sentinel, with no allowlist and no

per-row exemption — so the W11 audit does not demand a generation state on a

surface that has none.

6. THE REALM-TIER RESERVATION — BINDING, not an open question (the ruled realm-charter amendment; mechanism

verified by the P1-C presentation lens + its fresh-context refutation critic; register v1.4 entry 95).

Recorded here so the freeze carries it. Seven clauses, each riding machinery this contract already

declares:

FREEZE: background · region_standard · region_hero · realm · apex. Adding the value later

re-baselines docs/fidelity_baseline.json across every block-bearing registry and re-opens the

frozen contract — so the value exists years before the first realm asset is generated. Lands via

Batch 0 (§6). Honest label: the enum-must-exist-at-freeze argument is sound INFERENCE from the

standing baseline-refresh rule, not a citation — never re-quote it as a cited fact.

Convention paths stay tier-free, so a region→realm fidelity upgrade is a re-generation at the same

id: zero code, Blueprint or actor changes, exactly §1's invariant.

regeneration_required through the existing regeneration_trigger (§3). Self-enforcing: a realm

asset accidentally generated at region tier is caught by the state machine, not by a human noticing.

all TEN realm rows — the nine mythic_realm rows PLUS fairy_realm, which is

node_kind='story_anchor' and silently dropped by the obvious filter, while the safer-looking

page_status='dimensional' over-selects eleven by pulling in the epilogue. NEITHER existing column

is a clean realm selector (register v1.4 entry 105) — which is why the write is explicit. Ten rows,

one script, through the sanctioned writer, fidelity baseline re-emitted in the same commit.

register v1.4 entry 96).

§5.3/§5.4/§5.6 carry the realm class — realm surfaces inherit the region budgets as their FLOOR

until benchmark day mints realm numbers (doctrine §8.2 U-44). Landed with this clause commit.

band at freeze, so ceiling surfaces are never graded against a region floor and passed. Landed with

this clause commit.

audio and art references obey the corpus's own ruled wording — NO SAMPLING of consecrated

performance; reference-COMPOSITION is permitted. A blanket reference-first rule is care inflation

and is refused.

corruption-as-condition slice's refutation-survived F-9). Decay is BEAUTY DEGRADED ALONG A NAMED

AXIS: the hue the palette drains toward, the form-transform the growth-forms undergo, the

acoustic thinning — authored ONCE per realm as a RULE applied everywhere, never a mood note, so

a single frame says "this was lovely" and "this is wrong" simultaneously. Five clauses riding

the same machinery as 6.1-6.5:

decay_bearing flag on the assetgen block, landing through Batch 0 (§6) in the SAME commit

as generation_tier. A decay-bearing surface carries decay_stage as a REQUEST parameter

and a registry column — never a path segment. UNLIKE the tier axes, decay stages are

CONCURRENT, not successive: the fairy realm's height is playable at Prologue/Ch 1 while its

fading present is playable at Ch 75-77, so a stage change is NOT a re-generation at the same

bare id (that 6.2 framing governs TIER upgrades only). decay_stage is a VARIANT-KEY axis:

the resolver key is (canon id, decay_stage), and fallback walks to the realm's AUTHORED

PRESENT, never to a hard null — the item-7 shape, reused. The fairy realm's surfaces are

decay-bearing by canon: its fading state IS story canon (the Prologue-height court, the

fading present, the way home).

art-hook family the realm charter §2.3 DECLARES (palette_law · silhouette_law ·

impossible_architecture_budget · signature_phenomenon), whose registry home is the

Wave-2 T0_Realm_Ruleset_Registry mint (the registry does not exist yet; design intent:

same micro-grammar as palette_law, one parser for the family) — plus the requested

decay_stage. generation_prompt_hash includes the stage (the 6.3 self-enforcement: a

stage-key's prompt change sets regeneration_required for THAT variant through the existing

trigger).

pins: the realm AT ITS HEIGHT (the before-state — for the fairy realm, the Prologue-height

palette) and the realm's authored PRESENT. Any intermediate stage a later wave wants is a

DECLARED post-freeze re-baseline priced in its own commit (the 6.1 cost, accepted

deliberately for this axis because two endpoints are canon-sufficient at freeze — this keeps

the DR-2 freeze independent of the Wave-2 mint's schedule).

renewal share one visual vocabulary — so the ruled axis-7 world-cleansing meter moves the

world BACK along the same authored axis (the meter's worldstate key is NOT YET MINTED;

WS_039 is the distinct hollow-containment fork enum, named here so no one freezes the

wrong key). One axis, two directions, never two art sets: the economy argument in contract

form.

endpoint stages); any intermediate stage carries its sizing delta in the commit that mints

it. A realm tier sized without its decay axis buys one palette per realm and then needs two

(the F-9 tooth). Item 6 and this law freeze TOGETHER, exactly as v1.3 entry 92 recorded.

panel generates from the Prologue-height palette (art wave, F-9 rider); the RB-RLM-PROVENANCE

sibling rubric gains the before-state line — shown a fairy-realm frame, the critic states

what the place looked like BEFORE, and if it cannot, decay_law has FAILED on the art side

even where the data passes (zero new gate — rides the existing RB-RLM-CARE rubric family).

is not free: decay-bearing at least DOUBLES the generated-asset count for every surface it

covers. The per-realm decay_law VALUES are art-wave authoring under the realm program's

choice-board protocol.

7. THE PRESENTATION-VARIANT RAIL — FULL PER-TIER, RULED (Josh, 2026-07-28 fourteenth sitting: OPTION (A),

the band-collapse recommendation OVERRULED; register v1.4 entries 94/96/97/100):

docs/registry_extensions.json + docs/fidelity_baseline.json in the same commit, the mechanism §6

already calls proven across ten registries.

NEAREST AUTHORED tier, never to a hard null — the per-tier extension of §1's placeholder discipline.

The live variant key must be AUTHORED, never synthesized: the foundry currently prints the key from

an id and a clamped tier (its own comment says so), and a derived key is invisible to the audit

tooth, which covers ability_id but not the derived variant key.

8 live tier values), 372 at the 12-tier clamp. Variant volume is a first-class 5090 workload;

parametric interpolation is permitted only INSIDE an authored tier's polish pass, never instead of

one. The every-ability-beautiful requirement stands at full strength.

surface carries exactly (element, presentation_tier_ref, status class, counter class, phase) —

declared closed HERE so one Niagara system per family branches on parameters instead of forty

systems existing. §2's VFX row carries the same set.

rail lands at Batch 0 (today tooth: NONE(...) records the hole verbatim).

Generated by harness/site/structure_site.py — the URL path is the repo path. review root