feat(skills): ship ue-design-skills bundle, licensing and delivery gate

Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 23:48:55 +07:00
parent 80ea7b03a9
commit ecd87ac96d
67 changed files with 21630 additions and 0 deletions
@@ -0,0 +1,384 @@
# Failure modes: runtime allocation and caching
Ten ways a running game allocates more each minute, keeps what it should drop, or
dereferences memory that moved.
The shared property splits this list in two, and the split is worth stating
because it changes how you look for each half.
**The dangling-pointer half is data-dependent.** It needs a second key inserted
while a first entry is still live, or a new statistic to appear mid-session.
Neither happens in a short test, both happen in a match, and the crash lands far
from the cause. A null check never catches it, because the pointer is not null.
**The growth half is time-dependent.** Nothing is wrong at any instant; the
defect is a slope. A single memory sample cannot show it and a single frame
cannot contain it, which is why every recipe below asks for two measurements
rather than one.
Recipes use `rg` from a project source root, and were executed against the
audited project while this file was written.
---
## Pointers into moving storage
### RC-01 - A raw pointer into a map value, held across frames
**Mechanism.** A reference to a map value is taken, its address is stored in a
long-lived entry, and the map later gains a key. The map reallocates; the stored
address points into freed memory.
**Why it is silent.** Until a second key arrives, the address stays valid and
everything works. The window is narrow and specific — a live entry plus an insert
— so ordinary play produces it and ordinary testing does not.
**Why the obvious check misses it.** The code often *has* a check, and the check
passes: the pointer is not null, it is non-null garbage. Reading the storing line
shows a reference to a pool, which is exactly what the entry needs. The defect is
a property of the container's reallocation behaviour, which is nowhere in view.
**Symptom.** A crash inside the pooled object's own method, under load, with a
stack that looks like the pool is corrupt. In the audited project the window is a
one-second component lifespan plus a second visual style — trivially produced by
mixed normal and critical damage in the same second.
**Detect.** Find addresses taken from containers and stored:
```bash
rg -n -B2 -A4 "FindOrAdd\(|\.Find\(" --glob "*.cpp" . | rg "&\w+|Emplace\(|= &"
rg -n "\w+\*\s+\w+ = nullptr;" --glob "*.h" . | rg -i "pool|cache|list|entry"
```
The second search finds the *declaration* side — a bare pointer member whose name
suggests it points at a collection — which is often easier to spot than the
assignment.
**Guardrail.** Store a key or an index and resolve at use. If a stable address is
genuinely required, use a container that guarantees stable element addresses and
say so at the declaration. When you suspect one, run under the allocator's stomp
mode: it converts a probabilistic corruption into a deterministic crash on the
right line, and costs one run.
---
### RC-02 - A cache pointer handed out and stored by a widget
**Mechanism.** A subsystem returns a pointer into its own map so a widget can
read samples cheaply. The widget stores it. The map grows a key when a new data
source appears — a network connection, an optional module — and reallocates.
**Why it is silent.** The optimization is real and the pointer is valid at hand-
out. Growth is lazy and event-driven: the map gains keys minutes into a session,
when a connection is established or a feature is enabled, long after the widget
cached its pointer.
**Why the obvious check misses it.** Both sides are idiomatic. Returning a
pointer to avoid copying a sample buffer is good practice; caching it to avoid a
lookup per frame is also good practice. Neither side knows the map is still
growing, and nothing in either signature says the pointer has a lifetime.
**Symptom.** A crash in the widget's paint path, appearing only after the session
has been running long enough to acquire a new statistic — and therefore never in
a short repro. Compounded when the subsystem's teardown resets the tracker while
the widget still holds the pointer.
**Detect.** Find getters returning interior pointers, then find who stores them:
```bash
rg -n "const \w+\* \w+::Get\w*Data\(" --glob "*.cpp" . -A4 | rg "\.Find\("
rg -n "const \w+\* \w+ = nullptr;" --glob "*.h" . | rg -i "widget|graph|view"
```
A getter returning `Find` on a member map, plus a member pointer of that type in
a widget, is the pair.
**Guardrail.** Return a copy of the small value, or a handle the subsystem can
invalidate, or require the caller to re-fetch each frame. If a pointer must
escape, give the subsystem an invalidation broadcast and make subscribing
mandatory.
---
## Unbounded growth
### RC-03 - Read, append, write back — with no clear
**Mechanism.** Each event fetches an entire collection from another system,
appends one element, and writes the whole thing back. Nothing ever clears it.
**Why it is silent.** Every individual call is correct and fast. The array is a
legitimate data channel to the effects system, and its contents are consumed
visually, so nobody looks for a removal path.
**Why the obvious check misses it.** The two lines read as an idiomatic
accessor pair. The cost — two full copies per event — is invisible at small sizes,
and the growth is invisible without a second measurement. Reviewing the function
shows three lines of correct code.
**Symptom.** Cost per event rising through a match, total work quadratic in event
count, and memory that never returns. In a damage-number path this means the
hundredth hit costs measurably more than the first.
**Detect.** Find get-append-set triples on the same key:
```bash
rg -n -A3 "= \w*::Get\w*Array\w*\(" --glob "*.cpp" . | rg "Add\(|SetNiagara|Set\w*Array"
```
Then, for each hit, search the file for any clear:
```bash
rg -n "Empty\(\)|Reset\(\)|SetNum\(0\)|Clear" <that-file>
```
An append path with no clear anywhere in the file is the finding.
**Guardrail.** Give the collection an owner and an eviction rule — a cap, a
time-out, or a clear on the event that logically ends its contents. If the
consuming system owns the lifetime, ask it for a removal API rather than
assuming one.
---
### RC-04 - Re-append every previous element on every trigger
**Mechanism.** A component that spawns audio and effect components copies all
previously active ones into temporary arrays, appends the new ones, and writes
the union back — with no removal of finished components.
**Why it is silent.** Effects play and stop correctly, because stopping is the
component's own business. What accumulates is the *references*, and those are
invisible until something counts them.
**Why the obvious check misses it.** The function reads as careful state
management: gather, combine, store. The absent operation is a filter for finished
components, and absence in a function that is otherwise thorough is the hardest
kind to see.
**Symptom.** Thousands of retained effect components after a match. Because the
arrays are strong references, none of them can be collected, so this is
simultaneously a memory leak and a quadratic allocation profile in the hottest
cosmetic path — footsteps, at a few per second.
**Detect.** Find union-rebuild patterns and check for a prune:
```bash
rg -n -B4 -A6 "\.Append\(" --glob "*.cpp" . | rg "Empty\(\)|Reset\(\)"
rg -n "UPROPERTY\(Transient\)" -A2 --glob "*.h" . | rg "TArray<TObjectPtr<U\w*(Audio|Niagara|Particle)"
```
The second search alone is worth running in any project: a strong transient array
of effect components is a retention decision, and it needs a removal path.
**Guardrail.** Filter finished components on each pass, or use weak references
and let the components own their own lifetime. Prefer the reset-without-freeing
operation to the one that releases the buffer — see RC-06.
---
### RC-05 - Rebuilding descriptors on a timer instead of diffing
**Mechanism.** A periodic scan removes all its descriptor objects and creates new
ones for the same targets, several times a second.
**Why it is silent.** The result is correct: the right descriptors exist
afterwards. Object creation is fast enough that no single frame stands out.
**Why the obvious check misses it.** The function reads as a clean rebuild —
clear, then populate — which is a legitimate and common shape. The cost is in the
frequency, which is set elsewhere as a scan rate, and in the downstream churn:
each create and destroy propagates through observers into widget pooling and
canvas slot management.
**Symptom.** Steady allocation and observer churn while the player stands still
in a crowd of interactive objects. Profiles show constant low-level cost with no
obvious owner.
**Detect.** Find object creation inside timer-driven update functions:
```bash
rg -n -B10 "NewObject<" --glob "*.cpp" . | rg "Update\w+\(|OnTimer|ScanRate"
rg -n "ScanRate|Interval|SetTimer" --glob "*.h" --glob "*.cpp" . | rg -i "0\.[0-9]"
```
Also check the change-detection that gates the rebuild: comparison logic that
sorts only when sizes match will report a change on any reorder, making the
"only when changed" guard ineffective.
**Guardrail.** Diff against a stable key and reuse descriptors. If a rebuild is
genuinely simpler, measure it at the worst-case target count before accepting the
scan rate.
---
### RC-06 - Freeing the buffer in a hot path
**Mechanism.** The container operation that releases memory is used where the one
that keeps capacity was intended, in code that runs many times per second.
**Why it is silent.** Both operations leave the container empty and both are
correct. The difference is whether the next fill reallocates.
**Why the obvious check misses it.** The two calls are one word apart and read
identically at a glance. Nothing distinguishes a hot path from a cold one in the
call itself, and in teardown code the freeing version is the right choice — so the
same line is correct in one file and wrong in another.
**Symptom.** Allocator churn proportional to event frequency, showing up as
generic allocation cost rather than attributable to the system causing it.
**Detect.** Find the freeing operation and classify each site by frequency:
```bash
rg -n "\.Empty\(\)" --glob "*.cpp" . -B6 | rg -i "tick|paint|onhit|effect|update"
```
Teardown sites are fine. Anything reached more than once a second is the finding.
In the audited project the two hottest cosmetic arrays are freed and refilled on
every footstep.
**Guardrail.** Reset in hot paths, free at teardown, and write the reason in a
comment when it is not obvious which one a site is.
---
### RC-07 - Allocating in a paint or arrange path
**Mechanism.** A per-frame layout or paint function builds a local array, without
reserving, and lets it reallocate as it fills.
**Why it is silent.** It is correct, local and easy to read. The cost is a
handful of small allocations per frame, which no single profile sample
attributes.
**Why the obvious check misses it.** A local array in a function is the most
ordinary thing in the file. Knowing it matters requires knowing the function is
called once or more per frame — which is a property of the caller.
**Symptom.** A frame-time floor that nothing accounts for, and allocator pressure
that scales with the number of on-screen elements.
**Detect.** Find containers declared in per-frame widget paths:
```bash
rg -n -A12 "::OnPaint\(|::OnArrangeChildren\(|::Tick\(" --glob "*.cpp" . \
| rg "TArray<|TMap<"
rg -n "static TArray|static TMap" --glob "*.cpp" . | rg -i "widget|slate|paint"
```
The second search finds the wrong fix for the first: a static container avoids the
allocation and introduces sharing between every instance of the widget, plus a
thread-safety assumption nobody wrote down. In the audited project both appear —
one path allocates per frame, another uses a static.
**Guardrail.** A mutable member with a reset at the top of the function. Not a
local, not a static.
---
## Semantics
### RC-08 - A prefilled ring buffer reporting statistics
**Mechanism.** A fixed-capacity sample cache is filled with zeroes at
construction. Minimum, average and related statistics include the prefix until
the buffer wraps.
**Why it is silent.** Every value returned is a real number computed correctly
from the buffer's contents. There is no error state for "not enough samples yet",
because the buffer is always full.
**Why the obvious check misses it.** The implementation is correct for a full
buffer, and a reviewer checks the arithmetic rather than the initial condition.
The distortion disappears after a few seconds, so it is gone before anyone
investigates.
**Symptom.** A minimum of zero and a depressed average on any graph for the first
seconds after it appears — which is exactly when someone is looking at it.
**Detect.** Find prefilled buffers and check whether the statistics account for
fill level:
```bash
rg -n "AddZeroed\(|SetNumZeroed\(" --glob "*.h" --glob "*.cpp" .
rg -n -A6 "GetMin\(\)|GetAverage\(\)" --glob "*.h" . | rg "Num\(\)|SampleSize|NumValid"
```
Statistics dividing by capacity rather than by a fill count is the finding.
**Guardrail.** Track the number of valid samples and compute over that window.
Also check the accessor names: an index-based "current" that returns the *next
write position* is the oldest sample, not the newest — a naming trap worth
checking in any ring buffer.
---
### RC-09 - An identifier that is a count, at three different widths
**Mechanism.** A batch identifier is assigned from the number of in-flight
batches rather than a monotonic sequence, so identifiers are reused as batches
complete. Separately, the same value is one width at the source, another in the
message, and a third in storage.
**Why it is silent.** With one or two batches in flight, counts and sequences are
indistinguishable. The width mismatch is invisible until the value exceeds the
smallest type — which needs sustained fire and a degraded connection.
**Why the obvious check misses it.** Each half is defensible alone: a count is a
plausible identifier when everything completes promptly, and each width is
reasonable in its own context. Confirming means reading a declaration, a
constructor and a message signature that are not adjacent — in the audited project
the storage width and the message width are sixteen lines apart in the same
header, which is exactly far enough.
**Symptom.** Confirmations applied to the wrong batch when identifiers are
reused, and — once the counter passes the narrow type's range — confirmations that
match nothing at all, so the pending list grows without bound.
**Detect.** Trace one identifier through the whole path and compare widths:
```bash
I='UniqueId'
rg -n "\b$I\b" --glob "*.h" --glob "*.cpp" .
```
Read the output as a table of types. Then check the assignment: an identifier
taken from a `Num()` or a count getter is a reused identifier.
**Guardrail.** One monotonic sequence, one width, asserted at every boundary.
Define wrap behaviour. Give the pending list a timeout and a drain, so a matching
failure degrades instead of accumulating.
---
### RC-10 - Broadcasting before the state is consistent
**Mechanism.** A registry broadcasts an "added" event before inserting the item
into its own collection. A listener that responds by enumerating the collection
sees a state that does not include the item it was just told about.
**Why it is silent.** For a listener that only uses the event payload, the order
is irrelevant and everything works. The defect needs a listener that also reads
the collection — typically one doing late binding, catching up on existing items.
**Why the obvious check misses it.** Two adjacent lines, both correct, in an
order that reads naturally: announce, then record. Reversing them looks like a
stylistic preference until you know a listener enumerates.
**Symptom.** An item lost or added twice, depending on which listener ran when.
In the audited project a canvas both handles the event and enumerates existing
items on attach, and it adds to its own list without a duplicate check.
**Detect.** Find broadcasts that precede the mutation they announce:
```bash
rg -n -B2 -A2 "\.Broadcast\(" --glob "*.cpp" . | rg "Add\(|Remove\(|Emplace\("
```
A broadcast on the line *before* the insert is the finding. Then check the
listener for a duplicate guard.
**Guardrail.** Mutate, then broadcast. Listeners must observe a consistent
container, and any listener that also enumerates on attach needs an idempotent
add.
@@ -0,0 +1,260 @@
# Patterns: runtime allocation and caching in a measured product
A worked reading of the runtime allocation, pooling, caching and replication-cost
surface of one shipped project.
This file differs from the others in one respect worth flagging: **two of its
findings are memory-safety defects, not design smells.** Everywhere else in this
bundle the failures are silent-but-benign until adopted. Here, two of them are
dangling pointers in shipping paths, and they are silent for the ordinary reason —
freed memory usually still contains the right bytes.
Markers: **[measured]** — read in source; **[derived]** — conclusion from measured
facts; **[open]** — requires a runtime experiment.
**Nothing here was profiled.** Every claim is structural. That is a limitation
and also the point: both critical findings were found by reading, and both are
confirmable in one run under an allocator that poisons freed pages.
---
## 1. The same defect twice, in two unrelated subsystems
A raw pointer taken from a container's storage and held across frames. It appears
independently in a cosmetic feedback pool and in a statistics cache
**[measured]**.
**Case one — the pool.** A map from mesh to a pooled component list. Code takes a
reference to a map value, stores its address in a live-entry struct, and
dereferences it up to a second later when the entry expires **[measured]**.
**Case two — the statistics cache.** A map from statistic type to a fixed-size
sample cache. A getter returns the address of a map value; a Slate widget stores
that pointer as a member and dereferences it **every frame during paint**
**[measured]**.
**[derived]** Both are correct until the map rehashes, and both maps grow after
the pointer is taken:
- the pool map gains a key the first time a second visual style appears — mixed
normal and critical damage within one second is enough;
- the statistics map is **populated lazily**, gaining keys as a game state
appears, a player state appears, a network connection appears, and a latency
module is enabled **[measured]**. A widget that cached a pointer before any of
those events is holding a stale address afterwards.
The second case is worse in a specific way: the dereference happens in a paint
path that the widget declares as always volatile, so it runs every frame with no
invalidation mechanism **[measured]**.
**[derived]** The general rule this yields is worth more than the two instances:
**a pointer obtained from a container and stored beyond the current scope is a
review blocker, regardless of how the container is used.** Store a key or an
index and resolve at use. The audited code even guards one of them with a
non-null check, which cannot detect this failure — the pointer is non-null
garbage. Recipes: RC-01, RC-02.
---
## 2. Unbounded growth, three shapes
| Shape **[measured]** | Where | What it costs |
|---|---|---|
| Read whole array, append one, write whole array back — never cleared | damage number effect data | Linear per event, quadratic per session, two full copies per hit |
| Re-append every previously active component on each trigger, never prune finished ones | footstep and impact effects | Strong references pin every one-shot sound and effect for the whole match |
| Pending protocol entries removed only on a matching confirmation | hit-marker batches | Grows without bound once matching stops working (§4) |
**[derived]** The first two share a structure that is easy to miss in review: the
code that grows the collection is also the code that appears to manage it. Reading
`Empty()` followed by `Append()` looks like a refresh. It is a refresh that
re-adds everything, and the array is a strong property, so nothing is ever
released.
The second one also uses `Empty()` rather than `Reset()` in a path that runs at
footstep frequency **[measured]**, which frees the buffer and guarantees a
reallocation on the next call — a smaller problem sitting inside a larger one.
Recipes: RC-03, RC-04, RC-06.
---
## 3. Pools without lifecycle
The component pool grows to the peak concurrent count and **never shrinks**
**[measured]**. One burst of two hundred simultaneous hits leaves two hundred
components allocated for the rest of the match.
A second pool in the indicator system is fixed at ten entries with surplus
requests simply hidden **[measured]** — which is a legitimate policy, stated
nowhere.
**[derived]** The contrast between the two is the lesson: one pool has no
high-water policy and the other has an undocumented one. Both are the same
omission — **a pool is not correct until its exhaustion and trim behaviour are
written down.** Neither project reader can answer "what happens at peak?" without
reading the implementation.
Related and cheaper to fix: the live queue removes from the front of an array,
shifting the tail on every timer tick **[measured]**, where a ring buffer would
not move anything.
---
## 4. An identifier that is a count
The clearest single defect in the audit, and a good example of why protocol
identifiers deserve their own review rule.
A hit-marker batch is identified by **the current number of unconfirmed batches**
rather than by a monotonically increasing sequence **[measured]**.
**[derived]** The failure needs no packet loss to reason about. Batch A takes
identifier 0. Batch B takes 1. A is confirmed and removed. C now takes 1 — the
same identifier B is still holding. The confirmation handler matches on the first
entry with that identifier, so a confirmation for one batch can be applied to
another.
Compounding it, the same value is carried at **three different widths** along one
path: signed 32-bit at the source, unsigned 16-bit in the network call, and
unsigned 8-bit in storage **[measured]** — the last two declared sixteen lines
apart in the same header. Past 255 unconfirmed entries the storage wraps and the
network value does not, so matching stops entirely and the pending list grows
without bound.
Recipe: RC-09. The rules that fall out: identifiers are sequences, one width for
the whole path, and pending lists get a timeout and a drain policy.
---
## 5. Allocation in paint and arrange paths
Three instances, all in per-frame Slate code **[measured]**:
- a local array built and sorted on every arrange, without reserving;
- a full array copy returned by value from a getter called during paint;
- a **static** array used as scratch space in a draw helper.
**[derived]** The third is the interesting one, because it is an optimisation. It
avoids per-call allocation and introduces two new problems: the buffer is shared
by every instance of the widget in every window, and it is not thread-safe in a
context where paint is not guaranteed to be on one thread. The correct form —
a mutable member reset per call — is the same cost and none of the risk.
Recipe: RC-07. Also visible in the same code: a component lookup by class
performed both in paint and in tick, twice per frame, uncached **[measured]**.
---
## 6. Rebuild instead of diff
An interaction system destroys every descriptor object and creates new ones on a
tenth-of-a-second timer, for the same targets **[measured]**. Each cycle
allocates objects, invalidates observers, and drives a full add/remove cycle
through the indicator canvas.
**[derived]** The change-detection that gates this is itself unreliable: the
comparison sorts the candidate list only when the two lists have equal length
**[measured]**, so an equal-length reordering reports a change that did not
happen. In a crowd of interactable objects the rebuild runs ten times a second.
The fix is the general one: compute the desired set, diff against the current
set, and add and remove the difference. Recipe: RC-05.
A related ordering defect sits in the same subsystem: the add notification is
broadcast **before** the element is inserted **[measured]**, so a listener that
enumerates existing elements during that window either misses it or receives it
twice — and the receiving side adds without a duplicate check. Recipe: RC-10.
---
## 7. Statistics that are wrong before the window fills
The sample cache is pre-filled with zeroes at construction **[measured]**. Until
the ring wraps — which takes as many frames as its capacity — the minimum reads
zero and the average is divided by the full capacity rather than by the number of
real samples **[measured]**.
**[derived]** So the first seconds of every session report a minimum of zero and
an artificially low average, on a performance overlay whose entire purpose is to
be read during those seconds.
There is a second, quieter trap in the same class: a method named for the current
sample returns the **oldest** one, because the index points at the next write
position **[measured]**. The correctly-named sibling exists and is what the
project uses, which is why nobody noticed. Recipe: RC-08.
The cache itself is well bounded — capacity times sample size times statistic
count is on the order of tens of kilobytes **[derived]** — so this is a
correctness finding, not a memory one. Both matter; conflating them is how
"performance work" produces no performance.
---
## 8. Replication: name the axis
The audit's replication half produced no new defects and one clarification worth
carrying, because it is routinely got backwards:
| Mechanism | Memory | CPU | Bandwidth |
|---|---|---|---|
| Excluding presentation on a dedicated server | down | down | **unchanged** |
| Delta-serialised arrays | **up** (per-item key, per-connection state) | **up** (delta walk) | down |
| Subobject replication | down | down | down |
| Dormancy, relevancy, replication graph | — | down | down |
**[derived]** Two of these surprise people. Excluding cosmetics on a server saves
memory and CPU and changes bandwidth by exactly zero, because the intent still
replicates. And delta serialisation is a **bandwidth** optimisation that costs
memory and CPU — adopting it to "reduce replication cost" without naming the axis
can make the measured problem worse.
The genuine object-count reducer is subobject replication: many owned items, zero
additional actors and channels.
Before claiming any replication optimisation: state which axis improves and which
regresses, measure that axis specifically, and **confirm the mechanism is
actually enabled** — see `ue-multiplayer-authority`, NA-15, for a replication
graph that ships disabled with configured routing.
---
## 9. What to do with this list
In order of ratio of certainty to effort:
1. **Run once under an allocator that poisons freed memory**, with a mixed-style
damage burst and with a latency module toggled on while the performance
overlay is open. Those two scenarios turn RC-01 and RC-02 from arguments into
crashes at the right line. **[open]** — this is the one experiment that
settles the two critical findings.
2. **Add five counters to the existing profiling category** — pool size, live
entries, pending protocol entries, active effect components, indicator count.
The category and an example already exist in the audited project
**[measured]**; extending it is three lines each and produces boundedness
curves over a match, which is what actually distinguishes a cache from a leak.
3. **Grep for the two structural patterns** — stored container pointers, and
`Empty()` in hot paths. Both are one command and both found real instances
here.
4. Everything else is ordinary optimisation and can wait for a profile.
**[derived]** Note the shape of that list: the cheapest actions are the ones that
convert reading into evidence. A structural audit without step 1 produces a
plausible argument; with it, a defect report.
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6, read as source in
a single workspace: feedback and effect components, the indicator system, the
performance statistics subsystem, weapon state and targeting, equipment and
inventory, and the replication surface of each. Source addresses stay in the
research archive that produced this skill; each `RC-` identifier resolves back to
the audited location there.
## Evidence boundary
No profiler was run and no allocator instrumentation was used. Every finding is
structural, derived from reading; the growth characterisations are arguments
about code shape, not measurements. Quantities such as pool capacities and buffer
sizes are declared values read from source. Re-run the recipes and the two
scenarios above against your own tree before acting on any of it.