ecd87ac96d
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>
402 lines
16 KiB
Markdown
402 lines
16 KiB
Markdown
# 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.
|