Files
MagentaDolphin ecd87ac96d 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>
2026-09-05 23:48:55 +07:00

201 lines
8.9 KiB
Markdown

# Patterns: a cue subsystem read end to end
A worked reading of one real GameplayCue subsystem, audited as source. It is here
because the cue layer has a property that makes reading a class list useless:
**large parts of it can be present, correct, maintained, and unreachable**, and
nothing in the structure says which parts.
Individual failures are in [failure-modes.md](failure-modes.md), one entry each,
with a recipe. This file is about the shapes — what the subsystem is for, where
its cost lives, and which of its parts were actually running.
Markers: **[measured]** — read in source; **[derived]** — conclusion from measured
facts; **[open]** — not settled by source reading, and left open.
---
## 1. The trade the mechanism makes
A cue replaces an asset reference with a string-like tag, and a registry resolves
the tag by scanning content directories.
What you buy is real: gameplay assets carry no presentation payload, presentation
ships in a separate plugin, and the whole layer is absent on a dedicated server.
What you pay is one specific thing, and it is worth naming precisely:
> **You have exchanged a reference the toolchain checks for a join the toolchain
> does not check.**
A hard pointer that dangles is a cook error. A tag with no notify is silence.
Both halves of the join stay individually valid — the tag exists, the asset
exists — and no tool owns the relation between them. Everything in this file
follows from that one exchange.
This is why cues are an application of tag governance with a loading subsystem
attached, rather than a feature in their own right. Every rule about string joins
applies here first, and the loading subsystem is where the surprises live.
---
## 2. Where the complexity actually is
Reading the audited manager, the code divides into three very unequal parts:
| Part | Size | Status in the shipped configuration |
|---|---|---|
| Dispatch: tag arrives, notify is invoked | small | running |
| Discovery: scan directories, build the tag-to-asset index | moderate | running |
| Preload: async library load, preload set, always-loaded tags, reference tracking, GC and map-transition hooks | **most of the file** | **unreachable** |
The third row is the finding, and it is a shape rather than a bug.
A load-mode constant is set to "load everything upfront" **[measured]**. That
value short-circuits three separate switch sites **[measured]**, and the third of
them returns before the statements that bind the subsystem's delegates
**[measured]**.
The consequence **[derived]**: tag-loaded, post-garbage-collect and post-load-map
handlers are never bound; the preload set, the always-loaded set and the reference
tracker are never populated; and the project's own diagnostic command reports
zero, forever.
**That last detail is the trap.** A diagnostic reporting zero reads as "there is
nothing to preload". It actually means "this counter is dead". Anyone
investigating cue memory starts from a number that is not measuring anything, and
the natural next step — reading the preload implementation for a bug — is a day
spent in correct code. Recipe: GC-11.
The transferable rule is not about cues at all:
> Before debugging a subsystem, establish that its code runs. A diagnostic that
> reports zero is a claim about reachability until proven otherwise.
---
## 3. Knobs that are not knobs
In the same file, a namespace named after console variables contains three
declarations **[measured]**. Exactly one — a console *command* — is registered
with the engine. The other two, including the load mode from §2, are plain
statics **[measured]**.
Project-wide the ratio is one such namespace against eight real console-variable
registrations **[measured]**.
Two things make this worth an entry rather than a footnote:
1. **The naming is the entire defect.** The code is correct; it is a constant and
behaves as one. What lies is the scope name, and nothing checks scope names.
2. **The obvious search fails.** Grepping for the literal `namespace Cvars` finds
nothing, because real names are prefixed with the owning type. The first draft
of the detection recipe for this returned zero hits and would have been read as
"we do not have this problem". The corrected recipe searches for any namespace
whose name *contains* the token. This is recorded in GC-12 with the trap
spelled out, because the trap generalises further than the finding does.
---
## 4. Feature-plugin registration: the one thing done right
Cue path registration from a feature plugin has a timing constraint that is easy
to get wrong and invisible when you do: the manager builds its index during its
object library scan, so a path added after that scan is not indexed until
something triggers a rescan. In the editor, incidental rescans hide it. In a cold
packaged run, nothing does. Recipe: GC-14.
The audited project solves it with a pattern worth copying verbatim
**[measured]**:
> **The feature action is a pure data declaration; a lifecycle observer is the
> executor.**
The action object declares the directory list and validates it in the editor — and
has *no activation body at all* **[measured]**. A separate observer registered
with the feature policy listens for the earliest lifecycle phase and performs the
registration for every action of that type it finds. Paths are added with the
rescan flag off, and a single index rebuild follows **[measured]**.
The empty activation body is the part people delete when adopting this, because an
action with no `Activate` looks unfinished. It is not: it is the point.
### And the asymmetry that survived inside it
The same function pair is also the audit's best example of a near-miss
**[measured]**:
| Direction | Manager resolution | Refresh call |
|---|---|---|
| Registering | project's own manager subclass | present |
| Unregistering | engine base class | **absent** |
Both calls compile and both work, because the subclass inherits the method. They
are equivalent exactly as long as the subclass adds no bookkeeping **[derived]**.
And the refresh performed on the way in has no counterpart on the way out, so the
asset manager keeps describing directories that have been removed **[derived]**.
Recipes: GC-15, GC-16.
**What redeems it, and what to actually copy:** the removal path counts what it
removed and asserts the count against what was added **[measured]**. In an audit
whose dominant finding across the whole project was "add works, remove is
incomplete", this was the one place where teardown was both implemented and
asserted. Copy the assertion; fix the asymmetry it sits next to.
---
## 5. The network rule, and why it is first
Cues never run on a dedicated server, because the presentation layer does not
exist there.
Every consequence of that is a design constraint rather than a caution:
- a notify may not carry gameplay side effects — on a dedicated server that code
never executes (GC-01);
- cue parameters are a lossy channel, and what a conversion helper drops is
dropped silently on every client. The audited helpers drop a context tag
container in **both** directions, each with its own unfinished-work comment
**[measured]** (GC-17);
- delivery is unreliable by design: packet loss, relevancy changes and mid-effect
joins all produce missed cues;
- cue assets belong in the client bundle only.
The acceptance test that covers all four at once is cheap and nobody runs it: **run
the feature on a dedicated server with every cue asset removed, and require
byte-identical gameplay results.**
---
## 6. What source reading could not settle
Two limits, stated because they bound the claims above:
1. **Blueprint call sites are invisible to text search.** The conversion helpers
in §5 have zero C++ callers and are Blueprint-callable **[measured]**. That is
recorded as **[open]** — an open question, not dead code. Deleting them on the
strength of an empty `rg` result is GC-18, committed by the person who wrote
the audit. (It was not.)
2. **Editor memory figures do not describe shipping.** The experience loader's
bundle selection unions client and server bundles when running in the editor
**[measured]**, so in-editor cue residency is overstated by whatever the server
set contains. Editor measurements are good for direction of change and nothing
else (GC-20).
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6, read as source
rather than run, in a single workspace. Source addresses stay in the research
archive that produced this skill; each `GC-` identifier resolves back to the
audited location there, so any specific claim above can be produced on request.
## Evidence boundary
One project, one engine version, one workspace. These are examples and failure
evidence, not guarantees about other engine versions or other samples. Several
claims are explicitly marked open and stay open. Re-run the recipes in
[failure-modes.md](failure-modes.md) against your own tree before acting on
anything here.