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,229 @@
# Patterns: a measured settings and performance layer
A worked example of `SKILL.md` against one reference product. The conclusion is
unusual enough to state at the top, because it changes what you should do with
the rest of the file:
> **The framework is the best thing in the audited project. The wiring around it
> is where every defect is.**
Roughly 5700 lines of settings framework contain no reference to the game that
ships it, and no finding in this file is about its design. The nine failure modes
are all in the 2100 lines of project-specific backend and registry that bind it —
paths that the compiler cannot check, a pipeline with no caller, a composition
part with no writer. That distinction is the adoption advice: **take the
framework, audit the wiring**.
Markers: **[measured]** — read in source; **[derived]** — conclusion from
measured facts; **[open]** — not answerable from source.
---
## 1. What the framework gives you, concretely
| Capability **[measured]** | Why it is hard to rebuild |
|---|---|
| Setting model fully separated from widgets | The separation is what makes search, grouping and testing possible at all |
| Widget resolved by walking the model's **superclass chain** | Map one base class, cover every derived setting, keep specific overrides |
| Apply / cancel / reset as a real transaction over a dirty set | Correct restore of *runtime* state, not only stored values |
| Edit conditions with five outcomes, not a boolean | disabled-with-player-reason, hidden-with-developer-reason, single option disabled, killed |
| Cascading dependencies between settings | Changing one setting re-evaluates the others without hand-written wiring |
| Full-text search over rich text reduced to plain text | Falls out of the model separation |
| Asynchronous readiness before the screen renders | Audio device lists and cloud values arrive late; the screen waits |
| A debug mode that shows developer names and classes in the UI | Makes every other finding in this file findable |
**[derived]** The five-outcome edit condition is the piece most worth
internalising even if you write your own framework. Hand-built settings screens
almost universally have a boolean, and the two missing things — *tell the player
why it is disabled* and *tell the developer why it is hidden* — are exactly the
two questions asked later at cost.
---
## 2. The trade the framework makes, and where it lands
Bindings are **string paths resolved by reflection** — a getter and setter path
rooted at the local player **[measured]**. That removes a class per setting and
removes the compiler from the relationship.
The failure mode is precise: the stringification macro checks that a function
*name* exists, not its type, arity or constness; mismatches surface as an
assertion at registry initialization; **assertions are compiled out of shipping**
**[measured]**.
**[derived]** So the worst case is a shipping build where a setting reads a
default and writes nowhere, with no diagnostic anywhere. That is not an argument
against the design — it is an argument for one automated test that walks the
registry and resolves every path. Recipes: GS-01, GS-02.
---
## 3. The ownership split, and the leak inside it
The design is a clean two-store split **[measured]**:
| | Machine-local | Player-shared |
|---|---|---|
| Base | engine user settings, ini | per-player save game, cloud-syncable |
| Holds | resolution, quality, frame limits, device profile, benchmark result | colour-blind mode, sensitivities, subtitles, language, haptics |
The authors state the rationale in a comment: shared settings are safe to put in
the cloud and are stored per player, so controller preferences belong there
rather than in local settings where every user would receive them **[measured]**.
Then the accessor breaks it. The per-player getter for local settings returns a
**process-global singleton** **[measured]**.
**[derived]** "This player's local settings" is a fiction; there is one object per
process. The corroborating evidence is structural rather than a second defect:
the registry is dense with "only while playing as the primary player" conditions,
which is the architecture apologising for the accessor. Recipe: GS-03.
A second, milder leak in the same area: audio volumes live in machine-local
storage **[measured]** — a pure player preference placed by where the applying
subsystem reads, not by what the value means. The audio registry reaches into
both stores from adjacent lines. Recipe: GS-04.
**[derived]** Both are worth seeing together, because they are the same mistake at
two scales: **storage placed by implementation convenience rather than by
semantics**, then papered over at the call sites.
---
## 4. Three findings that are the same shape
This is the most transferable observation in the file. Three unrelated features,
one structure:
| Feature | Present **[measured]** | Missing **[measured]** |
|---|---|---|
| Automatic quality detection at first launch | capability check, benchmark, threshold mapping, apply, save | **any caller** of the decision function |
| Per-game-mode device profiles | name composition, fallback chain, four candidate forms, logging | **any writer** of the mode suffix |
| Input sensitivity application | storage, dirty tracking, save, load, an apply function | **a body** in the apply function |
**[derived]** Each is complete except for one link, and in each case the missing
link is invisible to every check that the rest of the feature passes. The
decision function has two occurrences — its declaration and its definition. The
suffix has six occurrences, of which one is a log line printing it and none is an
assignment. The apply function is called correctly from the right place and
contains nothing.
The suffix case deserves emphasis because it is the canonical shape from
`ue-reference-project-adoption` appearing in a subsystem where the consequence is
architectural: **a variable read four times and written zero times**, with the
authors' own comment noting that nothing sets it. Any search asking "is this
used?" answers yes, loudly. Recipes: GS-06, GS-07, GS-08.
---
## 5. Where the transaction leaks
Video settings apply **immediately from inside a change notification**, not on
apply, and the code says so with a comment reading "for now" **[measured]**.
**[derived]** Cancel still restores the value, so the transaction is not broken —
it is leaky in a way the player sees: a preview they did not ask for, on a global
apply path, potentially once per slider tick.
The comment is what turns this from a judgement call into a finding. **A "for
now" on a global apply path is a scheduled item.** Recipe: GS-05.
---
## 6. The upgrade guard, and the number it does not tell you
Manual enumeration of an engine struct's members — copying, clamping and
maximising quality levels field by field — is protected by a compile-time
assertion on the struct's size, at **five separate sites** for one struct
**[measured]**.
**[derived]** This is good practice and it works: after an engine upgrade adds a
quality channel, the build breaks rather than silently ignoring it. The gap is
procedural. The person who hits the assertion during an upgrade needs to know
*how many functions* must be reviewed, and that number is nowhere near the
message. Recipe: GS-09.
**Put the count in the assertion text.** A guard is only as useful as the
instruction it gives the person it stops.
---
## 7. The reapplication point, and why it is where it is
Global scalability is reapplied at the **last line** of the mode-load completion,
after the load state is set and after all three waves of load notification
**[measured]**. The same function serves the other trigger that changes the same
state — a hotfixed device profile **[measured]**.
Four reasons, all measured, and together they are a small lesson in ordering:
1. feature plugins activated by the mode can change console variables and
frontend policy, so effective values are unknown until they have all run;
2. the device profile itself may change, and the reapplication reselects it;
3. scalability is process-global, not per world — the authors note this in a
comment explaining why they do not track it per world in multi-instance
editor play;
4. it is guarded off for servers, where render settings are meaningless.
**[derived]** Two different causes — mode loaded, profile hotfixed — reduced to
one idempotent reaction at one named point. That arrangement is worth copying
directly, and it is the opposite of letting each feature reapply what it thinks
it changed.
---
## 8. Performance statistics: one collector, and the honest limits
Statistics come from the engine's own frame-data consumer interface rather than
from hand-rolled timers **[measured]**, feeding a ring buffer for graphing.
Eighteen statistics, with a compile-time count assertion in three places
**[measured]**.
**[derived]** The design point worth copying is that presentation and collection
are separate: widgets read a cache, and no widget collects. The design point worth
questioning before copying is that a buffer size is meaningless without a
documented sample cadence — a hundred-odd samples describes a window only once you
know the interval.
Two hard couplings to be aware of before lifting the subsystem: one statistic
reads a game-specific state class, and latency statistics are gated on a specific
GPU vendor because the project integrates one vendor's plugin **[measured]**.
Both are small and both are visible; neither is a reason to rewrite the rest.
---
## 9. What to copy, in order
1. **The framework itself**, if the UI dependency is acceptable. Five-outcome edit
conditions, superclass-chain widget resolution, the change tracker, async
readiness.
2. **The reapplication point** (§7) — one idempotent function, two causes.
3. **The two-store split** (§3) — but decide placement by semantics and check
your accessor's body before trusting the word "local".
4. **The size-guard discipline** (§6), improved by writing the count into the
message.
5. **Collector/presenter separation** for statistics (§8).
Audit before shipping: every property path resolves (GS-01, GS-02); the accessor
is not a singleton (GS-03); every apply function has a body (GS-06); every
composition part has a writer (GS-07); every pipeline has a caller (GS-08).
**[derived]** That list is short, mechanical, and would have caught every defect
in this file. None of it requires understanding the framework — which is the
point of shipping recipes rather than conclusions.
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6 and the settings
plugin shipped with it: roughly 5700 lines of framework, 2100 of project settings
backends, 2000 of registry, and 800 of performance code, read as source in a
single workspace. Source addresses stay in the research archive that produced
this skill; each `GS-` identifier resolves back to the audited location there.
## Evidence boundary
Widget assets are binary and were not read, so nothing here describes the
presentation layer beyond its base classes. Configuration values quoted describe
that project on that day. Re-run the recipes against your own tree.