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,319 @@
---
name: ue-architecture-guardrails
description: >-
Audit an Unreal Engine architecture for systemic failure modes across
data-driven definitions, feature plugins, ability systems, input, UI,
messaging, cosmetics and teams, settings and editor workflows. Use before
adopting a modular architecture, at design review, before enabling runtime
feature switching, during engine upgrades, or when indirect systems have
become hard to debug. Provides ownership, lifecycle, context, validation,
observability and integration-test gates.
---
# UE architecture guardrails
This skill is for **cross-system review**. It does not cover the implementation
of any one subsystem — each of those has its own skill and its own detection
recipes, and this one tells you which of them to run and in what order.
Systemic patterns and the go/no-go review: [patterns](references/patterns.md).
Fifteen cross-boundary failure modes: [failure modes](references/failure-modes.md).
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
The invariant behind all of it:
> Every systemic failure in a modular architecture lives **between** two
> subsystems that are each individually correct. That is why no subsystem's
> tests find it, and why review by subsystem cannot either.
---
## The seven invariants
A modular, data-driven architecture is healthy only when all seven hold:
1. **Justified abstraction** — every layer pays for real, named variability.
2. **Explicit ownership** — every dynamic resource has an owner, a receipt and
an inverse.
3. **Symmetric lifecycle** — activate/deactivate and create/destroy are round
trips, tested as round trips.
4. **Explicit context** — world, local player, authority and activation context
are never inferred from globals.
5. **Compiled data graph** — semantic joins, references and bundles are
validated by something that can fail a build.
6. **Observable indirection** — composition, blockers and listeners can be
dumped at runtime.
7. **Cross-boundary tests** — lifecycle is tested across subsystem seams, not
inside one manager.
**If a design review cannot show evidence for an invariant, treat it as absent.**
Not "probably fine" — absent. Every finding in the failure modes was in a
codebase where the invariant was assumed rather than demonstrated.
---
## 1. Abstraction pressure test
For each proposed layer, fill this in before writing code:
| Layer | Concrete variability #1 | #2 | Operations it removes | New lifecycle and tests it adds |
|---|---|---|---|---|
Reject or defer a layer when it has fewer than two real variants, when direct
code would be smaller and equally safe, when the project will not use dynamic
activation or a packaging boundary, or when the team cannot maintain the
validation and observability the layer requires.
**Do not adopt a large-studio architecture as an identity marker.** The cost is
paid per layer, per release, by whoever debugs it. Recipe: AG-01.
---
## 2. Ownership audit
Build a ledger for every dynamically added resource:
```text
resource · owner · scope · receipt · apply · revoke · sharing policy · rollback
```
Red flags, each of which has been observed in shipping code:
- a weak pointer presented as ownership;
- a global subsystem that "owns everything";
- cleanup that clears **all** resources rather than owned ones;
- a handle discarded immediately after registration;
- an owner that can die before the resource;
- two owners that can both remove one shared resource.
### The kill-owner test
Destroy each owner at the worst moment: during an async load, after apply but
before deactivation, after the target actor is gone, during map travel. Every
owned resource must disappear or transfer explicitly.
Subsystem recipes: `ue-modular-gameplay` MG-01 to MG-03,
`ue-input-architecture` IN-03, `ue-gas-architecture` GA-01. Recipe: AG-02.
---
## 3. Lifecycle as a transaction
```text
validate → load → activate 1..N → committed
failure at K → reverse K-1..1 → failed, with a reason
```
Three tests, all mandatory:
```text
baseline → activate → deactivate → baseline
baseline → activate → fail each step → baseline
baseline → activate → deactivate → reactivate → exactly one copy
```
The third is the one that finds duplicates, and it is the one nobody runs during
development, because in development you restart the editor instead. Recipe:
AG-03.
---
## 4. Context audit
For every mutable map and every subscription, ask which key is actually
required: process, game instance, world handle, activation context, local
player, actor, or net role. **A global key is valid only when the behaviour is
genuinely global.**
Required scenario matrix:
| Scenario | What it catches |
|---|---|
| multi-world editor play | process-global leaks |
| dedicated server + client | local-broadcast and presentation assumptions |
| listen server | double execution of local paths |
| two local players | UI, input and settings context leaks |
| map travel | stale listeners and state on long-lived scopes |
Recipes: AG-04, plus `ue-game-settings-architecture` GS-03 and
`ue-gameplay-messaging` MB-08.
---
## 5. Compile the data graph
Generate and validate the whole chain: production roots → identifiers →
references → plugin boundaries → bundles → semantic joins → leaf assets.
Failures that must fail a build, not a log line: unresolved identifiers, null
required fields, plugin dependency cycles, role-mismatched bundles, orphaned
producers or consumers of a semantic tag, prototype content under a production
root, an input tag with no granted consumer, a UI contribution with no matching
point, an incompatible payload family on a parent channel.
**Editor-only validation is feedback, not a gate.** Recipes: AG-05, plus
`ue-data-driven-architecture` DD-09 and DD-10.
---
## 6. Temporal dependency audit
Draw initialization prerequisites as a directed graph and reject: any cycle; a
prerequisite produced only by an optional or disabled path; an anonymous
next-tick delay; a generic "extension added" event used where semantic readiness
is required; a transition that can block with no diagnostic reason.
Then permute: feature activation before and after actor spawn, data before and
after possession, replication arrival order, contribution before and after its
point, listener before and after event. **Correct systems converge to the same
state.** Recipes: AG-06, plus `ue-modular-gameplay` MG-14 and MG-15.
---
## 7. Observability requirements
At minimum, expose read-only dumps for: the selected composition and its load
state; pending assets, plugins and actions with reasons; active feature plugins
and who requires them; per-actor readiness state and blockers; active input
contexts and semantic joins; granted capabilities by owner receipt; UI layers,
points and contributions with owners; message channels and listener counts;
authoritative identity and its mirrors; requested versus effective settings.
Logs need correlation fields: world, local player, actor, composition, plugin,
action, channel, owner.
**If diagnosis requires stepping through many assets with no state dump, the
indirection is operationally incomplete** — the architecture works and cannot be
supported. Recipe: AG-07.
---
## 8. The false-genericity test
For every public field or hook that implies behaviour — an ordering value, a
viewer parameter, an automatic mode, a removal function, a policy suffix:
1. find its consumer;
2. mutate the value in a fixture;
3. assert an observable delta;
4. test both the native and the scripting API where both are exposed.
No consumer and no delta means: implement it, remove it, or mark it
unsupported. **Never let a configurable no-op survive as stable API.**
This single test has the highest yield of anything in this skill. In the audited
material it independently found an ordering field that never sorts, a viewer
parameter that is ignored, an automatic quality pipeline with no caller, a
removal function with an empty body, and a composition part with no writer.
Recipe: AG-08, plus `ue-ui-architecture` UI-01, `ue-cosmetics-and-teams` CT-10,
`ue-game-settings-architecture` GS-06 to GS-08.
---
## 9. Event, state and command
Classify every interaction:
- **state** — a queryable authoritative model;
- **command** — one accountable service with a result and a failure;
- **event** — zero or many optional observers, a past-tense fact.
Red flags: UI reconstructing state from transient messages; a message asking an
unknown listener to perform required work; a local bus assumed to replicate;
presentation treated as authority. Recipes: AG-09, plus
`ue-gameplay-messaging` MB-12 to MB-14.
---
## 10. Version and adoption audit
Before copying any framework or sample, classify each element: architectural
invariant, sample scaffolding, project-specific behaviour, incomplete path,
confirmed defect, prototype content, version-sensitive API. Then pin the engine
version, the sample version, plugin versions and any serialized schema version,
and re-run behaviour probes after every upgrade.
**A current documentation page does not prove the content you copied is
current.** See `ue-reference-project-adoption` for the full classification and
its four tests. Recipe: AG-10.
---
## 11. Risk-based integration matrix
Do not attempt a Cartesian product. Require this spine:
1. standalone baseline;
2. dedicated server + remote client;
3. listen server;
4. two local players;
5. feature activates **before** receiver readiness;
6. feature activates **after** receiver readiness;
7. deactivate and reactivate;
8. actor destroyed before feature teardown;
9. late join;
10. map travel or composition change;
11. packaged clean client;
12. engine or plugin upgrade fixture.
**Every critical invariant needs at least one test crossing two subsystem
boundaries.** Unit tests inside one manager cannot find anything in this skill.
---
## 12. Severity triage
**Critical — stop feature work.** Ownership or rollback unknown; authority
duplicated; world or local-player context leaked; production data graph
unvalidated; persistent state reconstructed from messages; client presentation
affecting authority.
**High — fix before runtime switching or shipping.** Initialization cycle or
timing dependency; missing bundle with no fallback; native/scripting semantic
mismatch; undocumented ordering dependency; insufficient observability; version
mismatch.
**Medium — track with a bound.** Abstraction tax; linear scans acceptable at
current data size; editor-only gaps; duplicated scaffolding.
---
## 13. Go/no-go
- [ ] Every layer has two named variability pressures.
- [ ] Ownership ledger complete, kill-owner test passes.
- [ ] Lifecycle rollback and reactivation tests pass.
- [ ] Context matrix passes.
- [ ] Data graph validation runs where it can fail a build.
- [ ] Prerequisite graph acyclic and observable.
- [ ] State dumps and log correlation exist.
- [ ] Every public generic hook has a consumer test.
- [ ] Event, state and command classifications are correct.
- [ ] Replicated intent has a packaged-client realization test.
- [ ] Version set pinned.
- [ ] Cross-boundary matrix automated or scheduled.
**No-go rule: three unchecked Critical gates mean stop adding abstraction.**
Repair ownership, context and validation first — further indirection multiplies
the failure surface rather than the capability.
---
## Provenance
The systemic patterns and their severities come from a cross-system audit of
Epic's Lyra Starter Game on Unreal Engine 5.6 — the same audit that produced the
subsystem skills in this bundle. This skill exists because the same shapes
appeared in every subsystem: the individual findings are in those skills, and
what is here is the structure they share.
Source addresses stay in the research archive. Each entry carries a stable
identifier (`AG-01`, `AG-02`, …) resolving back to the audited material there.
## Evidence boundary
Cross-system conclusions from one project at one engine version, read as source
rather than run. Where a finding is a general principle rather than a measured
instance, the failure modes say so explicitly. The subsystem recipes referenced
above are the measured half; this skill is the ordering.
@@ -0,0 +1,379 @@
# Failure modes: cross-system architecture
Ten failures that belong to **no single subsystem**, which is why each one
survived review of every subsystem it passes through.
The shared property here is different from the other documents in this bundle.
Elsewhere the mechanism is "a check exists and cannot fire". Here it is: **every
individual component is correct, and the defect lives in the seam.** Nobody owns
a seam. A reviewer of the messaging layer sees a correct bus; a reviewer of the
UI sees a correct widget; the failure is that the UI reconstructs state from the
bus, and that fact is written down nowhere.
That has a practical consequence for how these are detected. Most recipes below
are not a single search — they are a **join between two searches**, and the
finding is in the difference between the two result sets.
Where a mechanism is fully described by another skill, the entry says so and does
not restate it. Identifiers (`AG-01` and up) are stable and resolve back to the
audited material in the research archive.
---
## Abstraction
### AG-01 - A layer adopted for completeness rather than pressure
**Mechanism.** A modular architecture is copied whole from a reference project
because it is the reference's architecture, without asking which variability each
layer absorbs. Every layer is correctly implemented and paid for in full.
**Why it is silent.** Nothing fails. The cost is not a defect but a permanent tax:
every new mechanic now needs a class, a definition asset, a tag, an action, a
plugin, a validation rule and a UI extension. The team experiences this as "the
engine is like this".
**Why the obvious check misses it.** Review asks "is this implemented correctly?"
and the answer is yes at every layer. The question that finds it — "what are the
two concrete variants this layer separates?" — is not part of any code review,
because it is not about code.
**Symptom.** Feature velocity that falls as the project matures, with no single
slow component. Estimates that are consistently wrong in the same direction.
**Detect.** Count the axes against the variants they serve:
```bash
rg -c "class \w+ : public UGameFeatureAction" --glob "*.h" . # action types
fd -e uasset -p "Experiences|GameFeature" | wc -l # variants shipped
```
The finding is arithmetic, not textual: **planned variants fewer than
architectural axes.** In the audited reference, five feature plugins out of
eighty-one total carried the modular machinery — which pays for that project and
would not pay for a single-mode title.
**Guardrail.** For each layer, name two real variabilities it separates before
adopting it. If you cannot, defer the layer. See `ue-reference-project-adoption`
for the full classification and the adoption budget.
---
### AG-02 - Distributed control flow with no trace
**Mechanism.** A request travels definition → feature → action → extension event →
component → tag → message → widget. Each hop is decoupled by design.
**Why it is silent.** Decoupling is working exactly as intended. The absence of a
call stack is the feature, not a defect.
**Why the obvious check misses it.** A debugger shows one hop. Each hop's owner
can explain their hop. Nobody can explain the path, because reconstructing it
requires reading assets, config and code in three modules — and it has to be
reconstructed again next time.
**Symptom.** "Who triggered this?" costs an afternoon. Bugs reproduce reliably
while no class looks responsible. New engineers take months rather than weeks.
**Detect.** Test the property directly rather than searching for it: take a
recent bug and ask an engineer who did not write the feature to reconstruct the
path **from logs and dumps alone**, without opening assets. Then measure what
they needed and did not have.
Structurally, the precondition is visible:
```bash
rg -c "GetSubsystem<|BroadcastMessage|SendGameFrameworkComponentExtensionEvent" --glob "*.cpp" .
rg -c "UE_LOG.*Verbose.*(World|LocalPlayer|Experience|Feature)" --glob "*.cpp" .
```
A large first number with a small second is the finding: heavy indirection, no
correlated logging.
**Guardrail.** Structured lifecycle logs carrying world, player, experience,
plugin, action and owner identifiers; a generated composition graph; one index of
semantic tags and their consumers. Indirection without observability is not
architecture, it is a maze.
---
## Ownership and lifecycle
### AG-03 - Activation reviewed, deactivation assumed
**Mechanism.** Adding a component, a binding, a widget or a grant is easy and
visible. The inverse operation is written from memory, or not at all.
**Why it is silent.** Development restarts the editor instead of deactivating.
The happy path is exercised hundreds of times a day; the reverse path is
exercised by nobody until a player switches modes.
**Why the obvious check misses it.** Both functions usually exist and look
symmetric. The asymmetry is in what each enumerates — see `ue-modular-gameplay`
recipes MG-01 through MG-06 for the six distinct shapes this takes, each with its
own recipe.
**Symptom.** The second activation produces duplicates. Reported long after the
first, and usually attributed to the feature that was activated second.
**Detect.** The behavioural test is stronger than any search, and it is the one
gate this whole skill exists to insist on:
```text
baseline → activate → verify additions → deactivate → verify baseline
→ reactivate → verify exactly one copy
```
Run it with the actor existing before activation, spawned after activation, and
destroyed before deactivation.
**Guardrail.** An ownership ledger — resource, receipt, inverse — filled in
**before** implementation. A blank middle column is a rejected design, not a
follow-up task.
---
### AG-04 - A subsystem's lifetime mistaken for resource ownership
**Mechanism.** A long-lived subsystem holds registrations for resources owned by
short-lived objects. Weak references prevent crashes, so nothing appears wrong.
**Why it is silent.** Weak pointers do their job: the dead object is not called.
The **record** remains, and records are not visible in any profiler view that
answers "is this leaking?".
**Why the obvious check misses it.** The code is defensively written and looks
careful. Weak references read as evidence that ownership was considered — when
they are precisely the mechanism that lets the bookkeeping rot silently.
**Symptom.** Registration lists that grow across a session; cleanup that resorts
to clearing everything because per-owner removal was never possible; the same
resource removed twice by two owners.
**Detect.** For every registry, compare adds against removes and check the key:
```bash
rg -n "\.Add\(|\.Emplace\(|\.FindOrAdd\(" --glob "*.cpp" . | rg -i "listener|extension|handle|request"
rg -n "\.Remove\(|\.RemoveSwap\(|Unregister" --glob "*.cpp" . | rg -i "listener|extension|handle|request"
```
A registry with adds and no owner-keyed removal is the finding. A registry whose
only removal is a full clear is the same finding, one step later.
**Guardrail.** Every dynamic resource has exactly one named owner and one receipt.
Shared resources use reference counts or leases. "The subsystem owns it" is not an
answer; subsystems outlive the things they track.
---
## Context
### AG-05 - Global state where the scope is world or player
**Mechanism.** A registration, cache or setting is keyed globally, while the
things it describes belong to a world, a local player or an activation context.
**Why it is silent.** With one world and one player — the configuration in which
almost all testing happens — global and scoped are indistinguishable. The code is
correct in the case you run.
**Why the obvious check misses it.** The accessor reads naturally: a player asking
for its own settings, a subsystem holding its own registry. The scope error is one
line inside an accessor, or a missing key in a map declaration.
**Symptom.** Multi-world editor sessions cross-contaminate; split-screen players
share what should be per-player; a listen server processes an event twice. Each
appears as an unrelated bug in a different subsystem.
**Detect.** For every mutable registry, ask what the minimum sufficient key is,
then check what it actually is:
```bash
rg -n "TMap<.*>\s+\w+;" --glob "*.h" . | rg -v "FObjectKey|FGameFeatureStateChangeContext|ULocalPlayer"
rg -n -A4 "::Get\w*Settings\(\)" --glob "*.cpp" . | rg "::Get\(\)|GEngine->"
```
The second search is the specific case documented in
`ue-game-settings-architecture` GS-03: a per-player accessor returning a global
singleton, whose tell is a proliferation of "primary player only" conditions
elsewhere.
**Guardrail.** Key every mutable record by the minimum context that makes it
correct — activation context, world handle, local player where applicable. Then
run the required matrix: multi-world editor, dedicated server plus client, listen
server, two local players, map travel.
---
### AG-06 - Editor behaviour that differs from the shipped configuration
**Mechanism.** A branch keyed on running in the editor loads more, validates less,
or guesses identifiers that the packaged build resolves strictly.
**Why it is silent.** Both branches are correct for their environment. The editor
branch is usually more permissive, so everything works better where you are
looking.
**Why the obvious check misses it.** The branch is a single condition in a
subsystem nobody reads while working on a feature. Its consequences appear in
memory profiles, cook results and packaged-only failures — three places that are
each somebody else's job.
**Symptom.** Memory numbers that describe no shipping configuration. Features that
work in the editor and silently do nothing when packaged. Asset identifiers that
resolve in one and not the other.
**Detect.** Enumerate every editor divergence and judge each one deliberately:
```bash
rg -n "GIsEditor|WITH_EDITOR|IsRunningCommandlet|GIsPlayInEditorWorld" --glob "*.cpp" . -A3
rg -n "bShouldGuessTypeAndNameInEditor|PreloadInEditor|bOnlyCookProduction" Config/
```
In the audited reference this finds an editor branch that loads **both** role
bundles — which alone invalidates in-editor residency measurement — and a
configuration that guesses asset identifiers in the editor and not in the build.
**Guardrail.** Keep a written list of editor divergences and their justification.
Any measurement taken in the editor states which divergences apply to it.
See `ue-asset-loading-and-memory` AL-10.
---
## Data and validation
### AG-07 - A data graph with no compiler
**Mechanism.** Null references, wrong identifiers, cross-plugin cycles, prototype
content and semantic mismatches are all valid data. They load, they cook, they run.
**Why it is silent.** Data does not compile. There is no stage that can reject it
except one somebody chose to write — and that validation is typically
editor-only, so it does not run where it would matter.
**Why the obvious check misses it.** The validation *exists*, which satisfies the
question "is the data validated?". What it does not do is run in the build. In the
audited reference, eleven of twelve validation implementations were compiled out
of non-editor builds.
**Symptom.** A production playlist pointing at a test mode; a health pickup
granting a weapon definition; a missing bundle discovered only in a packaged
build.
**Detect.** Count validation, then count how much of it survives the build:
```bash
rg -c "IsDataValid" --glob "*.cpp" .
rg -n -B6 "IsDataValid" --glob "*.cpp" . | rg -c "WITH_EDITOR"
rg -n "class \w*ValidationCommandlet|UEditorValidatorBase" --glob "*.h" .
```
A ratio close to one, with no commandlet or automation path, means the data graph
is unvalidated where it ships. See `ue-data-driven-architecture` DD-09 and DD-10.
**Guardrail.** Run the same validation in automation that you run in the editor.
Add a production-root allow list so prototype paths cannot reach a shipped
playlist.
---
### AG-08 - A generic hook with no consumer
**Mechanism.** A public field or extension point is stored, copied and threaded
through an API — and never read by anything that changes behaviour.
**Why it is silent.** Every "is this used?" check answers yes, because the value
*is* used: passed, assigned, copied. What is missing is the comparison, the
branch, or the sort.
**Why the obvious check misses it.** This is the single most repeated shape in
this whole bundle, and it earns its own cross-system entry because it recurs in
every subsystem independently: an ordering field that never sorts, a viewer
identity that is ignored, a benchmark decision with no caller, a profile suffix
that is never populated, a removal function with an empty body, a flag written in
a constructor and never read.
**Symptom.** A designer configures a documented setting and observes no effect,
concludes their data is wrong, and works around it.
**Detect.** The general form — mentions minus comparisons:
```bash
F='Priority'
rg -c "\b$F\b" --glob "*.cpp" --glob "*.h" . # mentions
rg -n "\b$F\b\s*(<|>|<=|>=|==)|Sort.*\b$F\b|\b$F\b.*Sort" --glob "*.cpp" .
```
Mentions without comparisons is the finding. Per-subsystem instances have their
own recipes: `ue-ui-architecture` UI-01, `ue-cosmetics-and-teams` CT-10,
`ue-game-settings-architecture` GS-06 and GS-07, `ue-gas-architecture` GA-06,
`ue-modular-gameplay` MG-01.
**Guardrail.** Every public setting gets a consumer test: mutate it in a fixture,
assert an observable delta. Anything without one is implemented, removed, or
marked unsupported — never left as configurable decoration.
---
## Verification
### AG-09 - Single-process testing of a distributed property
**Mechanism.** A listen-server host shares memory with its client, so the
client/server split that produces a whole class of defects does not exist during
the test that would have caught it.
**Why it is silent.** The tests pass. They are real tests exercising real code;
they simply cannot express the failure.
**Why the obvious check misses it.** Coverage looks good and the feature demonstrably
works. The missing dimension is a **configuration**, not a code path, so no
coverage tool reports it.
**Symptom.** Features that ship having never run in the configuration they will
run in. Bugs that appear at first playtest and are attributed to the network layer.
**Detect.** This one is answered by an inventory rather than a search: list the
failure modes that require a separate process, and confirm each has a test in a
configuration that has one. From this bundle, the ones that cannot occur in a
single process include replicated-versus-validated confusion, call-site authority
guards, multicast used where state belongs, incomplete replicated-array callbacks,
readiness gates that assume a controller, and prediction with no correction path
(`ue-multiplayer-authority` NA-01, NA-04, NA-11, NA-13, NA-17, NA-19).
**Guardrail.** A dedicated server with two remote clients, one under latency and
packet loss, as the **minimum** configuration for accepting a networked feature.
Plus a mid-match joiner.
---
### AG-10 - A version set that drifts silently
**Mechanism.** Engine, reference sample and plugins evolve independently. Code
copied from a sample at one version keeps running against another.
**Why it is silent.** Compilation succeeds. Serialized fields still load. A stub
that changed behaviour between versions still returns something plausible.
**Why the obvious check misses it.** Documentation for the current version is
easy to find and describes the current version — not the vendored copy in the
project. Reading the docs actively produces false confidence.
**Symptom.** A method that "works differently now"; a structure size assertion
that fails after an upgrade; content that disagrees with the plugin that reads it.
**Detect.** Pin and verify rather than search. Where code manually enumerates the
members of an engine structure, a size assertion is the correct tripwire:
```bash
rg -n "static_assert\(sizeof\(" --glob "*.cpp" --glob "*.h" .
```
In the audited reference this appears five times against one engine structure. A
failing assertion after an upgrade is a **feature**: it means a new field would
otherwise have been silently ignored by five hand-written functions.
**Guardrail.** Pin the engine, sample and plugin versions. Keep compile-time
guards where you enumerate engine structures by hand. Re-run structural and
behavioural probes after every upgrade, and never treat a documentation page as
evidence about the code in your tree.
@@ -0,0 +1,330 @@
# Patterns: reviewing an architecture across its seams
This is the cross-system half of the bundle. The other fifteen skills each audit
one subsystem; this one exists because the most expensive failures are not inside
a subsystem at all.
[Failure modes](failure-modes.md) carries the detection recipes. This file
carries the review procedure — what to ask, in what order, and what evidence to
demand before believing an answer.
---
## 1. Why seams are where the cost is
A subsystem has an owner. Someone can be asked whether the ability system is
correct, and they can answer.
A seam has no owner. "The UI reconstructs state from the message bus" is not a
fact about the UI or about the bus; it is a fact about their relationship, and
relationships do not appear in any file. Every finding in this skill was reviewed
— twice, once from each side — and survived.
That produces the review technique this whole document is built on:
> **Ask each question of the pair, not of the component.** Not "is this registry
> correct?" but "which context is this registry keyed by, and which context do its
> entries belong to?" The defect lives in the difference.
---
## 2. The seven invariants
An architecture of this kind is healthy only when all seven hold:
1. **Justified abstraction** — every layer pays for real variability.
2. **Explicit ownership** — every dynamic resource has an owner, a receipt and an
inverse.
3. **Symmetric lifecycle** — activate/deactivate and create/destroy are round
trips, tested as round trips.
4. **Explicit context** — world, local player, authority and activation context
are never inferred from globals.
5. **Compiled data graph** — semantic joins, references and bundles are validated
somewhere that runs in the build.
6. **Observable indirection** — composition, blockers and listeners can be dumped.
7. **Cross-boundary tests** — lifecycle is tested across subsystem seams.
**If a review cannot produce evidence for an invariant, record it as absent.** Not
"probably fine" — absent. Every finding in the audit that produced this skill was
in a system whose author would have said it was fine.
---
## 3. Abstraction pressure test
Fill this before adopting a layer, not after:
| Layer | Variability #1 | Variability #2 | Operations removed | Lifecycle and tests added |
|---|---|---|---|---|
| feature plugin | | | | |
| experience / mode definition | | | | |
| semantic input | | | | |
| UI extension points | | | | |
| message bus | | | | |
Defer a layer when it has fewer than two real variants or owners; when direct code
would be smaller and equally safe; when the project will not use dynamic
activation or a packaging boundary; or when the team cannot maintain the
validation and observability the layer requires.
**The arithmetic that decides it:** planned variants versus architectural axes. If
axes exceed variants, the architecture is aspirational. Recipe: AG-01.
---
## 4. The ownership ledger
For every dynamically added resource:
```text
resource
owner
scope process / game instance / world / local player / actor
receipt the handle that proves this owner added it
apply the call that adds
revoke the call that removes exactly this
sharing what happens when two owners want the same resource
rollback what happens if apply fails halfway
```
Red flags, each seen in the audited reference:
- a weak reference presented as ownership;
- a global subsystem that "owns everything";
- cleanup that clears all resources rather than owned ones;
- a handle discarded at the moment it is returned;
- an owner that can die before its resource;
- two owners able to remove the same shared resource.
### The kill-owner test
For each owner, destroy it at the worst moment: during an async load, after apply
but before normal deactivation, after the target actor is destroyed, and during
map travel. Every owned resource must disappear or transfer explicitly.
This is a cheap test that finds expensive defects, and almost nobody runs it.
---
## 5. Lifecycle as a transaction
```text
validate → load → activate 1..N → committed
failure at step K
→ reverse K-1 .. 1
→ failed, with a reason that names the step
```
Three mandatory sequences:
```text
baseline → activate → deactivate → baseline
baseline → activate → fail each step in turn → baseline
baseline → activate → deactivate → reactivate → exactly one copy
```
The third is the one that finds duplicates, and the one development never runs
because development restarts the editor instead. Recipe: AG-03.
---
## 6. Context audit
For every mutable map or subscription, ask which key is **required**: process,
game instance, world handle, activation context, local player, actor, or net role.
A global key is correct only when the behaviour is genuinely global. The
temptation is that a global key is simpler and, with one world and one player,
indistinguishable.
### The required matrix
| Scenario | What it catches |
|---|---|
| multi-world editor session | process-global leaks |
| dedicated server plus remote client | local-broadcast and presentation assumptions |
| listen server | doubled local paths |
| two local players | UI, input and settings context leaks |
| map travel | stale listeners surviving a world |
Recipes: AG-05, AG-06.
---
## 7. The data graph needs a compiler
Generate and validate the path from production roots through identifiers,
references, plugin boundaries, bundles and semantic joins to leaf assets.
Fail the build on: unresolved identifiers or soft references; null required
fields; plugin dependency cycles; role-bundle mismatches; orphan producers or
consumers of a semantic tag; prototype or test paths reachable from a production
root; an input tag with no granted consumer; a UI extension with no point or
contract; an incompatible payload family under a parent channel.
**The crucial property is where it runs.** Editor-only validation is useful
feedback and is not a safety boundary. Recipe: AG-07.
---
## 8. Temporal dependencies
Draw initialization prerequisites as a directed graph — each feature state and
what it requires. Reject: cycles; a prerequisite produced only by an optional or
disabled path; an anonymous next-tick delay; a generic "extension added" event
used where semantic readiness is meant; a blocked transition with no diagnostic.
### Permutation test
Vary activation before and after actor spawn, archetype data before and after
possession, replication arrival order, UI point before and after extension, and
listener before and after event. **Correct systems converge to the same state
from every order.** Systems that pass by luck have one order that works.
---
## 9. Observability is a requirement, not tooling polish
Expose read-only dumps for: the selected mode and its load state; pending assets,
plugins and actions **with reasons**; active feature plugins and who requires
them; per-feature actor readiness and blockers; active input contexts and their
semantic joins; granted capabilities by owner receipt; UI layers, points,
extensions and owners; message channels with listener counts and owners; the
authoritative team identifier and its mirrors; requested versus effective
settings with clamps.
Logs carry correlation fields: world, local player, actor, experience, plugin,
action, channel, owner.
**The acceptance test:** an engineer who did not build the feature diagnoses an
injected failure from logs and dumps alone, without opening assets, within a
bounded time. If that is impossible, the indirection is operationally incomplete
regardless of how clean the code is. Recipe: AG-02.
One thing worth stealing outright: the audited reference ships console variables
that inject artificial delay into mode loading. A reproducible way to exercise
slow-load, timeout and cancellation paths is worth more than it costs, and almost
nobody builds one.
---
## 10. The false-genericity sweep
For every public field or hook implying behaviour — an ordering value, a viewer
identity, an automatic-benchmark decision, a removal function, a profile suffix,
a policy flag:
1. find its consumer;
2. mutate the value in a fixture;
3. assert an observable delta;
4. test both native and visual-script surfaces where exposed.
No consumer or no delta means: implement it, remove it, or mark it explicitly
unsupported. Never let a configurable no-op survive as stable API.
This sweep is worth running as a scheduled activity rather than a review step,
because the shape recurs independently in every subsystem. Recipe: AG-08.
---
## 11. Event, state and command
Classify every interaction:
- **state** — a queryable authoritative model;
- **command** — one accountable handler, with a result and a failure;
- **event** — a past-tense fact with zero or many optional observers.
Red flags: UI reconstructing state from transient messages; a message asking an
unknown listener to perform required work; a local bus assumed to replicate;
presentation treated as authority.
The test that settles it: **if a listener subscribes one second late, must it know
the current value?** Yes means state. No means an event is acceptable.
---
## 12. Version and adoption
Before copying from a reference, classify each element: architectural invariant
(adopt), sample scaffolding (replace), project-specific behaviour (evaluate),
unfinished path (do not claim supported), confirmed defect (fix with a regression
test), prototype content (exclude from production), version-sensitive API (verify
live).
Pin the engine version and changelist, the sample version, plugin versions, and
any serialized schema version. Re-run probes after every upgrade.
**A current documentation page is not evidence about the code in your tree.**
Recipe: AG-10. The full classification is `ue-reference-project-adoption`.
---
## 13. The integration matrix
Do not attempt the full product of configurations. Require this spine:
1. standalone baseline;
2. dedicated server plus remote client;
3. listen server;
4. two local players;
5. feature activates **before** receiver readiness;
6. feature activates **after** receiver readiness;
7. deactivate and reactivate;
8. actor destroyed before feature teardown;
9. late join;
10. map travel or mode change;
11. packaged clean client;
12. engine or plugin upgrade fixture.
Every critical invariant needs at least one test crossing **two** subsystem
boundaries. Unit tests inside one manager cannot express these failures.
Recipe: AG-09.
---
## 14. Triage
**Critical — stop feature work.** Resource ownership or rollback unknown;
authority duplicated; world or player context leaking; production data graph
unvalidated; persistent state reconstructed from messages; client presentation
affecting authority.
**High — fix before runtime switching or shipping.** Initialization cycles or
timing dependencies; missing packaged-asset fallback; native and visual-script
semantics diverging; undocumented ordering dependencies; insufficient
observability; version mismatch.
**Medium — track with a bound.** Abstraction tax; linear scans acceptable at
current data size; editor-only polish gaps; duplicated sample scaffolding.
### The no-go rule
Three unchecked Critical gates mean **stop adding abstraction and features**.
Repair ownership, context and validation first. More indirection on an
unvalidated base multiplies the failure surface rather than organising it.
---
## Provenance
The invariants and gates here are generalized from a full-project audit of Epic's
Lyra Starter Game on Unreal Engine 5.6 — its data-driven layer, feature plugins,
ability system, input, UI, messaging, cosmetics and teams, settings, loading and
streaming — read as source and configuration rather than run.
Every cross-system failure mode in [failure modes](failure-modes.md) was observed
in that project, in a subsystem whose local implementation was correct. That is
the reason this skill exists as a separate document rather than as a section in
each of the others.
Source addresses stay in the research archive that produced this skill. Entry
identifiers resolve back to the audited locations, so any specific claim can be
produced on request.
## Evidence boundary
One project, one engine version, one workspace. The invariants are transferable;
the specific instances are evidence, not guarantees about other versions. Re-run
the detection recipes against your own tree before acting on any specific claim.