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:
+739
@@ -0,0 +1,739 @@
|
||||
# Failure modes when adopting a reference project
|
||||
|
||||
Nineteen ways a borrowed subsystem stays quiet while doing nothing.
|
||||
|
||||
They share one property: **each is discovered late by construction.** Every entry
|
||||
here compiles, passes review, produces no warning and no log line. What is absent
|
||||
is a writer, a comparison, a validator or an owner — and absence does not raise.
|
||||
|
||||
Each entry gives you a `Detect` you can run against your own tree today. The
|
||||
recipes are deliberately crude: a grep that returns nothing where reads exist is
|
||||
worth more than a subtle static analysis nobody runs.
|
||||
|
||||
`RA-nn` identifiers are stable and resolve to the audited location in the research
|
||||
archive behind this skill.
|
||||
|
||||
Categories: **C1** defect · **C2** showcase stub · **C3** deliberate sample-scope
|
||||
narrowing. Architecture tax (C4) and foreign vocabulary (C5) are not failure modes
|
||||
and live in [patterns](patterns.md).
|
||||
|
||||
---
|
||||
|
||||
## C1 — Defects
|
||||
|
||||
Fix the line, keep the design. A defect is not evidence against the pattern that
|
||||
contains it.
|
||||
|
||||
### RA-01 - Tag picker filtered to a namespace that does not exist
|
||||
|
||||
**Mechanism.** An editable gameplay-tag property carries a
|
||||
`meta = (Categories = "Some.Root")` filter, and no tag in the project is declared
|
||||
under that root. The real tags live under a different prefix.
|
||||
|
||||
**Why it is silent.** The filter's only job is to narrow an editor dropdown. A
|
||||
filter that matches nothing produces an empty dropdown, which is visually identical
|
||||
to "no tags of this kind have been authored yet". Nothing compiles differently,
|
||||
nothing logs, and the property simply stays unset forever.
|
||||
|
||||
**Why the obvious check misses it.** The filter string is syntactically valid and
|
||||
the property is genuinely declared, read and used — every "is this wired up?" search
|
||||
answers yes. Nobody searches for the *filter value* as a tag prefix, because it
|
||||
reads like a category label rather than a key that has to resolve. In the measured
|
||||
reference this was one of only fourteen places in the entire project where tag input
|
||||
was constrained at all, so the one guardrail present was also the one misconfigured.
|
||||
|
||||
**Symptom.** A designer opens the dropdown, sees nothing, concludes the feature is
|
||||
unimplemented, and leaves the field empty. The system then behaves as if the
|
||||
designer chose "none".
|
||||
|
||||
**Detect.** Extract every filter value and confirm each resolves to declared tags:
|
||||
|
||||
```bash
|
||||
rg -o 'Categories\s*=\s*"([^"]*)"' -r '$1' Source/ Plugins/ | sort -u
|
||||
# then, per root R:
|
||||
rg -c "\"R\." Config/*.ini Source/
|
||||
```
|
||||
|
||||
A root with zero declarations is a dead filter.
|
||||
|
||||
**Guardrail.** A `Categories` filter is a guardrail only if something proves it
|
||||
resolves. Add a startup or CI assertion that every filter value matches at least one
|
||||
declared tag; an empty picker must be an error, not a shrug.
|
||||
|
||||
---
|
||||
|
||||
### RA-02 - A misspelling that works because every side repeats it
|
||||
|
||||
**Mechanism.** A tag or string key is misspelled in its declaration and in every
|
||||
consumer. The string is the join between config, code and platform overrides, so
|
||||
identical errors on both sides join correctly.
|
||||
|
||||
**Why it is silent.** Correctness for a string join means *all sides agree*, not
|
||||
*the string is a word*. Six identical misspellings across four files function
|
||||
exactly as six correct ones would. There is no side that could disagree.
|
||||
|
||||
**Why the obvious check misses it.** Searching for the correct spelling returns
|
||||
nothing, and an empty result reads as "this feature is not implemented here" rather
|
||||
than "it is spelled differently". The compiler never sees the literal as a name, and
|
||||
spell-checkers do not run over identifiers. The bug only becomes visible at the
|
||||
moment someone *fixes* it at one site.
|
||||
|
||||
**Symptom.** Nothing at all — until a cleanup pass corrects the spelling in one
|
||||
file. The join then breaks with no error, and the regression is attributed to
|
||||
whatever else shipped that week.
|
||||
|
||||
**Detect.** Find tag literals repeated as raw strings instead of referenced through
|
||||
one constant:
|
||||
|
||||
```bash
|
||||
rg -oN '"[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*){2,}"' Source/ Config/ \
|
||||
| sort | uniq -c | sort -rn | head -40
|
||||
```
|
||||
|
||||
Any literal appearing more than once is a join with no single source of truth.
|
||||
|
||||
**Guardrail.** One declaration site per tag; every other site references the
|
||||
constant. Then a rename is a compile error instead of a silent disconnection.
|
||||
|
||||
---
|
||||
|
||||
### RA-03 - A loop over a container that has no producer
|
||||
|
||||
**Mechanism.** A local container is declared next to a TODO explaining how it will
|
||||
one day be filled, then iterated immediately. Nothing ever fills it.
|
||||
|
||||
**Why it is silent.** Zero iterations is a legal, successful outcome. The function
|
||||
returns normally and reports success, because doing nothing to an empty set is
|
||||
indistinguishable from doing the work correctly.
|
||||
|
||||
**Why the obvious check misses it.** The code reads as a complete mechanism:
|
||||
declaration, loop, and a body that does real work. Review attention goes to whether
|
||||
the body is correct. The missing piece is one level up — the container has a
|
||||
consumer and no producer, and reviewers check consumers.
|
||||
|
||||
**Symptom.** A documented behaviour ("these entries are always loaded") reports zero
|
||||
at runtime, and is diagnosed as a content or configuration problem for days.
|
||||
|
||||
**Detect.** For every container that is iterated, look for a writer:
|
||||
|
||||
```bash
|
||||
rg -n "for\s*\(.*:\s*(\w+)\)" -r '$1' Source/ | sort -u > /tmp/iterated
|
||||
# per name N:
|
||||
rg -n "\bN\b\s*\.\s*(Add|Append|Emplace|Push|Insert|Reserve)" Source/
|
||||
```
|
||||
|
||||
Reads without writes mean the loop is decoration.
|
||||
|
||||
**Guardrail.** Do not ship a loop over a container that has no producer in the same
|
||||
translation unit or an obvious injection point. If the producer is future work, the
|
||||
loop is future work too.
|
||||
|
||||
---
|
||||
|
||||
### RA-04 - Progress arithmetic with inverted operands
|
||||
|
||||
**Mechanism.** A startup progress fraction computes each sub-step's contribution
|
||||
with the operands the wrong way round, so the reported value does not advance while
|
||||
a single long step runs.
|
||||
|
||||
**Why it is silent.** The result stays inside the valid range and still increases
|
||||
across step boundaries. It is a wrong number, not an invalid one, so no assertion
|
||||
and no clamp fires.
|
||||
|
||||
**Why the obvious check misses it.** On a developer machine with a warm cache the
|
||||
whole sequence finishes in under a second, and nobody watches one step long enough
|
||||
to see it stall. The code is exercised on every launch and observed on none.
|
||||
|
||||
**Symptom.** On a cold shader cache or a slow disk the bar freezes for a long time
|
||||
and players report a hang. The engineering response is to look for a deadlock,
|
||||
because the bar is trusted.
|
||||
|
||||
**Detect.** Two options. Force the slow path with whatever artificial-delay cvar the
|
||||
loading system provides and watch whether the value moves within a step. Or unit-test
|
||||
the progress function directly: feed it a fixed step count and assert the value is
|
||||
strictly increasing at every sub-step, not just at step boundaries.
|
||||
|
||||
**Guardrail.** Progress arithmetic gets a test. It is the canonical example of code
|
||||
that runs constantly and is observed only under conditions developers do not have.
|
||||
|
||||
---
|
||||
|
||||
### RA-05 - Listener removed using the wrong channel key
|
||||
|
||||
**Mechanism.** A tag-addressed pub/sub subsystem walks parent tags when
|
||||
broadcasting. On finding an expired listener it removes the handle using the
|
||||
*original broadcast channel* rather than the ancestor tag currently being visited.
|
||||
Handle identifiers are unique only within a channel.
|
||||
|
||||
**Why it is silent.** Both outcomes are legal operations on a map. Either the key is
|
||||
absent and removal is a no-op — the stale listener survives in the parent bucket — or
|
||||
a different listener happens to hold the same channel-local id and is removed from
|
||||
the wrong bucket. Neither path raises.
|
||||
|
||||
**Why the obvious check misses it.** The removal call is present, correctly typed,
|
||||
and looks right. The defect is in *which key* is passed, and both candidate keys are
|
||||
in scope, same-typed, and similarly named. Type checking cannot separate them, and
|
||||
review reads the statement as "remove the expired listener".
|
||||
|
||||
**Symptom.** A listener that was unregistered keeps receiving messages, or an
|
||||
unrelated listener silently stops receiving them. The failure surfaces in whichever
|
||||
system owned the collateral listener, arbitrarily far from the subsystem at fault.
|
||||
|
||||
**Detect.** Inside any parent-tag or hierarchy traversal, confirm every mutation
|
||||
keys on the loop cursor rather than the function parameter:
|
||||
|
||||
```bash
|
||||
rg -n -B12 "(Unregister|Remove\w*Listener|RemoveAt)" Source/ \
|
||||
| rg -n "for\s*\(|ParentTag|Ancestor"
|
||||
```
|
||||
|
||||
Then read each hit and check the key.
|
||||
|
||||
**Guardrail.** Make listener identity an explicit pair type — channel plus id — so
|
||||
passing the wrong channel fails to compile instead of silently mis-keying.
|
||||
|
||||
---
|
||||
|
||||
## C2 — Showcase stubs
|
||||
|
||||
The defining property: **the declaration and the consumer both exist; the writer
|
||||
does not** — or the consumer is a literal. Neither a compiler warning nor an
|
||||
"unused symbol" search finds these.
|
||||
|
||||
### RA-06 - Editable tunable with a live reader and no writer
|
||||
|
||||
**Mechanism.** A designer-facing cooldown is compared against a "last event"
|
||||
timestamp field. The timestamp is declared and read, and is never assigned anywhere
|
||||
in the codebase.
|
||||
|
||||
**Why it is silent.** Time elapsed since an unset timestamp is time since world
|
||||
start, which always exceeds any sane threshold. The comparison is permanently true,
|
||||
so the gate the setting was meant to impose never rejects anything. Permanently
|
||||
allowing is a valid runtime state, so nothing errors.
|
||||
|
||||
**Why the obvious check misses it.** The field is read twice and participates in a
|
||||
comparison, so every "is this used?" heuristic — grep, IDE find-usages, unused-symbol
|
||||
warnings — answers yes. What is missing is the **writer**, not the reader, and no
|
||||
default tool asks that question. This is the highest-yield check in the whole skill
|
||||
precisely because it inverts the usual direction of the search.
|
||||
|
||||
**Symptom.** A designer tunes the value for days, reports it has no effect, and is
|
||||
told to check their data. The setting is then either abandoned or "fixed" by
|
||||
changing something else that happens to correlate.
|
||||
|
||||
**Detect.** For every `EditAnywhere` or `EditDefaultsOnly` numeric property and for
|
||||
every timestamp it is compared against, search for an assignment rather than a
|
||||
mention:
|
||||
|
||||
```bash
|
||||
rg -n "\bLastFireTime\b" Source/ # reads: several
|
||||
rg -n "\bLastFireTime\b\s*(=|\+=|-=)" Source/ # one hit - and it is the declaration
|
||||
```
|
||||
|
||||
The second search does **not** come back empty, and that is the trap. A member
|
||||
declared with an initializer (`double LastFireTime = 0.0;`) matches every
|
||||
assignment pattern you can write, so the naive search reports a writer that does
|
||||
not exist. Subtract the declaration before concluding:
|
||||
|
||||
```bash
|
||||
rg -n "\bLastFireTime\b\s*(=|\+=|-=)" Source/ \
|
||||
| rg -v "\b(bool|u?int\d+|float|double|F[A-Z]\w+|T\w+<)\s+LastFireTime\b"
|
||||
```
|
||||
|
||||
Empty after that subtraction, with live reads elsewhere, is a stub. Verified on
|
||||
the reference: the unsubtracted search returns one line, the subtracted search
|
||||
returns none.
|
||||
|
||||
**Guardrail.** Never ship an editor-exposed property whose value has no writer.
|
||||
Every tunable needs one owning assignment site and one test that moves the value and
|
||||
asserts an observable delta.
|
||||
|
||||
---
|
||||
|
||||
### RA-07 - Ordering field that is stored, copied and never compared
|
||||
|
||||
**Mechanism.** A `Priority` integer is accepted by eight registration overloads,
|
||||
stored on the entry, copied into the request struct, and never appears in a sort, a
|
||||
predicate or a comparison anywhere.
|
||||
|
||||
**Why it is silent.** The field has a defined value at all times and travels
|
||||
correctly through the entire API. Ordering still happens — it is just registration
|
||||
order. A plausible order is produced on every run, so nothing looks wrong.
|
||||
|
||||
**Why the obvious check misses it.** The value is written, read, copied and passed
|
||||
across a large public surface. Usage counts are high. Any review that asks "is
|
||||
`Priority` used?" finds a dozen sites and stops. The right question is narrower: does
|
||||
it appear inside a `Sort`, `<`, `>` or predicate? That question is not one anyone
|
||||
thinks to ask about a field that is obviously plumbed.
|
||||
|
||||
**Symptom.** Widget or handler order changes between runs and between machines. In a
|
||||
plugin-based project, registration order is feature activation order, so the symptom
|
||||
reads as a race condition and gets chased in the wrong subsystem.
|
||||
|
||||
**Detect.** Search for the comparison, not the plumbing:
|
||||
|
||||
```bash
|
||||
F='(Priority|Order|Weight|Rank|SortKey)'
|
||||
rg -n "\b$F\b" Source/ Plugins/ | wc -l # plumbing: many
|
||||
rg -n "(Sort|StableSort|Algo::)" Source/ Plugins/ | rg "\b$F\b" # ordering: ?
|
||||
rg -n "\b$F\s*[<>]=?\s*\w|\w\s*[<>]=?\s*\w*$F\b" Source/ Plugins/ \
|
||||
| rg -v -- "->" # comparisons: ?
|
||||
```
|
||||
|
||||
The `-v -- "->"` matters. Without it, every `Entry->Priority = X` is counted as a
|
||||
comparison because the arrow contains `>`, and a field that is only ever assigned
|
||||
looks like a field that is ordered. On the measured reference the naive form
|
||||
reported two "comparisons"; both were assignments.
|
||||
|
||||
A large first number with empty second and third lines is an ordering field that
|
||||
does not order.
|
||||
|
||||
**Guardrail.** An ordering field ships with the comparator that consumes it, in the
|
||||
same change. If ordering is not implemented yet, do not expose the field.
|
||||
|
||||
---
|
||||
|
||||
### RA-08 - Tag-driven visibility with the check hardcoded off
|
||||
|
||||
**Mechanism.** A widget base class exposes an editable tag container for
|
||||
"hide when these tags are present". Both consumer sites read
|
||||
`const bool bHasHiddenTags = false;` with the real expression parked in a comment
|
||||
next to a TODO. The listener registration that would supply the tags is also a
|
||||
comment.
|
||||
|
||||
**Why it is silent.** `false` is a legitimate value: it means "no hiding tags are
|
||||
active right now". The widget is visible, which is a normal state, so no test and no
|
||||
reviewer can tell the difference between "correctly not hidden" and "never able to
|
||||
hide".
|
||||
|
||||
**Why the obvious check misses it.** The TODO admits *one* missing piece and thereby
|
||||
misdirects. The obvious fix — implement `bHasHiddenTags` — does not restore the
|
||||
intended behaviour, because a nearby visibility setter overwrites the
|
||||
designer-authored shown and hidden visibility values on its first call, which arrives
|
||||
during construction. A stub can be deeper than its own TODO says it is, and a reviewer
|
||||
who trusts the TODO stops one layer too early.
|
||||
|
||||
**Symptom.** A designer fills the tag container, observes nothing, and the class is
|
||||
recorded in team lore as "the tag hiding does not work" without anyone establishing
|
||||
why.
|
||||
|
||||
**Detect.** Find neutral literals feeding decision branches:
|
||||
|
||||
```bash
|
||||
rg -n "const bool b\w+ = (false|true);\s*//" Source/ Plugins/
|
||||
```
|
||||
|
||||
For each hit, check whether the property that should feed it is `EditAnywhere`. Then
|
||||
check that nothing else overwrites the same state before the branch is reached — the
|
||||
second step is the one people skip.
|
||||
|
||||
**Guardrail.** An editable property whose consumer is a literal constant must not
|
||||
ship. When you do implement one, re-derive the whole path rather than the single line
|
||||
the TODO names.
|
||||
|
||||
---
|
||||
|
||||
### RA-09 - Parameters threaded through for a subsystem that was never built
|
||||
|
||||
**Mechanism.** A `bIsSimulated` flag appears in three method signatures and is
|
||||
passed through six call sites. Its sole caller passes a literal `false`. A companion
|
||||
"replace this hit" method exists and is never called; the boolean recording its result
|
||||
is only ever read.
|
||||
|
||||
**Why it is silent.** A flag that is always `false` produces one consistent code
|
||||
path, which is the path everything is tested on. A method with no callers has no
|
||||
behaviour to be wrong. Both are inert rather than broken.
|
||||
|
||||
**Why the obvious check misses it.** The threading through multiple layers is exactly
|
||||
what a real, wired-up feature looks like — that shape is itself the disguise. Usage
|
||||
searches return many hits. Only two narrower questions expose it: is the parameter
|
||||
ever branched on, and does the sole call site pass a variable or a literal?
|
||||
|
||||
**Symptom.** An engineer reads the signatures, concludes server-side hit rewind
|
||||
exists, and budgets zero for it. The gap is discovered when the game meets real
|
||||
latency, at which point the structure of abilities, tracing and damage is already
|
||||
committed.
|
||||
|
||||
**Detect.** Find parameters that are carried but never branched on:
|
||||
|
||||
```bash
|
||||
rg -n "\bbIsSimulated\b" Source/ # many hits: plumbed
|
||||
rg -n "if\s*\(\s*!?bIsSimulated" Source/ # none: never decides anything
|
||||
rg -n "\w+\(.*\bfalse\b.*\)" Source/ | rg "bIsSimulated|Simulated"
|
||||
```
|
||||
|
||||
Also list public functions with zero callers: `rg -n "FunctionName"` returning only
|
||||
the declaration and definition.
|
||||
|
||||
**Guardrail.** Do not ship attachment points for a subsystem that does not exist.
|
||||
An unused parameter is a claim about the architecture; if the claim is false, delete
|
||||
the parameter or write the subsystem.
|
||||
|
||||
---
|
||||
|
||||
### RA-10 - Field silently dropped in both directions of a conversion
|
||||
|
||||
**Mechanism.** Helper functions convert between a gameplay message and cue
|
||||
parameters. A context tag container present on both sides is not copied in either
|
||||
direction. Both sites carry a TODO.
|
||||
|
||||
**Why it is silent.** The conversion succeeds and produces a fully valid object. The
|
||||
dropped field simply arrives empty, and empty is the same value a caller who did not
|
||||
set it would produce. There is no partial-failure signal because nothing failed.
|
||||
|
||||
**Why the obvious check misses it.** Round-tripping is the natural test, and a
|
||||
round trip through both directions loses the same field consistently — so a
|
||||
comparison of "before" against "after" on the fields anyone thought to compare
|
||||
passes. Structural equality is the test that would catch it, and structural equality
|
||||
is what nobody writes for conversion helpers.
|
||||
|
||||
**Symptom.** Effects lose their contextual tags when routed through the conversion,
|
||||
so downstream selection by context silently picks the default variant.
|
||||
|
||||
**Detect.** For every conversion helper, compare field counts on both sides:
|
||||
|
||||
```bash
|
||||
rg -n "^\s*(FGameplayTagContainer|F\w+|TArray<\w+>)\s+\w+;" Source/**/Struct.h
|
||||
rg -n -A30 "ToOtherForm|FromOtherForm|Convert\w+" Source/ | rg "\bField\b"
|
||||
```
|
||||
|
||||
Any field declared on both types and mentioned in neither direction is dropped.
|
||||
|
||||
**Guardrail.** Conversion helpers get a structural round-trip test that enumerates
|
||||
fields by reflection rather than by hand. A hand-written comparison tests the fields
|
||||
the author remembered, which is the same set they remembered to copy.
|
||||
|
||||
---
|
||||
|
||||
### RA-11 - Replicated structure with no callers and an empty removal hook
|
||||
|
||||
**Mechanism.** A replicated fast-array serializer type is fully declared, with the
|
||||
removal callback implemented as an empty body. Nothing in the codebase constructs or
|
||||
uses it.
|
||||
|
||||
**Why it is silent.** Unused replicated types cost nothing at runtime and generate no
|
||||
warning. The empty callback is a valid override — many are legitimately empty.
|
||||
|
||||
**Why the obvious check misses it.** The type is complete and idiomatic, so it reads
|
||||
as infrastructure that some subsystem depends on. Deleting it feels risky, so it
|
||||
survives every cleanup. Meanwhile a reader treats its existence as evidence that
|
||||
replicated messaging is solved.
|
||||
|
||||
**Symptom.** A team builds on top of it, assuming it works, and discovers on the
|
||||
first multi-client test that no path ever populated it.
|
||||
|
||||
**Detect.** Find declared-but-unconstructed types:
|
||||
|
||||
```bash
|
||||
rg -ln "struct F\w+ : public FFastArraySerializer" Source/ Plugins/
|
||||
# per type T:
|
||||
rg -n "\bT\b" Source/ Plugins/ | rg -v "\.h:|struct|USTRUCT|template"
|
||||
```
|
||||
|
||||
Hits only in the header mean the type has no user.
|
||||
|
||||
**Guardrail.** Replication infrastructure ships with at least one caller and one
|
||||
test that observes a replicated change. Untested replication is not infrastructure,
|
||||
it is a plan.
|
||||
|
||||
---
|
||||
|
||||
## C3 — Deliberate sample-scope narrowings
|
||||
|
||||
Nothing here is broken. The reference is internally consistent with every one of
|
||||
these. They become expensive only when the sample becomes the base of a product.
|
||||
|
||||
### RA-12 - No lag compensation, and the whole hit chain follows from it
|
||||
|
||||
**Mechanism.** The project contains no custom saved-move, server-move or compressed
|
||||
flags implementation, and therefore no historical positions on the server. Local
|
||||
targeting early-returns when the pawn is not locally controlled, so the server never
|
||||
traces. A `bIsTargetDataValid` local is initialised to `true` and consumed as if it
|
||||
were a validation result. Damage falloff is computed from the client's supplied trace
|
||||
start, and the material multiplier from the client's supplied physical material.
|
||||
|
||||
**Why it is silent.** Every step is the only available behaviour given the missing
|
||||
subsystem. The server cannot validate positions it never recorded, so trusting the
|
||||
client is not a slip — it is the sole option. Correct-looking code all the way down,
|
||||
with the gap one level below the code.
|
||||
|
||||
**Why the obvious check misses it.** Reviewing the damage path finds an authority
|
||||
check and a validity boolean, which together look like a validation layer. The
|
||||
boolean is a literal and the authority check answers a different question ("may this
|
||||
instigator damage this target?") than the one that matters ("did this shot happen?").
|
||||
Reading for the presence of checks finds checks; reading for what the adversary can
|
||||
assert finds nothing stopping them.
|
||||
|
||||
**Symptom.** In production, clients report impossible hits and the diagnosis becomes
|
||||
"someone forgot a validation call" — which sends engineers to patch individual sites
|
||||
instead of pricing the missing subsystem.
|
||||
|
||||
**Detect.** Ask what the adversary supplies, then trace each such value:
|
||||
|
||||
```bash
|
||||
rg -n "class \w+ : public FSavedMove_Character|ServerMove|CompressedFlags" Source/
|
||||
rg -n "const bool b\w*Valid\w* = true" Source/
|
||||
rg -n "TargetData|HitResult" Source/ | rg -i "damage|falloff|physmat"
|
||||
```
|
||||
|
||||
No saved-move type plus client-supplied geometry in the damage math means full
|
||||
client trust.
|
||||
|
||||
**Guardrail.** Choose a hit-registration model *before* borrowing a weapon stack:
|
||||
server-authoritative with rewind, client-claim with plausibility bounds, or explicit
|
||||
full trust. Write the choice down. Each has a different cost and a different cheat
|
||||
surface, and the choice dictates structure you cannot cheaply change later.
|
||||
|
||||
---
|
||||
|
||||
### RA-13 - Replication graph implemented, configured, and shipped disabled
|
||||
|
||||
**Mechanism.** A replication graph implementation and around a dozen tuning cvars
|
||||
exist. Project config disables it and the class routing table is essentially empty,
|
||||
with the few entries present marked not-routed.
|
||||
|
||||
**Why it is silent.** The default replication path works fine at sample scale.
|
||||
Disabled infrastructure produces no error and no measurable difference until player
|
||||
counts and actor counts grow.
|
||||
|
||||
**Why the obvious check misses it.** The presence of the implementation, the cvars
|
||||
and the config section reads as "this is configured". Nobody diffs the routing table
|
||||
against the actor classes that actually exist, because a populated-looking config
|
||||
section satisfies the eye.
|
||||
|
||||
**Symptom.** Bandwidth and server CPU limits are discovered in beta, at the point
|
||||
where the actor class layout is fixed and re-routing is a large change.
|
||||
|
||||
**Detect.** Compare routing entries against replicated classes:
|
||||
|
||||
```bash
|
||||
rg -n "bDisableReplicationGraph|ClassNodeMapping" Config/
|
||||
rg -c "GetLifetimeReplicatedProps" Source/ Plugins/
|
||||
```
|
||||
|
||||
A handful of routing entries against dozens of replicated classes means the graph
|
||||
has never carried traffic.
|
||||
|
||||
**Guardrail.** Copying a disabled subsystem's configuration copies untested
|
||||
configuration. Either enable it and measure under load, or delete the config and
|
||||
record that you owe the work.
|
||||
|
||||
---
|
||||
|
||||
### RA-14 - Preload machinery made unreachable by a load-mode constant
|
||||
|
||||
**Mechanism.** A cue manager supports delayed loading with garbage-collect and
|
||||
map-load hooks. A load-mode constant is set to "load upfront", which short-circuits
|
||||
every switch site — including the one that installs the three delegates.
|
||||
|
||||
**Why it is silent.** Loading everything upfront is correct behaviour. The
|
||||
machinery is not broken; it is bypassed. Memory is higher and nothing reports it,
|
||||
because nothing is measuring against a budget.
|
||||
|
||||
**Why the obvious check misses it.** Searching for the delegates finds them bound in
|
||||
source, so the wiring appears present. The early return that makes the binding
|
||||
unreachable is in a different function, guarded by a constant that reads like a
|
||||
development convenience. The project's own diagnostic command reports zeros, which is
|
||||
then read as "no cues are preloaded yet" rather than "this counter is dead".
|
||||
|
||||
**Symptom.** A team diagnosing memory or hitching concludes the engine's preload
|
||||
system is broken and descends into engine code, when the sample simply opted out.
|
||||
|
||||
**Detect.** Find switch sites gated by a constant, and confirm delegate binding is
|
||||
reachable:
|
||||
|
||||
```bash
|
||||
rg -n "LoadMode|ELoadMode|EEditorLoadMode" Source/ Plugins/
|
||||
rg -n -B6 "AddUObject|AddRaw|BindUObject" Source/ | rg "return;|LoadUpfront"
|
||||
```
|
||||
|
||||
**Guardrail.** Distinguish "the mechanism is broken" from "the sample configured it
|
||||
off" before you spend a day in engine code. Record which subsystems the reference
|
||||
bypasses; that list is part of what you are adopting.
|
||||
|
||||
---
|
||||
|
||||
### RA-15 - Public virtual state writers with no authority check
|
||||
|
||||
**Mechanism.** Death-state transitions are written by two public virtual methods on
|
||||
a health component, neither of which checks for network authority.
|
||||
|
||||
**Why it is silent.** Single-process play-in-editor never separates authority from
|
||||
simulation, so the code path is exercised constantly and always on the authority. The
|
||||
missing check has no observable consequence in the environment where the sample runs.
|
||||
|
||||
**Why the obvious check misses it.** Authority checks are usually reviewed at the
|
||||
RPC boundary, and these methods are not RPCs — they are ordinary virtual functions
|
||||
that a caller in the right context invokes correctly. Being public and virtual is
|
||||
what makes it dangerous: the class invites overriding and calling from anywhere, and
|
||||
the invitation carries no precondition.
|
||||
|
||||
**Symptom.** A client-side subclass or a Blueprint calls the method during
|
||||
prediction, the client's death state diverges from the server's, and the resulting
|
||||
desync is chased as a replication bug.
|
||||
|
||||
**Detect.** List state writers and check each for an authority guard:
|
||||
|
||||
```bash
|
||||
rg -n "void (Start|Finish|Set)\w*(Death|State|Team)\w*\(" Source/
|
||||
rg -n -A6 "void (Start|Finish|Set)\w*(Death|State|Team)\w*\(" Source/ | rg -c "HasAuthority"
|
||||
```
|
||||
|
||||
A count below the number of writers names the unguarded ones.
|
||||
|
||||
**Guardrail.** Every writer of replicated state either checks authority or is
|
||||
private with a single guarded caller. Public plus virtual plus unguarded is an
|
||||
invitation.
|
||||
|
||||
---
|
||||
|
||||
### RA-16 - Faction query fails open on invalid input
|
||||
|
||||
**Mechanism.** A team comparison returns an "invalid argument" result for unknown
|
||||
membership, and the damage path treats anything that is not an explicit "same team"
|
||||
as damageable. The site carries a TODO calling itself temporary.
|
||||
|
||||
**Why it is silent.** Unknown membership does not occur in a sample where every pawn
|
||||
is assigned a team at spawn. The fail-open branch is never taken, so it is never
|
||||
observed to be wrong.
|
||||
|
||||
**Why the obvious check misses it.** The function returns a three-valued result,
|
||||
which reads as careful design. The defect is in the *consumer's* collapse of three
|
||||
values into two, in a different file. Reviewing the query finds correct code;
|
||||
reviewing the damage path finds a plausible boolean.
|
||||
|
||||
**Symptom.** Neutral, spectating or mid-transition actors take or deal damage. The
|
||||
bug appears only in modes the sample does not have, which is to say, in yours.
|
||||
|
||||
**Detect.** Find three-valued results collapsed to booleans:
|
||||
|
||||
```bash
|
||||
rg -n "Invalid|Unknown|Indeterminate" Source/ | rg -i "team|faction|relation"
|
||||
rg -n -B4 -A4 "CanCauseDamage|CanDamage|IsHostile" Source/
|
||||
```
|
||||
|
||||
Read the branch: does the invalid case land with "allowed" or "denied"?
|
||||
|
||||
**Guardrail.** Unknown must be denied, never allowed, on any decision that costs
|
||||
something. If denial is not acceptable, the unknown state itself is the bug.
|
||||
|
||||
---
|
||||
|
||||
### RA-17 - No tag or asset redirects declared at all
|
||||
|
||||
**Mechanism.** Config declares zero gameplay tag redirects. Every tag name is a hard
|
||||
string with no migration path.
|
||||
|
||||
**Why it is silent.** A project that has never renamed a tag needs no redirects. The
|
||||
absence is invisible right up to the first rename, and samples do not rename after
|
||||
release.
|
||||
|
||||
**Why the obvious check misses it.** This is an absence with no consumer to inspect
|
||||
— there is no line of code to review. It is only visible if you go looking for a
|
||||
capability you have not needed yet, which is exactly the thing nobody does during
|
||||
adoption.
|
||||
|
||||
**Symptom.** The first tag rename after content exists breaks every asset that
|
||||
referenced it. Since references live in binary assets, the breakage is silent data
|
||||
loss rather than a compile error, discovered per-asset over weeks.
|
||||
|
||||
**Detect.**
|
||||
|
||||
```bash
|
||||
rg -c "GameplayTagRedirects|\+ActiveGameRedirects|ClassRedirects" Config/
|
||||
```
|
||||
|
||||
Zero, in a project that has shipped content, means renames have not yet happened —
|
||||
not that they are safe.
|
||||
|
||||
**Guardrail.** Establish redirect discipline before the first rename, not after.
|
||||
Add a CI check that a removed tag name has a corresponding redirect entry.
|
||||
|
||||
---
|
||||
|
||||
### RA-18 - Server RPC declared without validation
|
||||
|
||||
**Mechanism.** A quick-bar style component exposes a `Server, Reliable,
|
||||
BlueprintCallable` function with no `WithValidation`, accepting an index from the
|
||||
client.
|
||||
|
||||
**Why it is silent.** Well-behaved clients send valid indices, and the sample only
|
||||
ever has well-behaved clients. Out-of-range access either clamps harmlessly or hits a
|
||||
path no test covers.
|
||||
|
||||
**Why the obvious check misses it.** Nearby functions are marked
|
||||
`BlueprintAuthorityOnly`, which reads as an access restriction and satisfies a
|
||||
reviewer scanning for guards. That specifier constrains Blueprint execution context
|
||||
only — it does not validate parameters, and it does not constrain C++ callers at all.
|
||||
A guard that answers a different question is worse than no guard, because it stops
|
||||
the search.
|
||||
|
||||
**Symptom.** A modified client sends an arbitrary index. Depending on the container,
|
||||
this is a crash, a read of unrelated memory, or equipping something the player never
|
||||
earned.
|
||||
|
||||
**Detect.**
|
||||
|
||||
```bash
|
||||
rg -n "UFUNCTION\([^)]*\bServer\b[^)]*\)" Source/ Plugins/ | rg -v "WithValidation"
|
||||
```
|
||||
|
||||
Every hit takes client-controlled input on trust.
|
||||
|
||||
**Guardrail.** Every server RPC validates its parameters, including range and
|
||||
ownership, and `BlueprintAuthorityOnly` is never counted as validation.
|
||||
|
||||
---
|
||||
|
||||
### RA-19 - Readiness gate admits simulated proxies with no controller
|
||||
|
||||
**Mechanism.** An initialisation-state transition treats "has a controller" as a
|
||||
precondition, but admits simulated proxies that have none, so readiness is reached
|
||||
on different grounds depending on net role.
|
||||
|
||||
**Why it is silent.** Both branches produce "ready", and ready is the state everything
|
||||
downstream waits for. No consumer asks *why* readiness was granted.
|
||||
|
||||
**Why the obvious check misses it.** The gate is short and reads as a careful
|
||||
role-aware special case — which is what it is. The problem is that "ready" then means
|
||||
two different things, and the divergence lives in every consumer rather than in the
|
||||
gate. Reviewing the gate finds nothing wrong.
|
||||
|
||||
**Symptom.** Systems that assume a controller exists after readiness crash or
|
||||
no-op on simulated proxies. The failure appears only with a remote client, so it
|
||||
survives all local testing.
|
||||
|
||||
**Detect.** Find role-dependent readiness and enumerate its consumers:
|
||||
|
||||
```bash
|
||||
rg -n -B4 -A10 "CanChangeInitState|HasReachedInitState" Source/ | rg "IsLocallyControlled|GetController|ROLE_SimulatedProxy"
|
||||
rg -n "HasReachedInitState|GameplayReady" Source/ | wc -l
|
||||
```
|
||||
|
||||
**Guardrail.** One readiness state means one set of guarantees. If proxies reach it
|
||||
by a different route, they need a different state name, so consumers must choose.
|
||||
|
||||
---
|
||||
|
||||
## How to use this list
|
||||
|
||||
Do not read it as a defect report about someone else's project. Read it as a list of
|
||||
**question shapes**:
|
||||
|
||||
1. Which of my editable properties has no writer?
|
||||
2. Which of my ordering fields never reaches a comparison?
|
||||
3. Which of my decision branches reads a literal?
|
||||
4. Which of my parameters is threaded but never branched on?
|
||||
5. Which of my unknown-input paths fails open?
|
||||
6. Which of my public state writers has no authority check?
|
||||
7. Which of my capabilities exists only as configuration that is switched off?
|
||||
|
||||
Each has a one-line `Detect` above. Run all seven against your own tree before you
|
||||
run any of them against the reference.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
Every entry was read in source in one reference project on one engine version. They
|
||||
are worked examples of the classification, not a defect list to carry into other
|
||||
engine versions or other samples. An entry that does not reproduce under its own
|
||||
`Detect` in your tree does not apply to you.
|
||||
@@ -0,0 +1,294 @@
|
||||
# Adoption patterns: a worked classification
|
||||
|
||||
This is the C1-C5 classification from `SKILL.md` applied end to end against one
|
||||
real reference project, plus the tax analysis and vocabulary hygiene that follow
|
||||
from it.
|
||||
|
||||
Individual silent failures are not repeated here. They live in
|
||||
[failure modes](failure-modes.md), one entry each, with a detection recipe you
|
||||
can run against your own tree. This file is about *how the categories behave* and
|
||||
what each one costs, so that when you meet a new element you can place it.
|
||||
|
||||
---
|
||||
|
||||
## 1. What each category feels like from the inside
|
||||
|
||||
The categories are not severity levels. They are answers to a different question:
|
||||
**what does the author of the reference believe about this element?**
|
||||
|
||||
| | Author's belief | Your evidence | Your move |
|
||||
|---|---|---|---|
|
||||
| **C1 Defect** | "this works" | it demonstrably does not | fix the line, keep the design |
|
||||
| **C2 Showcase stub** | "this is not finished" | reads as finished | implement or delete the field |
|
||||
| **C3 Scope narrowing** | "a sample does not need this" | nothing is wrong at all | budget it or accept the risk in writing |
|
||||
| **C4 Architecture tax** | "this is worth it for us" | correct, working, expensive | adopt by need, not completeness |
|
||||
| **C5 Foreign legacy** | nobody believes anything | a name from another product | rename on entry |
|
||||
|
||||
The dangerous boundary is C2 against C3, because from the outside they look
|
||||
identical: a mechanism that produces a neutral result forever. The distinction is
|
||||
whether the author intended to come back. It changes nothing about your cost, and
|
||||
everything about how you talk to your team: a C2 is a hole in the reference, a C3
|
||||
is a hole in your understanding of what you bought.
|
||||
|
||||
---
|
||||
|
||||
## 2. C1 - Defects
|
||||
|
||||
A defect in a reference project is the cheapest category and the least
|
||||
interesting. Fix the line, keep the surrounding design, and do not let it
|
||||
discredit the pattern that contains it.
|
||||
|
||||
Three shapes recur often enough to be worth naming:
|
||||
|
||||
**The filter that filters nothing.** A field constrains designer input to a
|
||||
namespace, and the namespace does not exist in this project. The picker comes up
|
||||
empty, which reads as "no matching entries yet" rather than "this constraint is
|
||||
misconfigured". Where such filters are rare in a codebase, each one is load-bearing
|
||||
and worth checking individually.
|
||||
|
||||
**The symmetric typo.** A misspelled identifier used as a string join across
|
||||
config, code and platform overrides works perfectly, because every side misspells
|
||||
it identically. It is only discovered by someone fixing the spelling in one place.
|
||||
The lesson is not "check spelling"; it is that when a string literal is the join,
|
||||
correctness means *all sides agree*, and the way to get that is one declaration
|
||||
site referenced everywhere else.
|
||||
|
||||
**The loop over an empty container.** A local collection is declared next to a
|
||||
TODO, never populated, and then iterated. The loop reads as a mechanism. It is a
|
||||
placeholder for one.
|
||||
|
||||
None of these survives a careful read of the consumer. They survive a read of the
|
||||
declaration, which is why they get copied.
|
||||
|
||||
---
|
||||
|
||||
## 3. C2 - Showcase stubs
|
||||
|
||||
The defining property is precise, and worth memorizing:
|
||||
|
||||
> The declaration exists. The consumer exists. The **writer** does not - or the
|
||||
> consumer is a literal.
|
||||
|
||||
Neither a compiler warning nor an "unused symbol" search finds these, because the
|
||||
symbol is used. Reading the code convinces you it is wired up. This is why the
|
||||
highest-yield check in the whole skill is searching for assignment rather than
|
||||
mention.
|
||||
|
||||
Sub-shapes, each with an entry in the failure-mode index:
|
||||
|
||||
- a tunable compared against a timestamp that is never assigned (RA-06);
|
||||
- an ordering field stored, copied through the entire API surface, and never
|
||||
compared (RA-07);
|
||||
- an editable tag container whose consumer is a hardcoded `false` (RA-08);
|
||||
- a parameter threaded through several call layers, whose only call site passes a
|
||||
literal, waiting for a subsystem that was never written (RA-09);
|
||||
- a field dropped silently in both directions of a conversion helper (RA-10);
|
||||
- a replicated structure with no callers at all (RA-11).
|
||||
|
||||
### The trap inside the trap
|
||||
|
||||
A stub can be deeper than its TODO admits. In one measured case, implementing the
|
||||
obvious missing condition would still not restore the intended behaviour, because
|
||||
a second method on the same class overwrites the designer-authored values on its
|
||||
first call. The TODO points at one line; the repair is two.
|
||||
|
||||
Treat a TODO as evidence that the author knew about *something*, not as a
|
||||
specification of what is missing. Read the whole class before pricing the fix.
|
||||
|
||||
### What to do
|
||||
|
||||
Either implement it before exposing it, or delete the field. There is no third
|
||||
option that is honest. An editable property whose consumer is a literal constant
|
||||
is a trap for designers, who will tune it for days and then report that the build
|
||||
is broken.
|
||||
|
||||
---
|
||||
|
||||
## 4. C3 - Deliberate sample-scope narrowings
|
||||
|
||||
Nothing here is broken. The reference is internally consistent with every one of
|
||||
these. They become expensive only when the sample becomes the base of a product.
|
||||
|
||||
This is the category that costs a subsystem rather than a line, and the one that
|
||||
adoption discussions habitually skip, because there is nothing to point at.
|
||||
|
||||
### The largest one: no lag compensation
|
||||
|
||||
When a reference project has no server-side rewind - no historical position
|
||||
storage, no custom saved-move pipeline - everything downstream follows
|
||||
mechanically:
|
||||
|
||||
- the server does not trace, because tracing is gated on being locally
|
||||
controlled, and the server controls nobody;
|
||||
- the client's target data is consumed as authoritative, because there is nothing
|
||||
to compare it against;
|
||||
- damage falloff is computed from the client's own reported trace origin;
|
||||
- surface-material multipliers are taken from the client's reported hit.
|
||||
|
||||
Read as a defect list, this is four bugs. Read correctly, it is one design
|
||||
decision with four consequences. "The reference forgot to validate hits" is the
|
||||
wrong diagnosis, and it leads to four local patches that do not add up to server
|
||||
authority.
|
||||
|
||||
**Adoption decision:** choose a hit-registration model *before* borrowing a weapon
|
||||
stack. Server-authoritative with rewind, client-claim with plausibility checks, or
|
||||
full trust. A sample usually implements the third. Each has a different cost and a
|
||||
different cheat surface, and the choice dictates how abilities, tracing and damage
|
||||
are structured - which is why it cannot be retrofitted cheaply.
|
||||
|
||||
### Configuration that ships disabled
|
||||
|
||||
A subsystem can be present, implemented, documented by its own console variables,
|
||||
and switched off in shipped configuration with an essentially empty routing table.
|
||||
The implementation being there tells you nothing about whether it was ever
|
||||
exercised. Copying that configuration copies untested settings, and the settings
|
||||
are the part that needs production-scale traffic to tune.
|
||||
|
||||
### Machinery made unreachable by a mode switch
|
||||
|
||||
A load-mode enum set to "load everything upfront" can short-circuit every branch
|
||||
that would install a delay-load subsystem's delegates. The inherited machinery is
|
||||
functional; the sample bypasses it. The diagnostic tools built for that subsystem
|
||||
then report zeros forever, which reads as "nothing to report" rather than "this
|
||||
code never runs".
|
||||
|
||||
Important qualification: this is a *configuration choice of the sample*, not an
|
||||
engine defect. Diagnosing it as a broken engine feature sends you into engine
|
||||
source for no reason. Check the mode before you check the mechanism.
|
||||
|
||||
### The rest, in one table
|
||||
|
||||
| Narrowing | What it costs a product |
|
||||
|---|---|
|
||||
| State transitions on a health or death component written without an authority check, on public virtual methods | Death becomes a state with no owner; every caller is an authority |
|
||||
| Team or faction lookup that fails open on an invalid argument | Unknown membership resolves to "damage allowed" |
|
||||
| No tag or asset redirects configured anywhere | Every rename is a breaking change with no migration path |
|
||||
| A reliable server RPC declared without validation | Unvalidated input from the network; a Blueprint-authority marker does not constrain C++ callers |
|
||||
| An init-state gate that admits simulated proxies with no controller | "Ready" means something different per net role |
|
||||
|
||||
Each row is a line item in your adoption budget, not a bug report against the
|
||||
reference.
|
||||
|
||||
### The rule
|
||||
|
||||
> Every C3 narrowing you accept must be written down as an accepted risk with an
|
||||
> owner, or scheduled. "We did not notice" is the only unacceptable outcome.
|
||||
|
||||
---
|
||||
|
||||
## 5. C4 - Architecture tax
|
||||
|
||||
Correct, working, and sized for the reference author's team, release cadence and
|
||||
platform matrix. The question is never "is this good architecture". It is "does my
|
||||
product have the problem this solves".
|
||||
|
||||
### Measured shape of one reference project
|
||||
|
||||
| Element | Measured | When it stops paying |
|
||||
|---|---|---|
|
||||
| Definition-to-instance stack: experience selects feature plugins, which contribute action sets, which grant abilities and data | 5 gameplay-feature plugins out of 81 plugins total | One game mode and no post-launch content: you buy distributed control flow and receive no modularity in return |
|
||||
| C++ / Blueprint boundary | 493 Blueprints scanned, median 5 nodes each; 1256 `BlueprintCallable` entry points against 45 `BlueprintImplementableEvent` plus `BlueprintNativeEvent` override points | The 28:1 ratio means C++ exposes *calls*, not *extension points*. That assumes a permanently available C++ team. Without one, designers stall on their first non-trivial request |
|
||||
| Plugin-mounted content roots | primary game root holds 2 838 assets; all 105 mount roots hold 17 256; project-owned content is 3 876 | Any census, budget or memory number computed over the primary root alone understates by roughly six times |
|
||||
|
||||
The second row is the one people copy without noticing they copied it. A C++ to
|
||||
Blueprint ratio is a staffing decision wearing an architecture costume. Measure
|
||||
both columns before adopting the boundary, and name the person who will add to the
|
||||
second one.
|
||||
|
||||
The third row is a measurement discipline, not an architecture choice, and it
|
||||
generalizes: in any plugin-based project, enumerate all mount roots. Tooling that
|
||||
defaults to the primary game root will quietly report a fraction of reality.
|
||||
|
||||
### How to decide
|
||||
|
||||
State the question the modular stack answers *for you*:
|
||||
|
||||
- more than one team shipping into the same binary?
|
||||
- content added after launch without a client patch?
|
||||
- game modes swapped at runtime by data?
|
||||
- platform-specific feature sets?
|
||||
|
||||
Zero yes answers means the whole stack is tax. One or two means adopt the layers
|
||||
that answer them and stop there. Adopting for completeness is paying for an answer
|
||||
to a question you do not have.
|
||||
|
||||
---
|
||||
|
||||
## 6. C5 - Foreign-product legacy
|
||||
|
||||
Three recurring shapes:
|
||||
|
||||
- global constants and type identifiers carrying another title's brand, usually
|
||||
because the subsystem was lifted from that title;
|
||||
- a tag whose own descriptive comment admits it is in the wrong namespace and
|
||||
needs to move;
|
||||
- a class comment copy-pasted from a different class, describing something the
|
||||
class is not.
|
||||
|
||||
Harmless as code. Corrosive as vocabulary. Within a year nobody remembers why a
|
||||
primary asset type is named after another game's content, and part of the team
|
||||
assumes it is an engine term. The comment case is worse than the constant case: a
|
||||
wrong name is eventually questioned, a wrong explanation is believed.
|
||||
|
||||
Rename at import time. After the first sprint it becomes archaeology.
|
||||
|
||||
---
|
||||
|
||||
## 7. The counterexample worth copying
|
||||
|
||||
A register-and-unregister pair, in which the unregister path removes exactly the
|
||||
paths that registration added and then asserts on the removal count:
|
||||
|
||||
```cpp
|
||||
// on unregister
|
||||
const int32 NumRemoved = Registry.RemovePathsAddedBy(Feature);
|
||||
ensure(NumRemoved == PathsAddedBy(Feature).Num());
|
||||
```
|
||||
|
||||
In an audit whose dominant finding was "add works, remove is incomplete", this was
|
||||
the one place where teardown was both implemented and asserted. Copy the shape:
|
||||
paired add/remove, plus an assertion that the counts match. The assertion is what
|
||||
turns a silent leak into a test failure.
|
||||
|
||||
Note the asymmetry that remained even there: registration went through the
|
||||
project's own manager subclass while unregistration reached for the engine base
|
||||
class, and a refresh call made on the way in had no counterpart on the way out.
|
||||
Even the good example is worth reading twice - copy the shape, fix the asymmetry.
|
||||
|
||||
---
|
||||
|
||||
## 8. What source reading cannot settle
|
||||
|
||||
Three limits are worth stating, because they bound every claim in this skill:
|
||||
|
||||
1. **Blueprint call sites are invisible to source search.** A function with zero
|
||||
C++ callers may be called from a Blueprint graph. Absence of C++ callers is not
|
||||
absence of callers; settling it requires opening the graphs.
|
||||
2. **Data tables and data assets are binary.** How many entries a table
|
||||
contributes to a registry cannot be read from text. It requires the editor.
|
||||
3. **An empty search result proves the pattern, not the absence.** Widen the
|
||||
pattern, check the path, and check what your search tool excludes by default
|
||||
before concluding that something does not exist.
|
||||
|
||||
The third has caught real errors in this material more than once. Treat it as a
|
||||
standing rule rather than a caveat.
|
||||
|
||||
---
|
||||
|
||||
## Provenance
|
||||
|
||||
The classification and every measured number above come from a line-by-line audit
|
||||
of Epic's Lyra Starter Game on Unreal Engine 5.6, read as source rather than run,
|
||||
during a research project whose product was this skill set.
|
||||
|
||||
Source addresses stay in that archive. What ships is the recipe, because an
|
||||
address in a tree you do not have is not actionable and a recipe is. Entry
|
||||
identifiers (`RA-01` and up) in [failure modes](failure-modes.md) resolve back to
|
||||
the audited locations, so any specific claim can be produced on request.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
One reference project, one engine version, one workspace. This is a worked example
|
||||
of the classification, not a defect list to carry into other engine versions or
|
||||
other samples. Re-run the tests from `SKILL.md` against your own reference before
|
||||
trusting any specific claim here.
|
||||
Reference in New Issue
Block a user