feat(skills): ship ue-design-skills bundle, licensing and delivery gate

Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ue-toolchain
2026-09-05 23:48:55 +07:00
parent 9e2194c298
commit dab3f35079
67 changed files with 21630 additions and 0 deletions
@@ -0,0 +1,413 @@
---
name: ue-data-driven-architecture
description: >-
Design or review data-driven architecture in Unreal Engine: definition layers,
data asset versus zero-node Blueprint class defaults versus instanced
fragments and actions, primary asset identifiers, soft and hard and class
references, asset bundles, modular composition, validation, and the boundary
between code and data. Use when adding game modes, pawn archetypes, ability
packages, item and equipment definitions, playlists or feature plugins, or
when a manager is growing per-variant branching.
---
# UE data-driven architecture
Data-driven does **not** mean "put fields in a data asset". It means:
> Code owns lifecycle, algorithms, replication and invariants. Data selects and
> composes implementations at explicit architectural layers.
Measured patterns from a reference product: [patterns](references/patterns.md).
Detection recipes for silent data-graph failures:
[failure modes](references/failure-modes.md).
**The characteristic failure of a data graph is that it compiles.** A wrong
reference, an unregistered identifier, a missing bundle and a copy-pasted
prototype are all valid data. Validation is the only compiler this layer will
ever have, and building it is part of the architecture rather than a follow-up
task.
Related skills: `ue-modular-gameplay`, `ue-gas-architecture`,
`ue-input-architecture`, `ue-ui-architecture`, `ue-cosmetics-and-teams`.
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
---
## 1. Start with layers, not asset classes
Before creating an asset, name the level of composition it owns. A robust
product usually needs several narrow definitions, not one omniscient
configuration:
```text
catalog / playlist
├─ where and how to launch (map, session, display metadata)
└─ gameplay definition
├─ feature plugins to enable
├─ reusable action sets
├─ mode-specific delta
└─ pawn archetype
├─ pawn class
├─ capability packages
├─ input semantics
├─ policy matrix
└─ camera
```
Items form a separate pipeline:
```text
pickup presentation
└─ item definition
├─ inline fragments (display, stats, equippable, reticle)
└─ equipment definition
├─ runtime instance class
├─ capability packages
└─ actors and components to spawn
```
### The layer test
For every field ask:
1. Does it describe **launch and catalog UI**, or **runtime gameplay**?
2. Is it common to several modes, or a mode-specific delta?
3. Does it define an archetype, a capability, or presentation?
4. Who consumes it, and at which lifecycle phase?
If one asset answers all four, split it.
---
## 2. Pick the container by lifecycle
The storage type is an architectural decision, not a preference.
| Need | Use | Why |
|---|---|---|
| Standalone immutable record referenced as an object | data asset | object identity, editable in Details |
| Asset-manager discovery, bundles, async loading | primary data asset | stable identifier, bundle rules |
| A class default, a class reference, inheritance, or a runtime instance of that class | zero-node Blueprint subclass | class identity plus editable defaults |
| Polymorphic optional facet embedded in a definition | instanced object, edit-inline | composition without asset explosion |
| Polymorphic load/activate/deactivate operation | instanced action object | behaviour in code, targets in data |
| Dense homogeneous tabular records | data table | rows, import and export, bulk editing |
### A Blueprint with zero nodes is a data container
This is the most consequential and least visible fact in this skill. An entire
definition layer can live in Blueprint classes that contain no graph at all,
with values on their class defaults.
**An inventory that searches only for data-asset instances will not see any of
it.** Count both, or your census of the data layer is wrong by however much that
layer holds. Recipe: DD-01.
### When not to use a Blueprint class default
Do not use one merely because the editor offers it. Prefer a data asset when
consumers need an object rather than a class, when no runtime instance of the
subclass is created, when inherited defaults add nothing, and when asset-manager
identity matters more than a class reference.
---
## 3. Pick reference semantics explicitly
Every reference chooses loading and ownership behaviour.
| Reference | Use when | Failure mode |
|---|---|---|
| primary asset identifier | the target must be addressed before it is loaded | type or name not registered; resolves to nothing |
| soft object or class pointer | async load, optional content, role-specific content | not loaded when dereferenced; omitted from the build |
| hard pointer | the target is required whenever the owner is loaded | dependency graph grows transitively |
| class reference | data selects an implementation class or its defaults | construction and lifecycle become class-coupled |
| instanced object | the owner exclusively owns a small polymorphic facet | duplication; cannot be shared independently |
Write the decision into the property metadata and the validation, not only into
a design document.
### Asset bundles belong beside the soft reference
```cpp
UPROPERTY(EditAnywhere, meta=(AssetBundles="Client"))
TSoftClassPtr<UUserWidget> WidgetClass;
UPROPERTY(EditAnywhere, meta=(AssetBundles="Client,Server"))
TSoftClassPtr<UGameplayAbility> AbilityType;
```
The definition then declares both **composition** and the **load and cook
boundary** in one place. Do not maintain a separate, undocumented table of which
assets belong to which role.
Two failure modes follow directly, and both are silent:
- a soft reference with **no** bundle annotation is not pulled into the build by
its owner, so it resolves to null in a packaged game and works in the editor;
- a bare accessor on a soft reference assumes the bundle worked, and returns null
without complaint when it did not.
Recipes: DD-03, DD-04.
### Cross-plugin dependency gate
For every reference crossing a feature-plugin boundary: is the target plugin
active before this asset resolves? Does the owning bundle pull it into the
build? Can the source deactivate while the target is still referenced? Is the
dependency one-way, or did you just create a cycle?
---
## 4. Code consumer, data selection
A healthy definition has a narrow code consumer. The consumer owns when loading
happens, authority and client rules, replication, initialization order, apply and
rollback, validation and error handling.
Data owns which class or asset or tag is selected, numeric and presentation
parameters, variant composition, and role flags where the consumer supports them.
### The switch test
If adding a new mode, weapon or pawn requires another branch in a manager, a
definition, action or fragment boundary is probably missing.
### But do not move an invariant into data
These stay in code, always:
- "only the authority grants capabilities";
- "the archetype must exist before this readiness state";
- replication rules;
- teardown order;
- "one grant revokes only its own handles".
**The variant is data; the safety rule is code.** A rule that can be edited by
opening an asset is a rule that will be edited by someone who does not know it is
a rule.
---
## 5. Compose reuse; do not hide it in inheritance
Prefer flat composition:
```text
experience = shared input + standard components + standard HUD + mode delta
```
over an inheritance chain:
```text
base → shooter → team → deathmatch
```
Composition makes dependencies visible, reusable and diffable. Inheritance
accumulates defaults silently and creates ordering questions nobody asked.
If your definition type supports Blueprint subclassing, consider **rejecting it
in validation** and pointing the author at composition instead. A reference
product does exactly this, with an error message naming the alternative
— a validator that teaches rather than only refuses. Recipe: DD-05.
### The symmetry gate for actions
Before approving any action that adds something at runtime, fill this table:
| Resource added | Handle retained | Exact rollback |
|---|---|---|
| extension handler | request handle | release handle |
| component | component request or instance | remove only the owned component |
| capability grant | granted handles | revoke only those handles |
| input binding | bind handles | remove each binding |
| mapping context | player plus context | remove from the same player |
| widget or layout | extension or layout handle | unregister or remove |
**A blank middle column means the action cannot be rolled back.** Do not
advertise runtime-deactivatable modularity until the table is complete and
tested. See `ue-modular-gameplay` for the failure modes this prevents.
---
## 6. Use fragments for optional facets
Avoid one definition carrying every possible field for every possible item.
Use polymorphic inline fragments and let consumers query by class:
```cpp
UCLASS(DefaultToInstanced, EditInlineNew, Abstract)
class UItemFragment : public UObject {};
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<TObjectPtr<UItemFragment>> Fragments;
```
The definition stays open to new facets without changing its base class,
irrelevant fields do not appear on unrelated items, and designers compose facets
in the editor.
Fragment rules: one responsibility each; state whether duplicates are legal;
validate required combinations; never depend on array order unless the order is
named and documented; fragments select and configure behaviour, they do not own
lifecycle.
---
## 7. Put cross-cutting policy in a policy definition
When many capabilities repeat the same relationships, do not copy the same
containers into all of them. Use a matrix selected by the archetype, so the same
capabilities can run under different policies.
Good candidates: capability relationships, presentation parameter bags,
sensitivity curves, class-to-widget mappings, quality thresholds.
Bad candidates: authority decisions, anti-cheat rules, irreversible lifecycle
transitions, and the validation logic itself.
---
## 8. Validation is part of the architecture
A data graph creates failure classes that code-only systems do not have. Every
definition family needs validation, and it needs to run somewhere other than the
editor.
### Universal checks
- required references non-null;
- arrays with no effective entries rejected;
- class derives from the required base or implements the required interface;
- tags valid and constrained to the expected category;
- identifiers resolve;
- soft targets belong to available, cooked plugins;
- duplicate semantic keys detected;
- client-only assets not bundled for the server, and the reverse;
- every action has a complete rollback path;
- production definitions reference no prototype or test content.
### Cross-family checks
These are the ones that catch real defects, because they cross an ownership
boundary that no single reviewer owns:
```text
playlist identifier → resolves to a registered gameplay definition
gameplay definition → archetype has a class, or is explicitly empty
archetype capabilities → input tags match a reachable input config
item equippable fragment → resolves to an equipment definition
equipment definition → capability packages valid for its instance type
pickup presentation → matches the item definition it claims to present
```
The last row is not theoretical. In the audited reference, two health pickups
reference the pistol item definition. It compiles, cooks, and ships. Recipe:
DD-07.
### Editor-only validation is feedback, not a boundary
If invalid data can reach a packaged build through an automated cook, the
validation must run there too. In the audited reference **eleven of twelve
validation entry points are compiled out of non-editor builds** — which is
normal, correct, and exactly why a commandlet or automation test has to exist
beside them. Recipe: DD-09.
---
## 9. Auditing an existing project
### A. Inventory both containers
1. Query the asset registry for instances deriving from the data-asset base.
2. Query the class registry for zero-node classes whose parent is a definition
type.
3. Include every mount root — the primary content root alone is incomplete in a
plugin-based project.
4. Exclude world-partition external actor packages from asset counts.
**Do not read binary asset files from disk.** Use the asset registry and live
editor APIs. This is a hard constraint, not a preference: a text search over
binary assets produces confident nonsense.
### B. Build the reference graph
For each definition record the owner layer, the consumer, the identifier, the
reference kinds, the bundle metadata, and the incoming and outgoing plugin
boundaries.
### C. Measure behaviour density
A definition class should have zero or very few graph nodes. A definition
carrying a large graph is secretly both data and manager.
### D. Trace one complete variant end to end
Do not stop at class declarations. Pick one real variant and follow it:
```text
playlist → definition → action sets → archetype
→ capability and input policy → item → equipment → grant
```
Record the actual configured values. Generic reflection frequently hides
inherited class-default fields and reports zero properties on a populated
object; read known fields by name from the schema instead of concluding the
object is empty. Recipe: DD-02.
---
## 10. Review checklist
- [ ] The definition's architectural layer has a name.
- [ ] It has exactly one consumer owner.
- [ ] Container type matches lifecycle.
- [ ] Reference kinds are intentional and written in metadata.
- [ ] Bundle metadata matches client and server use.
- [ ] Reuse is composition, not a chain of inherited defaults.
- [ ] Actions and fragments have focused responsibilities.
- [ ] Apply and rollback symmetry is proven, not assumed.
- [ ] Required links and semantic joins are validated.
- [ ] Cross-plugin references are acyclic and cooked.
- [ ] A real instance was traced end to end.
- [ ] Production data contains no prototype or copy-pasted references.
If more than two boxes are unknown, the architecture is not data-driven yet — it
is data-shaped.
---
## 11. When to stop
Introduce a definition layer when at least one is true: several modes reuse the
same composition; content ships after launch; variants are authored by people who
do not write code; a manager is accumulating per-variant branches.
Do not introduce one because the reference project has it. Every layer costs an
asset type, a validation surface, a loading path and a debugging hop. The
measured benefit of this architecture is that a mode becomes a list rather than a
subclass — if you have one mode, you are paying for an answer to a question you
do not have.
---
## Provenance
The measured material comes from an inspection of Epic's Lyra Starter Game on
Unreal Engine 5.6, performed through a live editor bridge rather than by reading
files: 84 native data assets across 21 runtime classes, 23 zero-node definition
classes, and 37 inline actions across ten definitions and five action sets. That
method matters — see DD-01, which exists because a purely text-based census of
this layer is structurally incapable of seeing most of it.
Source addresses and asset paths stay in the research archive that produced this
skill. Each entry carries a stable identifier (`DD-01`, `DD-03`, …) resolving
back to the audited location there.
## Evidence boundary
Values obtained through the editor describe one project at one moment. Graph
assets whose internals no safe reader exposed — environment queries, state trees,
blackboards — were listed but not read, and are marked as such rather than
summarised. Re-run the recipes against your own project; the counts here show the
shape of a contrast, not a target.
@@ -0,0 +1,401 @@
# Failure modes: data-driven architecture
Ten ways a data graph is wrong while remaining perfectly valid.
The shared property: **data does not compile, so nothing rejects it.** A null
reference, an unregistered identifier, a missing bundle annotation, a
copy-pasted prototype and a field nobody reads are all well-formed assets. They
load. They cook. The only thing that can reject them is validation somebody
chose to write, and that validation is usually compiled out of the build where
it would matter.
A second property specific to this area, and the reason the first recipe is
first: **most of this layer is invisible to text search.** Definitions live in
binary assets; the values live on class defaults. A recipe that greps source
answers a question about the *schema*, not about the *data*. Several recipes
below therefore require the editor, and say so rather than pretending a grep
will do.
---
## Census
### DD-01 - Half the definition layer is invisible to the census
**Mechanism.** A definition layer can be built from two containers at once: data
asset instances, and Blueprint classes with no graph nodes whose values live on
the class default object. An inventory that queries for one container reports a
complete-looking number.
**Why it is silent.** The number is real, plausible, and internally consistent.
Nothing indicates it is partial — an inventory has no way to report the
containers it did not think to look in.
**Why the obvious check misses it.** "Find the data assets" is the correct
phrasing of the wrong question. The second container is not a data asset by class,
is not named like one, and appears in the content browser as a Blueprint. In the
audited reference this was not a corner case: **every** gameplay definition, item
definition and equipment definition lived in the second container.
**Symptom.** Audits, rename sweeps, memory budgets and dependency graphs that are
each individually correct and collectively wrong. Decisions get made on a census
that missed the most important layer.
**Detect.** Count both containers, from the editor — this cannot be done from
source:
```python
# editor-side, via the asset registry
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# 1. instances deriving from the data asset base
# 2. blueprint classes whose native parent is a definition type,
# then check node count on the generated class
```
From source you can only establish the *schema* — which types exist and which
are meant to be subclassed:
```bash
rg -n "class \w+ : public UPrimaryDataAsset|class \w+ : public UDataAsset" \
--glob "*.h" .
rg -n "UCLASS\([^)]*Blueprintable" --glob "*.h" . | rg -i "definition|data"
```
**Guardrail.** Define "the data layer" as a set of *types*, not a set of asset
classes, and enumerate instances per type. Record the count per container so a
future reader can see which containers were searched.
---
### DD-02 - Reflection reports an empty object that is full
**Mechanism.** A generic property dump over a class default object returns
nothing, or almost nothing, for an object whose inherited fields are populated.
**Why it is silent.** An empty result is a legitimate answer — plenty of objects
genuinely have no properties set. The tool did not fail; it succeeded and
returned nothing.
**Why the obvious check misses it.** The check *is* the dump. Its output is
authoritative-looking structured data, and the natural conclusion — "this
definition is empty, so the values must live elsewhere" — sends the investigation
in a direction with no bottom.
**Symptom.** Hours spent looking for where the configuration "really" lives,
followed by a confident and wrong claim that a definition carries no data.
**Detect.** Never conclude from a generic dump. Read known fields by name,
driven by the schema you extracted from source:
```bash
# 1. get the field names from the C++ declaration
rg -n -A20 "class \w*ExperienceDefinition" --glob "*.h" . | rg "UPROPERTY" -A1
```
```python
# 2. editor-side, read each named field explicitly rather than enumerating
for field in ("GameFeaturesToEnable", "DefaultPawnData", "Actions", "ActionSets"):
value = cdo.get_editor_property(field)
```
A field that a generic dump omitted and a named read returns is the finding, and
it invalidates every conclusion drawn from the dump.
**Guardrail.** Treat generic reflection as a discovery aid and named reads as
evidence. When reporting "this object has no data", state which method produced
that answer.
---
## Loading and boundaries
### DD-03 - Soft reference with no bundle annotation
**Mechanism.** A definition holds a soft reference to content needed only by one
role. Without a bundle annotation, nothing tells the asset manager to pull that
content in when the owner loads.
**Why it is silent.** In the editor everything is loaded anyway — commonly both
role bundles at once — so the reference resolves and the feature works. The
failure requires a build where only what was asked for is present.
**Why the obvious check misses it.** The property is correct C++ and the
reference is correct data. The annotation is optional metadata whose absence
looks like every other property that legitimately does not need one. Reviewing
the definition shows nothing missing.
**Symptom.** A feature that works in the editor and silently does nothing in a
packaged build, because a class pointer resolved to null and the code path that
uses it treats null as "nothing to do".
**Detect.** List soft references, list annotations, and compare:
```bash
rg -n "TSoftObjectPtr<|TSoftClassPtr<" --glob "*.h" . | wc -l
rg -no 'AssetBundles="[^"]*"' --glob "*.h" . | sed 's/.*AssetBundles=//' \
| sort | uniq -c
```
The gap between the two counts is your exposure. In the audited reference the
whole project carried **ten** bundle annotations — two client-only and eight for
both roles — against a much larger population of soft references. Most of those
are loaded by other means; the point of the recipe is that it produces the short
list worth checking rather than a verdict.
**Guardrail.** Annotate at the declaration, beside the reference, and validate
that every soft reference in a definition family either carries an annotation or
is documented as loaded by its consumer.
---
### DD-04 - Bare accessor on a soft reference
**Mechanism.** The consumer resolves a soft reference with an accessor that
returns whatever is already loaded, and uses the result without checking.
**Why it is silent.** Inside the pipeline the annotation was designed for, the
asset is always loaded. The null path is unreachable in every configuration
anyone tests.
**Why the obvious check misses it.** The bundle annotation is visible on the
property, so the loading question appears answered. The accessor call is one
word and reads as a dereference, not as a decision.
**Symptom.** One feature's content silently missing, in one configuration, with
no log line naming the asset.
**Detect.** Find bare accessors on soft references in consumer code:
```bash
rg -n "\.Get\(\)" --glob "*.cpp" . | rg -i "class|widget|ability|component"
rg -n -A2 "\.Get\(\)" --glob "*.cpp" . | rg -c "if \(|ensure|check"
```
The finding is a resolve with no null branch. Compare against the same file's
other resolves — in the audited reference one loop tested for null and the
adjacent loop did not, which is the clearest possible evidence that the check was
intended.
**Guardrail.** Either load explicitly with a failure path, or check and log at
warning level with the asset name. "The bundle guarantees it" is a claim about
one pipeline, not about the code.
---
## Composition
### DD-05 - Inheritance used where composition was intended
**Mechanism.** A definition type is Blueprint-subclassable, so authors subclass
one definition from another to reuse its values, producing a chain of inherited
defaults.
**Why it is silent.** It works. Inherited defaults are a supported feature, and
the child behaves as the sum of the chain.
**Why the obvious check misses it.** Each subclass is individually reasonable and
locally minimal — it changes two fields. The cost is structural: values now come
from several assets that must be opened in sequence, and a change to a parent
silently alters every descendant.
**Symptom.** A definition whose effective configuration cannot be read from the
definition. Diffs that show nothing while behaviour changes.
**Detect.** Find whether the type permits subclassing, and whether validation
objects:
```bash
rg -n "UCLASS\([^)]*Blueprintable" --glob "*.h" . | rg -i "definition"
rg -n -B2 -A6 "IsDataValid" --glob "*.cpp" . | rg -i "parent|subclass|inherit"
```
The audited reference does this well and is worth copying exactly: its
definition validation **rejects Blueprint subclasses of Blueprint definitions**
and the error text names the alternative — use composition via action sets. A
validator that says what to do instead is worth several that only refuse.
**Guardrail.** Decide per definition type whether subclassing is legal. If it is
not, reject it in validation with a message naming the composition mechanism.
---
### DD-06 - An action set that is really a parent class
**Mechanism.** Reusable slices are introduced, and then one slice starts
depending on being applied after another, or on values from a third.
**Why it is silent.** Order is stable in practice, because it comes from a list
that rarely changes. The coupling produces correct behaviour for as long as
nobody reorders anything.
**Why the obvious check misses it.** Composition was adopted specifically to
avoid ordering questions, so nobody looks for one. The dependency is not
expressed anywhere — it exists only in the fact that the current order works.
**Symptom.** Reordering a list, or reusing one slice without its neighbour,
breaks a feature that has no visible relationship to the change.
**Detect.** Look for actions that read state another action writes:
```bash
rg -n "class UGameFeatureAction_\w+" --glob "*.h" . -A30 \
| rg "FindComponentByClass|GetSubsystem|Find\w+ForActor"
```
Every hit is an action that consumes something it did not create — a candidate
ordering dependency. For each, ask what happens if it runs first.
**Guardrail.** An action set is a set, not a sequence. If order matters, make it
explicit — a phase, a prerequisite declaration, or a single combined action — and
never leave it implied by list position.
---
## Content correctness
### DD-07 - Production data references the wrong definition
**Mechanism.** A presentation asset is duplicated to make a new one, and the
reference inside it is not updated. The copy points at the original's definition.
**Why it is silent.** Both assets are valid, both load, and the reference is a
real object of the right type. Nothing in the graph is broken; it simply says
something untrue.
**Why the obvious check misses it.** Validation checks that references are
non-null and correctly typed, which this passes. Confirming it means comparing
the *meaning* of two assets — that a health pickup should reference a health
item — and no type system encodes that.
**Symptom.** Picking up one thing and receiving another. In the audited
reference, two health pickups reference the pistol item definition. It ships.
**Detect.** This one is a cross-family check, and it needs the editor:
```python
# editor-side: for each pickup definition, compare its presentation identity
# against the item definition it references
for pickup in pickup_definitions:
item = pickup.get_editor_property("InventoryItemDefinition")
# heuristic that actually works: does the pickup asset's name share a token
# with the item definition's name?
```
The heuristic is crude and that is the point — a crude cross-family check finds
copy-paste errors that no type check can. Add it to validation and let it produce
warnings a human triages.
**Guardrail.** Every cross-family reference gets a semantic check, even a weak
one. Also: keep prototype content out of production roots so that a naming-based
check has a chance.
---
### DD-08 - Fields that exist, are authored, and are never read
**Mechanism.** A definition exposes extension fields for future use. They are
never wired to a consumer.
**Why it is silent.** An unread field costs nothing and looks like configuration.
Its default is usually the correct behaviour, so leaving it empty is right.
**Why the obvious check misses it.** The field is declared, is editable, and
appears next to fields that do work. "Is this used?" answers yes on the
declaration. Only searching for a *reader* answers correctly — and unlike the
same failure in C++, the reader may be in a Blueprint graph that no text search
can inspect, so a negative result is weaker here.
**Symptom.** A designer configures a field and observes no effect. Because the
consumer may be in a binary asset, the investigation cannot conclude from source
alone, and often stops with "it must do something".
**Detect.** From source, find declaration-without-reader candidates; then confirm
in the editor:
```bash
F='ExtraArgs'
rg -n "\b$F\b" --glob "*.h" --glob "*.cpp" .
```
A single hit — the declaration — makes the field a candidate. **Do not conclude
it is dead**: check the editor's reference viewer for Blueprint consumers before
deleting. In the audited reference several such fields were empty in every
instance, which is evidence about usage but not about wiring.
**Guardrail.** Do not ship an editable field with no consumer. If it exists for
forward compatibility, mark it as such where the designer sees it, not only in a
commit message.
---
## Validation
### DD-09 - Validation compiled out of the build that needs it
**Mechanism.** Data validation is written as editor-only, so it runs when an
author opens the asset and never during an automated cook.
**Why it is silent.** It is the correct default and it works — authors do get
feedback. Nothing degrades; the check simply is not present in the pipeline where
bad data would otherwise be caught.
**Why the obvious check misses it.** Searching for validation finds it, in
quantity, well written. The question that matters is narrower: *does any of it
run outside the editor?* — and answering that means reading preprocessor guards
rather than validation logic.
**Symptom.** Invalid data reaches a packaged build. The team's belief that "we
validate our data" is true and irrelevant.
**Detect.** Count validation entry points, then count the ones that survive a
non-editor build, then look for a runner:
```bash
rg -n "IsDataValid" --glob "*.h" . | wc -l
rg -n -B3 "IsDataValid" --glob "*.h" . | rg -c "WITH_EDITOR"
rg -n "Commandlet|AutomationTest|ValidateAssets" --glob "*.h" --glob "*.cs" .
```
In the audited reference this gave twelve entry points, **eleven** of them
editor-only, plus a content-validation commandlet and a validator base class —
so the mechanism to run them in automation exists and the question becomes
whether the pipeline invokes it.
**Guardrail.** Editor validation is feedback; a commandlet or automation test in
the build pipeline is the boundary. Ensure the runner **fails the build** on
error — a validator whose result is only printed is a log line, not a gate.
---
### DD-10 - The validator reports and the pipeline does not care
**Mechanism.** Validation runs in automation and prints errors. The process exit
status does not reflect them, so the pipeline continues.
**Why it is silent.** Everything works: the check runs, the errors are found, the
log contains them. The only missing link is the one nobody looks at until a bad
asset ships.
**Why the obvious check misses it.** Both halves are present and correct — there
is validation, and there is a pipeline step running it. The defect is in the
contract between them, which is a single integer nobody reads.
**Symptom.** A green build over a log full of validation errors. Confidence
inversely proportional to actual coverage.
**Detect.** Check what the runner returns and what the pipeline does with it:
```bash
rg -n -A20 "class UContentValidationCommandlet|int32 Main\(" --glob "*.h" --glob "*.cpp" . \
| rg "return |ExitCode|GIsCriticalError"
rg -n "Commandlet" .github/ *.yml *.yaml 2>/dev/null
```
A commandlet whose `Main` returns zero unconditionally is the finding. Confirm by
deliberately breaking one asset and running the pipeline step: if it stays green,
the gate does not exist.
**Guardrail.** Prove the gate by breaking something on purpose. **A check that
has never failed is indistinguishable from a check that cannot fail** — this is
the same principle as the poisoned-fixture rule for any linter, applied to your
content pipeline.
@@ -0,0 +1,268 @@
# Patterns: a measured data-driven layer
A worked example of the layering rules from `SKILL.md`, measured in one reference
product. Unlike most files in this bundle it was produced **through a live editor
session**, not by reading source — because the thing being measured is data, and
data lives in binary assets.
That method is the first lesson and it is not a technicality. Everything below
would have been invisible, or wrong, from a text search.
Markers: **[measured]** — read from live objects through the editor;
**[derived]** — conclusion from measured facts; **[open]** — not readable with
the tools available.
---
## 1. The census, and why the obvious census is wrong
| Container | Count **[measured]** | Visible to a data-asset query |
|---|---:|---|
| Native data-asset instances, 21 runtime classes | 84 | yes |
| Zero-node Blueprint classes used as definitions | 23 | **no** |
| **Total inspected definition objects** | **107** | |
The 23 break down as ten gameplay definitions, seven item definitions and six
equipment definitions **[measured]**. In other words: **every gameplay,
item and equipment definition in the project is in the container that a
data-asset query does not return.**
**[derived]** A census that asks "which data assets exist?" returns 84, is
internally consistent, and misses the layer that actually composes the game. This
is the single most portable warning in this file. Recipe: DD-01.
The same structure produced a second trap. A generic property dump over one of
those class defaults reported almost nothing, because inherited fields were not
enumerated **[measured]**. Reading the *named* fields from the schema returned
full configuration. **[derived]** An empty reflection result is evidence about the
reflection call, not about the object. Recipe: DD-02.
---
## 2. The layer split, as actually built
```text
playlist (8 instances) map + definition + session and menu metadata
└─ gameplay definition (10) plugins + archetype + action sets + delta
├─ action set (5) reusable slice
└─ archetype (6) pawn class, capabilities, input, policy, camera
├─ capability package (12)
├─ input config (5)
└─ policy matrix (1, nine rules)
```
Two properties of this split are worth copying.
**The playlist knows about maps and menus; the definition does not.** One mode
can run on several maps, one map can host several modes, and menu metadata never
enters the runtime composition **[measured]** — all eight playlists were verified
to carry map, definition, visibility, default flag and player count, and nothing
gameplay-facing.
**Reuse is a list, not a parent.** Two shooter modes differ only by a
mode-specific delta — a different capability package, different scoring and music
components, different score widgets — while sharing input, components and HUD as
action sets **[measured]**. Adding a mode is editing a list.
That is enforced rather than merely encouraged: validation **rejects a Blueprint
subclass of a Blueprint definition** and its error text names composition as the
alternative **[measured]**. A validator that says what to do instead is worth
several that only refuse. Recipe: DD-05.
---
## 3. Composition is dependency injection, spelled as data
37 inline actions across ten definitions and five action sets **[measured]**. The
shape of the most common one:
```text
(target actor class, component class, apply on client?, apply on server?)
```
One mode adds scoring on both roles, music on the client only, and bot spawning,
team setup and spawn rules on the server only — **without modifying the game state
class** **[measured]**. A second mode swaps three of those and keeps the rest.
**[derived]** This is the payoff of the whole architecture, and it is worth
stating plainly: the base classes contain no branch per mode, because the modes
are not known to them. The switch test from the skill is passed by construction.
The same mechanism carries UI: one action pushes a layout into a layer, another
registers eleven widgets into named slots **[measured]**. Mode-specific UI is two
more rows in a list.
---
## 4. Where the role boundary is declared
Soft references in the action data carry bundle annotations, so the definition
declares composition **and** the cook boundary in one place **[measured]**.
The measured population is small and the number is instructive: **ten annotations
in the entire project — two for the client, eight for both roles** **[measured]**.
**[derived]** That is not a criticism; most content is loaded by other means. It
is a statement about how narrow the annotated surface is, which matters because
the consumers of those references use a bare accessor that returns null when the
bundle did not deliver, and one of the two loops checks the result while its
neighbour does not **[measured]**. The annotation and the null check are the two
halves of one contract, and only one half is consistently present. Recipes:
DD-03, DD-04.
---
## 5. The capability layer, and why input is not one-to-one with it
12 capability packages **[measured]**, ranging from an eleven-capability hero
package down to packages that grant **only effects** and no capabilities at all.
The instructive detail: **not every granted capability has an input tag**
**[measured]**. Death, spawn effects, auto-reload and auto-respawn are activated
by events and policies, not by a button.
**[derived]** This is the concrete reason to keep the semantic input layer
separate from the capability layer rather than folding one into the other. A
design that assumes "capability implies binding" has no place to put the four
above, and will grow a special case for each.
Six archetypes reuse the same capability packages across different pawn classes
and input configurations **[measured]** — one archetype reuses the shooter
capability package while changing pawn class and input config, which demonstrates
that implementation and capability are genuinely independent axes.
One archetype has a **null** pawn class **[measured]**. **[derived]** It is a
sentinel used as a configuration fallback, not an archetype — worth recognising
because a validator that requires a non-null class must special-case it, and a
validator that does not will be silently satisfied by every broken archetype.
---
## 6. Fragments: composition inside a definition
Item definitions carry a display name and a list of instanced polymorphic
fragments; consumers query by fragment class **[measured]**. The measured pipeline:
```text
pickup presentation (data asset)
└─ item definition (zero-node class)
├─ display, quickbar, pickup, reticle fragments
├─ ammo statistics
└─ equippable fragment → equipment definition (zero-node class)
├─ runtime instance class
├─ capability package
└─ actor to spawn, at a named socket
```
Three weapons differ only in these values **[measured]** — different instance
class, capability package, spawned visual, reticle fragment and ammo numbers —
with no change to the inventory or equipment managers.
**[derived]** The fragment list is what keeps the item definition from growing a
boolean and five fields for every item type that will ever exist. It is the same
move as the action list one layer up, applied inside a single asset.
---
## 7. The two content defects, and what they teach
**A health pickup that references the pistol item definition.** Two of them
**[measured]**. Both assets are valid, correctly typed and non-null; the graph is
intact and says something untrue.
**[derived]** No type check can catch this, and no validation in the project does.
The only mechanism that would is a weak semantic check — does a pickup's identity
share anything with the item it presents? — which feels too crude to write until
you see what it catches. Recipe: DD-07.
**Prototype content in the production tree.** One pickup carries a display name
and visual assets belonging to a different weapon entirely **[measured]**.
**[derived]** Prototype content in a production root is what makes the crude check
above hard to write, because naming stops being a signal. The two defects
compound: keep prototypes out of production roots and a naming heuristic becomes
viable.
Both are recorded here for the reason the whole bundle exists: **these shipped in
a reference project that many teams copy from.** Copying the architecture is
correct; copying the content is not.
---
## 8. Fields that exist and are never set
All eight playlists carry extra launch arguments and a replay flag. In every one
of the eight, the arguments are empty and the flag is false **[measured]**.
**[open]** Whether anything reads them cannot be settled from source, because a
consumer could be in a Blueprint graph. Recorded as open rather than concluded —
which is the honest form of this answer and the same discipline as any empty
search result.
**[derived]** Regardless of wiring, a field that is empty in every instance is an
extension surface rather than a feature, and a reader who assumes otherwise will
look for behaviour that is not there. Recipe: DD-08.
---
## 9. Validation: present, well built, and mostly not in the pipeline
| Fact **[measured]** | Reading |
|---|---|
| 12 validation entry points across the definition families | The discipline exists and is applied broadly |
| **11 of them compiled out of non-editor builds** | Feedback for authors, not a gate on the build |
| A content-validation commandlet and a validator base class exist | The mechanism to run them in automation is present |
**[derived]** This is the correct default arrangement, and it means the whole
question reduces to one that source cannot answer: does the build pipeline invoke
the commandlet, and does it fail on error? A validator whose result is printed
rather than returned is a log line.
Worth being precise about, because an earlier form of this material overstated
it: **structural validation exists in this project.** What does not exist is
semantic cross-family validation — nothing checks that a pickup presents the item
it claims to. The correct criticism is narrow, and the broad version ("the data
graph has no compiler") is wrong. Recipes: DD-09, DD-10.
---
## 10. What to copy, and in what order
1. **Separate the launch catalog from the runtime definition.** One asset type,
large payoff, no dependencies on anything else here.
2. **Reusable slices as flat lists, and reject definition inheritance in
validation** with a message naming the alternative.
3. **Fragments for optional facets**, so item definitions do not grow a field per
item type.
4. **Bundle annotations beside soft references**, plus a null check at every
consumer — the two halves of one contract.
5. **A cross-family semantic check**, however crude, run in automation with a
non-zero exit status.
Items 1–3 are architecture and transfer directly. Items 4–5 are the parts the
audited reference left incomplete, and are therefore the parts most likely to be
inherited by anyone copying it.
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6 through a live
editor bridge on a single day: 84 native data assets across 21 runtime classes,
23 zero-node definition classes, 37 inline actions, all eight playlists, six
archetypes, twelve capability packages and five input configurations, read as
live objects and class defaults. No binary asset was parsed from disk; no
gameplay session was run.
Asset paths and configured values stay in the research archive that produced this
skill. Each `DD-` identifier resolves back to the audited location there.
## Evidence boundary
Graph assets — environment queries, state trees, blackboards — were enumerated
but their internals were **not** read, because no safe reader was available.
They are listed as present and nothing is claimed about their contents.
Everything here describes one project at one moment through one tool. Counts are
quoted to show the shape of a contrast, not as targets. Re-run the census against
your own project, and count both containers.