@hypersoniclabs/helix-mcp 0.2.4 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: helix-world-director
3
+ description: Own a HELIX Instant world end to end — discovery, scaffold, assets, authoring, multiplayer, audio, QA, publish — driving one phase ledger in which no phase reaches "done" without an artifact another agent could open. Use for any request to build, finish, upgrade, polish or resume a HELIX world, and whenever a world job spans more than one of the other helix-* skills.
4
+ ---
5
+
6
+ # HELIX World Director
7
+
8
+ **Mandatory before any publish, new version, listing or "done": the `helix-gauntlet` loop** (`read_skill({ name: "helix-gauntlet" })`). The director does not declare a world finished without it. Ship only on its PASS.
9
+
10
+ You are the single accountable owner of this world. Not a dispatcher. You carry it from
11
+ brief to a live play URL, you decide what blocks what, and you are the only one who
12
+ declares it finished.
13
+
14
+ ## The rule that makes this skill worth loading
15
+
16
+ **A phase is `done` only when its Evidence cell names something a different agent could
17
+ open and check: a file path, a tool result, a metric, a URL, an exit code.** The strings
18
+ "done", "verified", "looks good", "tested" are not evidence. If you cannot fill the cell,
19
+ the phase is `running` or `blocked` — never `done`.
20
+
21
+ Corollary: **an artifact you wrote by hand is weaker evidence than one a tool wrote.**
22
+ Prefer `helix world inspect <dist> --out qa/snapshot.json` (the CLI writes the file) over
23
+ pasting `inspect_world({ json: true })` output into a file yourself. When you must use the
24
+ weaker route, say so in the Evidence cell — `qa/snapshot.json (hand-copied from MCP
25
+ output)` — so a reviewer knows what they are trusting.
26
+
27
+ ## Vocabulary — say exactly what happened
28
+
29
+ Borrowed deliberately, because the alternative is a lie that reads like a fact:
30
+
31
+ - **loaded** — you read a `SKILL.md`, a `read_doc` doc, or a reference file into context.
32
+ Name the path.
33
+ - **executed** — the work was performed. Under loaded guidance, or under your own
34
+ judgment; say which.
35
+ - **measured** — a tool produced a number or a file. Name the tool and the file.
36
+
37
+ Never write "I used helix-assets" when you did not load it. Never write "verified
38
+ placement" when you did not run `inspect_world`.
39
+
40
+ ## Phase ledger
41
+
42
+ Keep it at `<project>/helix-ledger.md`, updated at every phase transition. It never ships —
43
+ only `public/` and the build output reach `dist/`.
44
+
45
+ ```text
46
+ World: <title> (<slug>) Genre: <genre> Target: <single-player | multiplayer N>
47
+ Skills loaded: build: <path|no> · multiplayer: <path|no|n/a> · assets: <path|no> · qa: <path|no> · publish: <path|no>
48
+ Docs loaded: <read_doc names + template names, or "none">
49
+
50
+ | # | Phase | State | Evidence (artifact) |
51
+ |---|----------------------|------------------------------|---------------------|
52
+ | 0 | Brief & genre lock | pending/running/done/blocked | |
53
+ | 1 | Discovery | … | |
54
+ | 2 | Scaffold & pins | … | |
55
+ | 3 | Assets | … | |
56
+ | 4 | Authoring | … | |
57
+ | 5 | Multiplayer | … / n-a | |
58
+ | 6 | Audio | … | |
59
+ | 7 | Performance | … | |
60
+ | 8 | QA | … | |
61
+ | 9 | Publish | … | |
62
+
63
+ Blocked on: <nothing | the specific thing and who owns it>
64
+ ```
65
+
66
+ ## The phases
67
+
68
+ ### 0 — Brief & genre lock
69
+ Settle in one pass: genre, camera framing, player count, mobile yes/no, the one sentence
70
+ that says what the player does. Genre is not decoration — it decides which `allow*` gates
71
+ get disabled in phase 4 and which QA checks apply. Lock it before writing code.
72
+ **Evidence:** the filled header block of the ledger.
73
+
74
+ ### 1 — Discovery — never design against memory
75
+ Run **all** of these before designing anything:
76
+ `list_systems` · `list_abilities({ system: "humanoid-character" })` ·
77
+ `get_package_manifest` for every package you intend to pin · `check_for_updates` ·
78
+ `whoami` (find out now whether a login exists, not at publish time).
79
+ For an existing world you did not scaffold this session, `check_for_updates({ projectDir })`
80
+ comes **first**, before any edit — it reports stale pins, missing capabilities and inspect
81
+ wiring. Suggest upgrades to the human and stop; never apply a pin change unasked.
82
+ **Evidence:** the version list you pinned against, and the `check_for_updates` verdict.
83
+
84
+ ### 2 — Scaffold & pins
85
+ `scaffold_world` returns the CLI scaffold command and the applicable recipe. Run it, then `npm install`
86
+ → `install_world_packages` → `npm run build`. Pick the **lowest** `helixVersion` that has
87
+ what you need (0.1 bare scene · 0.2 character · 0.3 multiplayer).
88
+ **Evidence:** `helix.lock.json` exists with resolved exact versions; `tsc --noEmit` exit 0
89
+ and `npm run build` exit 0.
90
+
91
+ ### 3 — Assets — load `helix-assets`
92
+ Sourcing happens **before** authoring, not after. A scene authored around boxes does not
93
+ get retrofitted with real assets; it gets rebuilt.
94
+ **Evidence:** `public/helix.assets.json` with an `assets[]` provenance row covering every
95
+ shipped media file (or the recorded reason each surface has no sourced asset — see the
96
+ allowed-skip list in `helix-assets`), and `helix assets check-loaders` exit 0.
97
+
98
+ ### 4 — Authoring — load `helix-world-build`
99
+ Scene, character config, lighting, materials, HUD, input, game logic.
100
+ **Evidence:** `qa/snapshot.json` from `helix world inspect`, with findings adjudicated and
101
+ `inspect.baseline.json` written. Touch controls exist, or `supportsMobile` is `false`.
102
+
103
+ ### 5 — Multiplayer — load `helix-multiplayer` (skip only for a genuinely solo world)
104
+ **Evidence:** `validate_world` accepts the `multiplayer` block, plus the two-client
105
+ verification artifact.
106
+
107
+ ### 6 — Audio
108
+ Every player action, pickup, hit, UI press, state change and ambience has a sound.
109
+ A silent world is a defect, not a style. This is its own phase because when it is a
110
+ sub-bullet of "polish" it does not happen.
111
+ **Evidence:** `qa/audio-coverage.txt` from `helix world source-audit` — coverage 100%, and every
112
+ declared cue resolves to a file that exists in `dist/`.
113
+
114
+ ### 7 — Performance — load `helix-world-qa` → G9
115
+ Its own phase for the same reason audio is: as a sub-bullet of QA it becomes a number someone
116
+ prints at the end, and two worlds shipped that way at 39 fps and 11 fps with every other gate
117
+ green. **A frame-time artifact is required to reach done** — this phase cannot be closed by
118
+ looking at a screenshot and deciding it feels smooth.
119
+
120
+ Measure the **built** world (`npm run build && npm run preview`, or the published play URL),
121
+ never `dev`:
122
+ ```bash
123
+ helix world perf-gate \
124
+ --url "http://localhost:4173/?perf=1" --label "<world> build <n>" \
125
+ --seconds 20 --dist dist --json qa/perf-gate.json | tee qa/perf-gate.txt
126
+ ```
127
+ The budgets are design constraints, not an optimisation pass — 8 punctual lights, 1 shadow
128
+ caster, a 2.6 M pixel drawing-buffer budget. If you are meeting them for the first time here,
129
+ phase 4 was built against the wrong constraints and this phase will be a rebuild, not a tune.
130
+ See `helix-world-build` → "Design inside the budget".
131
+
132
+ **Evidence:** `qa/perf-gate.json` exit 0, **naming the renderer it was measured on**. A frame
133
+ time without its GPU is not a measurement. `qa/perf-gate.txt` is the human-readable copy.
134
+
135
+ ### 8 — QA — load `helix-world-qa`
136
+ **Evidence:** `qa/world-audit.txt` and `qa/source-audit.txt`, both exit 0 — with
137
+ `source-audit --perf qa/perf-gate.json`, which is what enforces the light-count ceiling
138
+ against the lighting rules that push the other way.
139
+
140
+ ### 9 — Publish — read `read_doc("publishing")`
141
+ Follow the publishing contract from the server docs, then run the creator CLI's live identity
142
+ proof:
143
+
144
+ ```bash
145
+ helix world prove-live \
146
+ --url "<playUrl returned by publish_world>" \
147
+ --world-api "<worldUrl returned by publish_world>" \
148
+ --build "<build returned by publish_world>" \
149
+ --dist dist --perf qa/perf-gate.json | tee qa/live-proof.txt
150
+ ```
151
+
152
+ **Evidence:** the play URL, and `qa/live-proof.txt` exit 0 — which now requires a proven
153
+ readback (live build number AND/OR the deployed entry document's sha256 matching your bundle),
154
+ not merely a 200.
155
+
156
+ ## Blocking rules
157
+
158
+ These are the only things that stop a phase from being marked done, and each is checked by
159
+ an artifact, not by your opinion:
160
+
161
+ 1. `validate_world` reports errors → phases 8 and 9 blocked.
162
+ 2. `tsc --noEmit` exits non-zero → phases 4–9 blocked. `npm run build` is `vite build` and
163
+ never typechecks, so a type error ships and fails silently — including the one where the
164
+ character config sits on the wrong options key and every tuned value is ignored at
165
+ runtime. See `helix-world-build` → "Typecheck, because `vite build` does not".
166
+ 3. `helix world audit` exits non-zero (primitive-dominant, no loaded models, default-material
167
+ dominant, over budget, a bundled model requiring Draco/meshopt, unrecorded asset
168
+ provenance) → phase 8 blocked. This is where "the scaffold's box-and-plane world" gets
169
+ caught.
170
+ 4. `helix world source-audit` exits non-zero (ambient-only lighting, unmotivated local light, more
171
+ punctual lights than the measured ceiling, no tone mapping, audio coverage below 100%, a
172
+ cue with no file, a genre gate left on, `supportsMobile: true` with no touch controls) →
173
+ phase 8 blocked.
174
+ 5. `helix world perf-gate` exits non-zero (frame p50 over 16.7 ms, p95 over 33.3 ms, over 12 punctual
175
+ lights, a shadow-casting point light, a drawing buffer over 2.6 M pixels, or a run on a
176
+ software rasteriser) → phase 7 is not done, and phases 8 and 9 are blocked. **A world with
177
+ no `qa/perf-gate.json` at all is blocked too** — unmeasured is not the same as fast, and
178
+ two worlds shipped green on every other gate at 39 fps and 11 fps.
179
+ 6. A Draco- or meshopt-compressed model anywhere in the asset tree → phase 3 blocked. The
180
+ runtime has GLTFLoader + KTX2Loader only; this is a load failure at runtime, never a
181
+ warning. `helix assets check-loaders` reads both `.glb` and `.gltf`.
182
+ 7. `helix world prove-live` exits non-zero, **including when it could not prove anything, and
183
+ including a missing or stale frame-time artifact** → phase 9 is not done. Reachability is
184
+ not identity, and a green perf run from three builds ago is not this build's.
185
+ 8. `whoami` shows no creator session → phase 9 blocked on the human. This is the one
186
+ blocker you cannot clear yourself; surface it early (phase 1), not at the end.
187
+
188
+ ## Allowed reasons to skip work — a closed set
189
+
190
+ An open-ended "I judged it unnecessary" is how a world ends up as grey boxes. A phase or a
191
+ surface may be skipped only for one of these, and the ledger records which:
192
+
193
+ - The human explicitly scoped it out.
194
+ - A capability probe shows the endpoint or tool does not exist on this lane (record the
195
+ probe output, not your inference from it).
196
+ - A tool returned a real error — record the call and the error text.
197
+ - The phase is structurally inapplicable (multiplayer for a solo world; character config
198
+ for a bare scene world).
199
+
200
+ "Not needed" without one of those four is not a skip, it is an omission.
201
+
202
+ ## Report
203
+
204
+ Close with: what the world is, the play URL, the ledger table, the QA metrics that were
205
+ measured (not adjectives — the numbers `world-audit` printed), **the frame times and the GPU
206
+ they were measured on**, anything still blocked and who owns it, and — separately — anything
207
+ you changed that the human should look at.
208
+
209
+ Never claim premium quality with an automatic-failure row unresolved. Never claim a world
210
+ is published without a play URL that `helix world prove-live` returned 0 on.
@@ -0,0 +1,371 @@
1
+ ---
2
+ name: helix-world-qa
3
+ description: Prove a HELIX world actually plays, and plays at 60 fps, before it ships — mobile first, with the blocking gates (including frame time) computed by script from the world snapshot, the bundle, the source and a real GPU run rather than asserted in prose. Use before publishing, when reviewing someone else's world, when a world "looks flat", "feels unfinished", "runs badly", stutters or drops frames, and whenever a change needs a regression check.
4
+ ---
5
+
6
+ # HELIX World QA
7
+
8
+ **Mandatory before any publish, new version, listing or "done": the `helix-gauntlet` loop** (`read_skill({ name: "helix-gauntlet" })`). Bar: the named reference art or a comparable published world; renders: the fixed-camera and mobile screenshots this skill produces; your own look at each beside the bar, then a fresh blind critic. Ship only on its PASS.
9
+
10
+ Before the final GPU gate, call `analyze_scene_performance` with the exact Scene/Package and inspection snapshot. Call it again with the runtime receipt. Keep authored versus submitted triangles, visual/material/Palette counters, GPU versus compressed bytes, and owned/transitive/cold/warm delivery separate. A capped or reduced-resolution capture is not fullscreen proof; compare the same route, camera, exposure, time of day, viewport, DPR, quality, and cap. Full interpretation: `read_doc({ name: "scene-performance" })`.
11
+
12
+ QA here has one job: produce evidence that would convince someone who does not trust you.
13
+ Not a checklist you ticked — numbers a script printed, files on disk, an exit code.
14
+
15
+ Run everything from `<project>/`, write everything to `<project>/qa/`. That directory never
16
+ ships (only `public/` and the build output reach `dist/`).
17
+
18
+ ## The gates
19
+
20
+ Nine gates. Gates 1–6 and **G9** block; a blocking gate that fails means the world is not
21
+ ready, full stop. Gates 7 (beyond its touch check) and 8 are judgment, and are explicitly
22
+ marked as such — a rubric that grades itself is not a gate, and pretending otherwise is how QA
23
+ becomes theatre.
24
+
25
+ G9 used to be judgment too. It printed a frame rate and never failed on it, which is how two
26
+ worlds shipped that Jack reported as running badly with every gate green.
27
+
28
+ ### G1 — It typechecks, builds and validates *(blocking)*
29
+ ```bash
30
+ npx tsc --noEmit # exit 0 — run this FIRST
31
+ npm run build # exit 0
32
+ ```
33
+ **`npm run build` is `vite build`, which never typechecks.** It transpiles and discards
34
+ types, so a type error builds clean, ships, and fails silently at runtime. The scaffold
35
+ already defines `npm run typecheck`; nothing was making anyone run it.
36
+
37
+ This is not a style gate. The defect it catches is invisible any other way:
38
+ `CharacterMultiplayer.create` takes its tunables on **`character:`**, while
39
+ `Character.create` takes them on **`config:`**. Passing the wrong key is a type error and
40
+ nothing else — the world builds, publishes, and runs with **every tuned value silently
41
+ ignored**, including the genre control gates. A "first-person" world still flips to third
42
+ person on T, and no artifact says why. `source-audit` now flags this exact shape, but `tsc`
43
+ is what catches the whole class.
44
+
45
+ `validate_world({ directory: "<abs>/dist" })` → **VALID**. Fix every reported problem; the
46
+ validator lists them all at once, so one pass should clear them. Errors here mean publish
47
+ would reject the bundle anyway.
48
+
49
+ ### G2 — It boots *(blocking)*
50
+ ```
51
+ capture_world_screenshot({ directory: "<abs>/dist" })
52
+ ```
53
+ A successful capture proves the page booted and rendered a frame. **A failed capture is
54
+ still diagnostic** — the timeout message carries the page's boot errors. Look at the
55
+ returned image; do not just note that the call returned.
56
+
57
+ ### G3 — Layout is measured, not eyeballed *(blocking)*
58
+ ```bash
59
+ npx -y <lane-cli> world inspect ./dist --out qa/snapshot.json
60
+ ```
61
+ Use the CLI so the *tool* writes the file — that is stronger evidence than pasting MCP
62
+ output into a file yourself. `check_for_updates` reports the CLI package name for this lane.
63
+
64
+ Then adjudicate every finding. **Findings are measurements, not verdicts:** a hovering
65
+ pickup, an embedded ramp, walls overlapping at their corners all warn on good worlds. The
66
+ rule is *a warn that matches what you meant needs no fix; a warn that surprises you is the
67
+ bug.* Never edit geometry to silence a finding.
68
+
69
+ **Write the baseline. It is the normal first step, not a suggestion.** Once you have checked
70
+ the findings against intent, run `inspect_world({ directory, writeBaseline: true })` and
71
+ record the adjudication alongside it. From then on inspect reports only what **changed**,
72
+ which is what makes "this failure is pre-existing" a checkable claim instead of a convenient
73
+ one. Without a baseline file, "that was already broken" is unfalsifiable, and it is exactly
74
+ the sentence an agent reaches for when it wants to close a task.
75
+
76
+ Do not ask permission first. On any world whose premise generates findings by construction —
77
+ a floating archipelago, a zero-gravity station, anything where hovering geometry *is* the
78
+ design — the unbaselined report is dozens of warnings that all say "yes, on purpose", and an
79
+ agent reading it learns to skim inspect output instead of reading it. A dogfooded world
80
+ adjudicated **49 findings once**, wrote the baseline, and every run since reports clean; the
81
+ next real change will stand out on its own. Write the adjudication down (which findings, why
82
+ intended) next to the baseline — the baseline records *that* you judged, your note records
83
+ *what* you judged.
84
+
85
+ ### G4 — Composition and budgets *(blocking, scripted)*
86
+ ```bash
87
+ helix world audit --snapshot qa/snapshot.json --dist dist \
88
+ --root world --assets public/helix.assets.json | tee qa/world-audit.txt
89
+ ```
90
+ Exits non-zero on: the scaffold signature · zero loaded models · primitive-dominant (>60%
91
+ of visible meshes are built-in geometry) · default-white-dominant primitives · a bundle over
92
+ 200 files / 25 MB per file / 50 MB total / a disallowed extension / an illegal path / an
93
+ empty file · platform assets bundled under `helix_modules` · NaN transforms · **any bundled
94
+ `.glb` or `.gltf` requiring Draco or meshopt** · a provenance row **declared, not shipped** ·
95
+ a shipped media file **covered by no provenance row**.
96
+
97
+ The extension allowlist and the caps mirror `@hypersoniclabs/helix-manifest`'s own
98
+ bundle-rules, including the `.d.ts`-by-suffix exception (system packages ship consumer types
99
+ under `helix_modules/`; an `extname()` check reads `index.d.ts` as `.ts` and fails a file
100
+ publish accepts). The loader check parses both container forms — `.glb`'s binary header and
101
+ plain-JSON `.gltf`, which is what Poly Haven ships.
102
+
103
+ Put authored geometry under a Group named `world` (see `helix-world-build`) or the audit
104
+ falls back to the whole scene, counts the streamed character body as a loaded model, and
105
+ says so. Read that warning — it means the number is weaker than it looks.
106
+
107
+ ### G5 — Lighting, materials, audio, mobile, controls, authority *(blocking, scripted)*
108
+ ```bash
109
+ helix world source-audit --src src --dist dist \
110
+ --manifest public/helix.json \
111
+ --genre <first-person|third-person|top-down|side-on|walking-sim> [--multiplayer] \
112
+ --console qa/playtest/console.txt \
113
+ --perf qa/perf-gate.json \
114
+ | tee qa/source-audit.txt
115
+ ```
116
+ Exits non-zero on: ambient-only lighting · a `PointLight`/`SpotLight` added to the scene
117
+ root instead of its emitter mesh (a **pooled** light is exempt) · **more punctual lights than
118
+ the ceiling, measured by G9** · no `renderer.toneMapping` · no `scene.environment` /
119
+ PMREM · fewer than 60% of standard materials carrying roughness/metalness/a map · **any**
120
+ interaction site with no sound in its handler · no looping ambience · a cue name that
121
+ resolves to no shipped audio file and is not `synth:`-prefixed · a genre `allow*` gate left
122
+ at its default · **`supportsMobile: true` with no shipped v1 controls contract** · **the character
123
+ config passed on the wrong options key** · an honest fallback that **actually fired** in the
124
+ captured console · an unguarded client-side `teleport`/`respawn` in a multiplayer world.
125
+
126
+ **Runtime-lit worlds pass the renderer-owned checks by construction.** A world on the default
127
+ lighting lane — it calls `createVisualRuntime` and authors `public/helix.visuals.json` — has
128
+ no `renderer.toneMapping` write, no `scene.environment` assignment and no hand-placed sun *on
129
+ purpose*: the runtime owns all three, and `source-audit` recognises the runtime call and
130
+ treats those checks as satisfied. Do NOT add a decoy tone-mapping line or a second sun to
131
+ quiet the audit — that is exactly the double-lighting the runtime forbids (see
132
+ `read_doc({ name: "lighting-world" })`). The motivated-light rule and the punctual-light
133
+ ceiling still apply in full to world-authored local lights on BOTH lanes.
134
+
135
+ **The lighting rules here and the count ceiling are one rule, and they only work together.**
136
+ "Attach every local light to the mesh that emits it" is good art direction and, on its own, an
137
+ instruction to give every visible lantern its own real light — which is exactly how a world
138
+ ends up at 21 lights and 11 fps. `--perf` is the counter-pressure: without it the script says
139
+ `lighting.count-not-measured` and tells you nothing is pushing back.
140
+
141
+ **This script no longer counts lights, and it used to claim it did.** `lighting.census` counted
142
+ `new THREE.PointLight(` *call sites* and reported them as a census: it printed
143
+ `PointLight×4 · SpotLight×1` for a scene holding **18 point lights and 4 spot lights** — four
144
+ constructor sites, all inside loops — and `PointLight×1` for Skyward's 13. The one metric that
145
+ would have caught the defect was wrong by 4.5×, in the direction that passes. What is left is
146
+ labelled `lighting.sites` and says in its own output that it is not a count.
147
+
148
+ The audio denominator is extracted from the code — `input.wasPressed`, `events.on`,
149
+ `room.onMessage`, `sendAction`, DOM `click`/`pointerdown` handlers — not from a list you
150
+ write. That is the point: you cannot shrink the denominator by forgetting an interaction.
151
+ The window is the brace-matched handler body plus one level of named-callback resolution, so
152
+ `el.addEventListener('pointerdown', namedHandler)` is judged on `namedHandler`'s body.
153
+
154
+ **This script is static analysis and says so about itself.** Three of its checks read the
155
+ *shape* of your code rather than its effect, and each names the runtime gate that measures
156
+ the outcome: genre gates → G6's control assertion; audio coverage → G6's captured cue log;
157
+ honest fallbacks → the `--console` file from a real run. Where it warns
158
+ `…measured NOTHING`, that is not a pass — copy the line into the report.
159
+
160
+ **Never restructure code to satisfy a shape check.** A release-half pointer handler is not a
161
+ second interaction needing its own sound, and an earlier version of this gate said it was —
162
+ which taught an agent to move release handling to an event the gate does not watch. That is
163
+ better code and a worse outcome, because the gate was routed around rather than satisfied.
164
+ If a check fires on correct code, report it as a bug in the script.
165
+
166
+ ### G6 — It plays *(blocking when a browser tool is available)*
167
+ A screenshot proves it rendered. Only input proves it plays. Assert a **delta**, not a final
168
+ state — a final-state check passes on a disconnected client.
169
+
170
+ **A wall-clock delta assertion is invalid under a software rasteriser, and the default
171
+ headless browser is one.** Chromium falls back to SwiftShader unless you force a real GPU;
172
+ a modest scene then renders at ~2 fps. The scaffold clamps `dt` to 0.1 s per frame, so at
173
+ 2.3 fps the simulation advances at ~23% of wall-clock and every wall-clock measurement
174
+ under-reads by 4–8×. That is not a small error: it reported 0.7 m/s for a world configured
175
+ at `runSpeed: 6`, and a 1.41 m apex for `jumpHeight: 4.2`, and it nearly got a correct game
176
+ retuned to match a broken measurement.
177
+
178
+ Three requirements, all of them non-optional:
179
+
180
+ **1. Force a real GPU, and refuse the software fallback quietly.**
181
+ ```js
182
+ const browser = await chromium.launch({ headless: true, args: [
183
+ '--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist', // macOS; --use-angle=gl elsewhere
184
+ '--autoplay-policy=no-user-gesture-required', '--mute-audio',
185
+ '--window-position=-4000,-4000', // never steal focus
186
+ ]});
187
+ ```
188
+
189
+ **2. Measure per SIMULATED second, never per wall second.** Expose `simTime` on the probe —
190
+ the accumulated, clamped `dt` your loop actually stepped — and divide by that.
191
+ ```js
192
+ await page.waitForFunction(() => (window.__helixProbe?.frame ?? 0) > 12); // warm up first
193
+ await page.keyboard.down('KeyW');
194
+ await page.waitForTimeout(600); // let the ground-accel ramp finish
195
+ const a = await probe(page);
196
+ await page.waitForTimeout(2500); // the measured window
197
+ const b = await probe(page);
198
+ await page.keyboard.up('KeyW');
199
+
200
+ const travelled = Math.hypot(b.player.x - a.player.x, b.player.z - a.player.z);
201
+ const simDt = b.simTime - a.simTime; // NOT 2.5
202
+ const speed = travelled / simDt; // m per simulated second
203
+ expect(speed).toBeGreaterThan(0.75 * CONFIGURED_RUN_SPEED);
204
+ ```
205
+ Sample the sustained run, not the first 700 ms — the spawn drop and the acceleration ramp
206
+ both live in that window. A jump under low gravity needs a 5 s sampling loop, not one tick.
207
+
208
+ **3. Report the harness's own frame rate in the result line.** `(b.frame - a.frame) / seconds`.
209
+ It is how the next reader knows whether to believe the numbers, and how you notice the GPU
210
+ flags silently stopped working. Under ~15 fps, say the measurement is suspect.
211
+
212
+ **4. Prove the genre control gates HERE.** `source-audit` can only see that
213
+ `allowModeToggle: false` appears in your source; it cannot see whether the runtime read it
214
+ (a config block on the wrong options key is ignored silently — see G1). Press the control
215
+ and assert the state did not change:
216
+ ```js
217
+ const before = await page.evaluate(() => window.__helixProbe.cameraMode);
218
+ await page.keyboard.press('KeyT'); // the mode toggle
219
+ await page.waitForTimeout(300);
220
+ expect(await page.evaluate(() => window.__helixProbe.cameraMode)).toBe(before);
221
+ ```
222
+ Expose whatever the gate governs — `cameraMode`, zoom distance, yaw — on the probe.
223
+
224
+ **5. Drive the touch controls at a phone viewport**, in a context with
225
+ `hasTouch: true, isMobile: true`, using **only** the on-screen controls: press the stick,
226
+ assert a position delta; tap the jump button, assert an apex. A keyboard press in a mobile
227
+ context proves nothing about a phone. See G7.
228
+
229
+ **6. Confirm the render state that G5 can only guess at.** `source-audit` sees that
230
+ `renderer.toneMapping = …` appears in your source; only the running renderer knows what it
231
+ ended up as. One line on the probe turns two shape checks into measurements:
232
+ ```js
233
+ renderer: { toneMapping: renderer.toneMapping, exposure: renderer.toneMappingExposure,
234
+ environment: !!scene.environment, pixelRatio: renderer.getPixelRatio() }
235
+ ```
236
+ Assert `toneMapping !== 0` (`NoToneMapping`) and `environment === true`.
237
+
238
+ Expose `window.__helixProbe` behind a query param, the same no-op-during-play pattern
239
+ `world.ready()` and `screenshots.register()` use. Carry at least `frame`, `simTime`,
240
+ `player`, `grounded`, `cameraMode`, `renderer`, and the cues that reached a voice. Capture **all**
241
+ console output (not just errors) to `qa/playtest/console.txt` — `source-audit --console`
242
+ reads it to check whether an honest fallback actually fired. Attach screenshots to
243
+ `qa/playtest/`.
244
+
245
+ **If no browser tool is available**, say so in the report — "playability was not proven by
246
+ input; boot and render proven by capture" — and never write that you playtested it. An
247
+ honest gap is recoverable; a false claim is not.
248
+
249
+ ### G7 — Mobile *(blocking for the touch gate, judgment for the rest)*
250
+ The platform is mobile-first: most players arrive on a phone. Fresh character worlds mount the
251
+ generated `humanoid-character` touch HUD by default. It renders from registered action metadata and
252
+ provides movement, drag look, two-pointer pinch zoom, jump and contextual buttons. A world configures
253
+ or extends this system; it does not rebuild it from scratch.
254
+
255
+ - **`supportsMobile: true` requires a shipped controls contract and real touch evidence.**
256
+ `source-audit` validates `helix.controls.json` in source and dist (platform provider or an explicit
257
+ equivalent custom provider). That is the floor, not the proof: at both 390×844 and 844×390 with
258
+ `hasTouch: true`, assert touch-only movement, camera rotation, pinch zoom through first-person and
259
+ back, jump apex, and one world-specific action. A keyboard press in a mobile context proves nothing.
260
+ - Every gameplay verb is reachable without a keyboard. A keyboard-only binding is a desktop
261
+ cheat, not a control.
262
+ - The HUD clears the shell's top ~56px chrome bar. Countable: every HUD element's
263
+ `getBoundingClientRect().top` is ≥64 or it is anchored to a corner/bottom.
264
+ - Rotate portrait ↔ landscape and confirm safe-area/top-offset layout and no stuck virtual state.
265
+ - Re-run keyboard/mouse and a standard-mapped gamepad after touch QA; the HUD must not replace them.
266
+
267
+ ### G8 — Visual scorecard *(judgment — advisory)*
268
+ `references/visual-scorecard.md`. Ten categories, 0–3, with the automatic-failure list.
269
+ **Four of its rows are backed by G4/G5 and therefore actually block; the rest are your
270
+ opinion of your own screenshot and are recorded as such.** Do not present a self-graded
271
+ average as a gate — that is precisely the theatre this skill set exists to avoid.
272
+
273
+ ### G9 — Frame time *(blocking, scripted)*
274
+ ```bash
275
+ helix world perf-gate \
276
+ --url "http://localhost:4173/?perf=1" --label "<world> build <n>" \
277
+ --seconds 20 --dist dist --json qa/perf-gate.json | tee qa/perf-gate.txt
278
+ ```
279
+ Run it on the **built** world — `npm run build && npm run preview`, or the play URL
280
+ `publish_world` returned. Never `dev`.
281
+
282
+ Exits non-zero on: **frame p50 over 16.7 ms** (60 fps sustained) · **frame p95 over 33.3 ms**
283
+ (30 fps) · more than **12 punctual lights** (point + spot combined; warns over 6) · **any**
284
+ shadow-casting point light · more than one shadow-casting spot or directional light · a shadow
285
+ map over 2048² · a drawing buffer over **2.6 M pixels**, at the tested window *or at a larger
286
+ one* · over 300 draw calls or 500k triangles per frame · a software rasteriser · no light
287
+ census at all. Decoded texture bytes over 256 MB **warn** — that budget is unvalidated, and a
288
+ blocking gate on a number nobody has measured is how agents learn to optimise the wrong thing.
289
+
290
+ **This gate exists because QA already measured frame rate and threw it away.** Night Market
291
+ shipped at 25.5 ms p50 / 92.7 ms p95 with 26% of frames over 33 ms — 39 fps median, 11 fps
292
+ when the player turned around — and every gate we had passed it. Frame time was an
293
+ informational line. A world could run at 10 fps and be green.
294
+
295
+ Where the numbers come from and what each limit does and does not mean:
296
+ `references/perf-budgets.md`. The short version:
297
+
298
+ | Limit | Value | What it is |
299
+ | --- | --- | --- |
300
+ | frame p50 / p95 | 16.7 ms / 33.3 ms | **the outcome.** Everything else is a cause. |
301
+ | punctual lights (point + spot) | warn 6 · fail 12 · target 8 | **the cause.** No light culling in three.js: every light is evaluated per lit pixel, and the cost goes vertical between 12 and 16. The 21st light costs 13× the 4th. |
302
+ | shadow casters | 1 directional · 0 point · ≤1 spot | a shadow-casting point light renders the scene **six times per frame** |
303
+ | shadow map size | ≤2048², 1024² recommended | **a MEMORY limit, not a time limit.** 512 vs 1024 measured as noise. Say so, or someone shrinks it expecting frames back. |
304
+ | drawing-buffer pixels | ≤2.6 M, as a **derived ratio** | the largest single lever (69%) — and a fixed DPR cap silently becomes a cliff on a bigger window |
305
+ | draw calls / triangles | ≤300 · ≤500k per frame | **CPU-submission guards for weaker machines.** 418 calls and 778k triangles measured as costing *nothing* here. Do not mistake these for the thing that matters. |
306
+ | decoded texture bytes | ≤256 MB | **UNVALIDATED** — never observed to cost frame time on unified memory. A memory precaution, not a diagnosis. |
307
+
308
+ **Measurement discipline, or the numbers lie.** All three are in the script; know why:
309
+ 1. **Force a real GPU** (`--use-angle=metal` on macOS, `--use-angle=gl` elsewhere). Headless
310
+ Chromium falls back to SwiftShader, and the gate **refuses to report** numbers from a
311
+ software rasteriser rather than printing a frame time for a CPU.
312
+ 2. **`gl.finish()` once per frame.** WebGL draw calls are asynchronous; without a sync the CPU
313
+ runs ahead of the GPU and rAF deltas measure JS. Skyward read **562 fps** when it wasn't.
314
+ 3. **Report the renderer string next to the result.** A frame time without the GPU that
315
+ produced it is not interpretable, and it is how you notice the flags stopped working.
316
+
317
+ The world should ship the `?perf=1` handle (`references/perf-handle.md`). Without it the gate
318
+ still measures everything at the WebGL level — including the light count, read out of the light
319
+ arrays three.js compiled into its own fragment shader — but shadow-map size and per-light names
320
+ come back `measured NOTHING`.
321
+
322
+ **Then feed the artifact back into G5:** `helix world source-audit --perf qa/perf-gate.json`. Every
323
+ lighting rule in G5 pushes toward *more* lights; the measured count is the only thing pushing
324
+ back.
325
+
326
+ **One change, then re-measure the same scenario.** A batch of seven optimisations tells you
327
+ nothing about which one worked, and one of them is usually a regression. To attribute a frame
328
+ to a cause, run an ablation ladder against the `?perf=1` handle — recipe in
329
+ `references/perf-handle.md`.
330
+
331
+ ## Baselines are files, not memory
332
+
333
+ Every regression claim needs a stored baseline or it is not a claim:
334
+ - Layout → `inspect.baseline.json` (written by `inspect_world({ writeBaseline: true })`).
335
+ - Composition/budgets → the previous `qa/world-audit.txt`, committed.
336
+ - Source gates → the previous `qa/source-audit.txt`, committed.
337
+ - Frame time → the previous `qa/perf-gate.json`, committed. It carries the scenario and the
338
+ renderer string, so a regression claim can be checked against the same GPU at the same
339
+ viewport rather than against a number someone remembers.
340
+
341
+ Diff the new run against the committed one. "Pre-existing" means it appears in the committed
342
+ baseline. Nothing else counts.
343
+
344
+ ## Order of operations that saves a pass
345
+
346
+ Measure before you look. `inspect_world` answers *where things are* exactly; a screenshot
347
+ measurably cannot, and "is the bench 0.4 m in the air" read off an image is a coin flip.
348
+ Screenshots answer *how it reads* — framing, materials, lighting, mood — and that is the
349
+ closing check, after the numbers are already right. Each capture costs a build plus several
350
+ seconds, so one well-chosen angle beats a burst.
351
+
352
+ ## Report
353
+
354
+ ```text
355
+ QA — <world> (<slug>)
356
+ G1 typecheck+build pass/FAIL tsc --noEmit exit 0; validate_world: VALID, 47 files, 3.1 MiB
357
+ G2 boot pass/FAIL qa/screenshots/hero.png
358
+ G3 layout pass/FAIL qa/snapshot.json — 3 findings, all adjudicated (baseline written)
359
+ G4 composition pass/FAIL qa/world-audit.txt — 15 meshes, 20% primitive, 12 loaded, 28,806 tris
360
+ G5 source gates pass/FAIL qa/source-audit.txt — audio 14/14, 4 lights, 6/7 PBR materials
361
+ G6 playability pass/FAIL/not-proven qa/playtest/ — 5.8 m/s over 2.5 s SIMULATED (config 6), harness at 41 fps, T did not change camera mode, 0 console errors
362
+ G7 mobile pass/FAIL for touch (thumbstick delta + jump apex), notes + screenshot path
363
+ G8 scorecard average, and the automatic failures still standing
364
+ G9 frame time pass/FAIL qa/perf-gate.txt — 2.5 ms p50 (400 fps) / 5.7 ms p95, 6 punctual lights, 1 shadow caster, 2.60 M buffer px, 184 calls / 366k tris, on ANGLE Metal (Apple M1 Pro)
365
+ ```
366
+
367
+ The renderer string is part of the G9 line, not a footnote. "400 fps" means nothing without
368
+ the GPU that produced it.
369
+
370
+ Numbers, not adjectives. If a gate did not run, write **not-proven** and why — never leave
371
+ the reader to infer that a blank means pass.