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