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:
2026-09-05 23:48:55 +07:00
parent 80ea7b03a9
commit ecd87ac96d
67 changed files with 21630 additions and 0 deletions
@@ -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.