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:
@@ -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.
|
||||
+401
@@ -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.
|
||||
Reference in New Issue
Block a user