@aeonsparx/pi-unreal 1.0.0 → 1.1.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers follow
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every version bump in `package.json`
5
+ adds an entry here; release notes stay auto-generated from commits.
6
+
7
+ Rule for bump class (repo policy): new tools, new parameters and new answer types are **minor**;
8
+ **patch** is fixes with the same surface; **major** is a breaking surface change — a removed tool,
9
+ a renamed parameter, or answer text an existing consumer parses.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [1.1.0] - 2026-10-28
14
+
15
+ Closes the field findings in `docs/field-findings-2026-10-27.md` (measured on a UE 5.8.3 installed
16
+ engine against a project with 25 enabled plugin references).
17
+
18
+ ### Added
19
+
20
+ - `unreal_index` `action: "headers"` — a path-only header index answering "does this header exist in
21
+ this engine version?": exact path, owning module, owning plugin, `EnabledByDefault`, and whether
22
+ the `.uproject` references it. Invented names come back as *not a file path* plus the closest real
23
+ file names, labelled as files and never as types. Never built implicitly; keyed by its own root set
24
+ (`lib/store.ts` `buildHeaderIndex`, `lookupHeaderPaths`, `suggestHeaderPaths`; `lib/tools.ts`
25
+ `headerReport`, `headerLookup`).
26
+ - Engine-scope indexes include the project's enabled plugin roots, and `engineAreas` accepts engine
27
+ plugin dials: `["Plugins"]` (every `Engine/Plugins` source tree, ~2.8x the `Runtime` scan) or
28
+ `["Plugins/Animation/PhysicsControl"]` (one plugin).
29
+ - Every answer states what it scanned. A 0-row symbol answer names the index keys, the root kinds
30
+ (`279 engine-source root(s), 62 engine-plugin root(s)`), the plugins the project does not reference
31
+ whose names match the query, and the exact next call (`lib/tools.ts` `zeroResultNotes`,
32
+ `indexCoverageNote`, `rootKindsNote`). `unreal_index` `action: "status"` reports the same per index,
33
+ plus the header indexes.
34
+ - `unreal_context` `module: "X"` reports per-target availability: whether the target's module graph
35
+ contains it, its module directory, the declared module `Type`, its owning plugin with
36
+ `EnabledByDefault`, and the `.uproject` reference status as separate facts
37
+ (`lib/tools.ts` `moduleAvailability`).
38
+ - `unreal_symbol` exposes `substring: true` (the substring pass already existed in `lookupSymbol`;
39
+ it was unreachable from the tool).
40
+ - `unreal_project_inspect` / `unreal_plugin_inspect` name the modules a resolved plugin reference
41
+ buys — `Mover: resolved ← Engine · modules Mover(Runtime), MoverCVDData(Runtime),
42
+ MoverEditor(UncookedOnly)` — and list engine plugins available with **no** `.uproject` reference
43
+ (`EnabledByDefault: true`), separated from the references.
44
+
45
+ ### Changed
46
+
47
+ - Index format v6 (`INDEX_VERSION = 6`): `meta.rootKinds` + `meta.areas`. Engine-scope index keys
48
+ change because engine roots now include enabled plugin roots, so v5 engine indexes are not reused
49
+ (one rebuild per machine; project indexes are unaffected).
50
+ - Definitions-mode module attribution names modules after their `*.Build.cs` file, not the directory
51
+ leaf: `Engine/Plugins/Animation/PoseSearch/Source/Runtime` is module `PoseSearch`, not `Runtime`.
52
+ Multi-`Build.cs` directories are disambiguated by the `.uplugin` module names
53
+ (`store.ts` `moduleFromBuildCs`, `buildCsNames`, `pickModuleName`; `assembleRoots` root modules).
54
+ This is what makes `unreal_symbol module: "PoseSearch"` answer in `definitions` mode.
55
+ - Index writes and whole-file passes are streamed: `buildIndex` sorts definition rows per 40 000-row
56
+ shard and k-way merges them into `defs.tsv`, and the linear passes (substring lookup,
57
+ symbols-in-file, module-scope recovery) read the TSV in 4 MB chunks. The whole-Plugins engine scope
58
+ is 1.24 M rows / ~250 MB and previously died in the allocator at 1.91 GB
59
+ (`ReachJSValueFormalMemoryCommit Allocation failed: out of memory`).
60
+ - `unreal_context` `file:` derives scope from path shape (`Engine/Plugins/...` is engine scope), and
61
+ a miss falls back to the header index: "this file exists, in plugin X, `EnabledByDefault` false,
62
+ not referenced by your `.uproject`, here is the call that indexes it" — instead of "not in index".
63
+ - `unreal_index` `action: "compiledb"` with `scope: "project+engine"` now includes `Engine/Plugins`
64
+ sources, where plugin `.cpp` bodies live.
65
+
66
+ ### Fixed
67
+
68
+ - `engineAreas` schema wording: `["Runtime"]` is the engine's own gameplay code, not every gameplay
69
+ type — plugin-shipped types were unreachable and a 0 read as "the engine has no such type".
70
+ - `test/live-check.ts` no longer defaults to a machine-local `.uproject` path (it resolved to
71
+ nothing on CI) and prints a usage line instead.
72
+
73
+ ### Documentation
74
+
75
+ - `README.md` and `docs/internals.md`: engine/plugin coverage and its measured cost, the header index
76
+ and the path-vs-symbol distinction, "an empty clangd answer is evidence", and the asset boundary —
77
+ pi-unreal answers C++ declaration/module/include/dependency facts and does not answer UENUM index
78
+ order, UStruct field types, UCLASS/CDO values or Blueprint graphs (that needs the `unreal-engine`
79
+ editor MCP with the editor running).
80
+
81
+ ## [1.0.0]
82
+
83
+ - Initial release: `unreal_resolve_engine`, `unreal_project_inspect`, `unreal_plugin_inspect`,
84
+ `unreal_symbol`, `unreal_symbol_list`, `unreal_context`, `unreal_index` (compiler-free symbol
85
+ index, UnrealBuildTool Query facts, clangd compile-database enrichment).
86
+
87
+ [Unreleased]: https://github.com/Aeonsparx-Interactive/pi-unreal/commits/main
88
+ [1.1.0]: https://github.com/Aeonsparx-Interactive/pi-unreal/releases/tag/v1.1.0
89
+ [1.0.0]: https://github.com/Aeonsparx-Interactive/pi-unreal/releases/tag/v1.0.0
package/README.md CHANGED
@@ -10,6 +10,32 @@ the owning module. No compiler, no Visual Studio, no Rider, no clangd required.
10
10
  It is **read-only**: it never writes to your project, never touches the Windows registry, never
11
11
  launches the editor, and blocks three kinds of edit that damage Unreal work.
12
12
 
13
+ ## Usage
14
+
15
+ ### Requirements
16
+
17
+ - **Windows**, with **PowerShell 7** (`pwsh`) for registry-based engine discovery. Without it the
18
+ extension still works: resolution falls back to `UNREAL_ENGINE` / `UE_ROOT` /
19
+ `PI_UNREAL_ENGINE_ROOT`, the Launcher's install list, and a scan of
20
+ `C:/Program Files/Epic Games`.
21
+ - An Unreal Engine installation the project can resolve to (Launcher install or source build).
22
+ Descriptor field handling is transcribed from UE 5.8.
23
+ - Nothing else. No compiler, no IDE language service. clangd is optional enrichment for
24
+ `compiledb` and the `clangd --check` acceptance step.
25
+
26
+
27
+ ### Installation
28
+
29
+ ```bash
30
+ pi install npm:@aeonsparx/pi-unreal # optionally pin to a specific version, reproducible
31
+ pi install git:github.com/Aeonsparx-Interactive/pi-unreal@v1.1 # follows patch releases on the v1.1 line
32
+ ```
33
+
34
+ The `npm:` route installs from the registry and pins an exact version. The `git:` route clones the
35
+ repository and checks out the ref: a line alias (`@v1.1`) is a moving tag that patch releases
36
+ force-move, so `pi` picks up patches on update; pin `@v1.1.0` for a fixed snapshot. Loading straight
37
+ from a clone still works and needs no install: `pi -e ./extensions/unreal/index.ts`.
38
+
13
39
  ## What you get in a session
14
40
 
15
41
  **Context is detected for you.** When the session starts in a directory that contains a
@@ -29,8 +55,8 @@ project?", "Where is `FLpMovementDirectionThresholds` defined?", "What do I need
29
55
  | `unreal_plugin_inspect` | One `.uplugin` as the engine parses it: modules, loading phases, version fields, plugin dependencies |
30
56
  | `unreal_symbol` | "Where is this class / struct / function / `UPROPERTY` / delegate declared?" One line per hit: kind, file:line, module |
31
57
  | `unreal_symbol_list` | "What does this project define?" Names by prefix — `ULp`, `ANiMy`, `UAnim` |
32
- | `unreal_context` | "What does this file/module include, define, and depend on?" Include paths, dependency lists, the real `WITH_*` defines |
33
- | `unreal_index` | Builds and reports the source index, and the clangd compile database on request |
58
+ | `unreal_context` | "What does this file/module include, define, and depend on?" Include paths, dependency lists, the real `WITH_*` defines; for a module, also whether the target's module graph contains it and how the `.uproject` treats its plugin |
59
+ | `unreal_index` | Builds and reports the source index, the path-only **header index** ("does this header exist in this engine version?"), and the clangd compile database on request |
34
60
 
35
61
  **Slash commands you can type yourself:**
36
62
 
@@ -53,11 +79,12 @@ modules: Lp (Runtime), LpEditor (Editor)
53
79
  plugins declared: 25
54
80
  plugin index: 897 .uplugin (cache hit)
55
81
  plugin references:
56
- MassGameplay: resolved ← Engine
57
- ModularGameplay: resolved ← Engine
58
- SomeLocalPlugin: resolved ← Project
82
+ Mover: resolved ← Engine · modules Mover(Runtime), MoverCVDData(Runtime), MoverEditor(UncookedOnly)
83
+ MassGameplay: resolved ← Engine · modules MassEntity(Runtime), MassCore(Runtime), MassSimulation(Runtime), …
84
+ SomeLocalPlugin: resolved ← Project · modules SomeLocalPlugin(Runtime)
59
85
  RetiredPlugin: missing
60
86
  totals: 22 resolved, 3 missing of 25 references
87
+ engine plugins available with NO .uproject reference (EnabledByDefault true): GameplayCameras, IKRig
61
88
  ```
62
89
 
63
90
  Example — `unreal_symbol` for an engine type (engine index already built):
@@ -70,6 +97,36 @@ unreal_symbol 'AActor' scope=engine: 1 definition(s)
70
97
  That is a definition line, not a grep dump: grepping a hot engine class name returns ~176 files of
71
98
  noise, which is exactly what burns a session's context.
72
99
 
100
+ Example — `unreal_index` `action: "headers"`, the question a symbol index cannot answer:
101
+
102
+ ```text
103
+ unreal_index headers → hdr-UE_5.8-1a40c1c3d516
104
+ 37,788 header paths from 1,934 roots (1,655 engine-plugin root(s), 279 engine-source root(s))
105
+ 'MoverState.h': does NOT exist as a file path in this engine version
106
+ closest real file names (files, not types):
107
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MoverTypes.h [Mover]
108
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MoverSimulationTypes.h [Mover]
109
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MovementMode.h [Mover]
110
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MovementModeStateMachine.h [Mover]
111
+ ```
112
+
113
+ `MoverState.h` is the kind of name an agent invents. The answer says it is not a file, lists real
114
+ files, and never presents a file name as a type. The same call on
115
+ `PhysicsControlComponent.h` returns the path plus `plugin PhysicsControl [Engine] · EnabledByDefault
116
+ false · .uproject reference NO` — the file exists, the project does not reference the plugin, and
117
+ those are different facts.
118
+
119
+ Example — the coverage lines a 0-row answer prints (nothing here says "the engine has no such type"):
120
+
121
+ ```text
122
+ unreal_symbol 'PhysicsControlComponent' scope=engine: 0 definition(s)
123
+ · no definitions matched 'PhysicsControlComponent' in scope 'engine'
124
+ · scanned engine index idx-engine-defs-b8d82464af4a: 279 engine-source root(s), 62 engine-plugin root(s)
125
+ · engine plugins on disk: 897, referenced by this .uproject: 25; the engine index scans referenced plugins only
126
+ · a plugin the project does not reference is not in any index root: pass engineAreas ["Plugins"] (…2.8× the Runtime scan) or ["Plugins/Animation/PhysicsControl"]
127
+ · next: unreal_symbol query 'PhysicsControlComponent' scope "all" prefix true, or unreal_index action "headers" name "PhysicsControlComponent.h"
128
+ ```
129
+
73
130
  ## Guardrails (automatic, nothing to enable)
74
131
 
75
132
  | Situation | What happens |
@@ -94,8 +151,18 @@ Because of that model:
94
151
  - Symbol lookups never stall. If a question needs engine sources and no engine index exists, the
95
152
  agent reports the cost and how to pay it, and asks you. Say **yes** once per machine —
96
153
  `/unreal-cache stats` shows what you have. Engine scans default to `engineAreas: ["Runtime"]`,
97
- which is enough for gameplay base classes (`AActor`, `UAnimInstance`, `USceneComponent`);
98
- `Editor` and `Developer` are opt-in and roughly triple the scan.
154
+ which is the engine's **own** gameplay code (`AActor`, `UAnimInstance`, `USceneComponent`);
155
+ `Editor` and `Developer` are opt-in and roughly triple the scan. An engine-scope index always
156
+ includes the plugin roots your `.uproject` enables, so `UMoverComponent` and the PoseSearch types
157
+ are in it. Types shipped by plugins you do **not** reference (`PhysicsControl`, `BlendStack`,
158
+ `StateTree`, `GameplayAbilities`) need `engineAreas: ["Plugins"]` — every `Engine/Plugins`
159
+ source tree, ~2.8× the `Runtime` scan — or `engineAreas: ["Plugins/Animation/PhysicsControl"]`
160
+ for one plugin.
161
+ - "Does this header exist in this engine version?" is a path question, not a symbol question:
162
+ `unreal_index` with `action: "headers"` (and `name: "MoverState.h"`) answers it in seconds from a
163
+ path walk, and says which plugin owns a header it finds, whether that plugin is
164
+ `EnabledByDefault`, and whether your `.uproject` references it. Invented names come back as
165
+ *not a file path* plus the closest real **file** names — never as types.
99
166
  - Ask for `unreal_index` with `action: "build"`, `scope: "engine"` when you want engine-wide
100
167
  navigation, and `action: "compiledb"` when you want clangd's references/call hierarchy.
101
168
  - After a branch switch, a rebase, adding a plugin, or an engine version change, say "rebuild the
@@ -104,17 +171,6 @@ Because of that model:
104
171
  inside your project tree or your engine installation. The compile database encodes this
105
172
  machine's absolute paths, so it is machine-local by design.
106
173
 
107
- ## Requirements
108
-
109
- - **Windows**, with **PowerShell 7** (`pwsh`) for registry-based engine discovery. Without it the
110
- extension still works: resolution falls back to `UNREAL_ENGINE` / `UE_ROOT` /
111
- `PI_UNREAL_ENGINE_ROOT`, the Launcher's install list, and a scan of
112
- `C:/Program Files/Epic Games`.
113
- - An Unreal Engine installation the project can resolve to (Launcher install or source build).
114
- Descriptor field handling is transcribed from UE 5.8.
115
- - Nothing else. No compiler, no IDE language service. clangd is optional enrichment for
116
- `compiledb` and the `clangd --check` acceptance step.
117
-
118
174
  ## Troubleshooting
119
175
 
120
176
  | Report | What to do |
@@ -122,82 +178,23 @@ Because of that model:
122
178
  | `engine: AMBIGUOUS — more than one valid installation matches` | Two roots match the association. Pass the one you want as `engineRootHint` (e.g. `engineRootHint: "C:/Program Files/Epic Games/UE_5.8"`). |
123
179
  | `engine: UNRESOLVED` with rejected candidates | Read the reasons — a stale registry key, a missing `Build.version`, a version mismatch. The report lists remediation steps; the extension never writes registry keys for you. |
124
180
  | `no .uproject within 4 levels above …` | The search walks up from the working directory. Pass the project explicitly: `project: "C:/Work/Lp/Lp.uproject"` (a directory containing one works too). |
125
- | `unreal_symbol` finds nothing in scope `engine` | Build the engine index first (`unreal_index` → `action: "build"`, `scope: "engine"`). |
181
+ | `unreal_symbol` finds nothing in scope `engine` | Read the coverage lines the answer prints: they name the root kinds that were scanned. A plugin your `.uproject` does not reference is in no index root — `engineAreas: ["Plugins"]` (or one plugin subtree), then rebuild. |
182
+ | `unreal_symbol` finds nothing and you are not sure the name exists | `unreal_index` → `action: "headers"`, `name: "ThatName.h"`: a seconds-long path answer with the closest real file names. |
126
183
  | Results look stale after a rebase or engine update | Re-run with `refresh: true`, or `/unreal-cache clear`. |
127
184
  | `no plugin named "X" in the index` | That reference is genuinely not on disk. `/unreal` shows which roots were scanned. |
128
185
 
129
- ## What it deliberately does not do
186
+ ### What it deliberately does not do
130
187
 
131
188
  - It does not install, register, migrate, or "fix" an engine, and never writes registry keys.
132
189
  - It does not build, cook, package, launch the editor, or edit `.ini` files.
133
190
  - It does not hand-edit assets, and blocks attempts to.
134
191
  - Symbol lookup reports **declarations**. Type resolution, overloads, call sites, inheritance and
135
- diagnostics are clangd's job when clangd is installed.
192
+ diagnostics are clangd's job when clangd is installed. An empty clangd answer is evidence: on an
193
+ installed engine the compile database has no engine translation units, so "clangd found nothing"
194
+ in engine code is a property of the setup, not a bug this package fixes.
195
+ - A 0-row answer is a statement about **the roots that were scanned**, never about the engine. The
196
+ answer names its index keys and root kinds; `unreal_index` → `action: "status"` prints the same.
197
+ - pi-unreal answers C++ **declaration, module, include-path and dependency** facts. It does not
198
+ answer asset questions — UENUM index order, UStruct field types, UCLASS/CDO instance values,
199
+ Blueprint graphs. Those need the `unreal-engine` editor MCP with the editor running.
136
200
  - `.ini` provenance, live-coding state, and asset metadata are out of scope for v1.
137
-
138
- ## Install
139
-
140
- Published as **`@aeonsparx/pi-unreal`** on npmjs.
141
-
142
- ```bash
143
- pi install npm:@aeonsparx/pi-unreal@1.0.0 # pinned version, reproducible
144
- pi install git:github.com/Aeonsparx-Interactive/pi-unreal@v1.0 # follows patch releases on the v1.0 line
145
- ```
146
-
147
- The `npm:` route installs from the registry and pins an exact version. The `git:` route clones the
148
- repository and checks out the ref: a line alias (`@v1.0`) is a moving tag that patch releases
149
- force-move, so `pi` picks up patches on update; pin `@v1.0.2` for a fixed snapshot. Loading straight
150
- from a clone still works and needs no install: `pi -e ./extensions/unreal/index.ts`.
151
-
152
- ## Releasing (maintainers)
153
-
154
- | `package.json` bump | GitHub Release | npmjs |
155
- | --- | --- | --- |
156
- | `major` / `minor` | **new** release `v<major>.<minor>` | staged `<major>.<minor>.<patch>` |
157
- | `patch` | **updates** the existing `v<major>.<minor>` release | staged `<major>.<minor>.<patch>` |
158
-
159
- Merge a PR that bumps `version` into `main`. `.github/workflows/release.yml` runs `bun test` on
160
- Linux and Windows, pushes the immutable tag `v<major>.<minor>.<patch>`, moves the `v<major>.<minor>`
161
- alias, creates or updates that release, then stages the tarball with `npm stage publish`. **Nothing
162
- is public until a human approves it** (2FA, from any machine):
163
-
164
- ```bash
165
- npm stage list @aeonsparx/pi-unreal # stage ids (no 2FA)
166
- npm stage view <stage-id> # metadata
167
- npm stage download <stage-id> # inspect the exact tarball that will publish
168
- npm stage approve <stage-id> # 2FA - this is the publish act
169
- npm stage reject <stage-id> # 2FA - frees the staged version number
170
- ```
171
-
172
- The same queue is on npmjs.com (Staged Packages tab). `npm stage` requires npm >= 11.15.0
173
- (`npm install -g npm@12`) and Node >= 22.14.0. `NPM_TOKEN` (repository secret) is a Granular Access
174
- Token with Read+Write and **no** 2FA bypass: CI can stage, only you can publish. Staged versions
175
- occupy their version number, so a rejected stage burns `x.y.z` until `npm stage reject` frees it.
176
- Provenance is deliberately off in v1 (`--provenance` needs a Provenance-scope token, and the
177
- attestation path through stage-then-approve is unverified).
178
-
179
- ## Developers
180
-
181
- ```bash
182
- bun test # 66 tests: resolver, descriptors, plugins, cache, gate, scanner, index, compile-db pipeline, fixture hygiene
183
- bun run typecheck # tsc --noEmit
184
- bun test/live-check.ts "C:/path/to/Project.uproject" # live machine smoke
185
- bun test/keys.census.mjs "C:/Program Files/Epic Games/UE_5.8/Engine/Plugins"
186
- bun run tmp/smoke.mjs project "C:/path/to/Project" # symbol index against a real project
187
- bun run tmp/smoke.mjs query "C:/path/to/Project" # UnrealBuildTool Query round trip
188
- bun run tmp/smoke.mjs engine "C:/path/to/Project" # Engine/Source index (minutes, opt-in)
189
- bun run tmp/smoke.mjs compiledb "C:/path/to/Project" # clangd database + `clangd --check` acceptance
190
- ```
191
-
192
- From a clone, load the extension into a session without installing it:
193
-
194
- ```bash
195
- pi -e ./extensions/unreal/index.ts
196
- ```
197
-
198
- Layout: `extensions/unreal/index.ts` (tools, commands, context card, convention gate),
199
- `extensions/unreal/lib/` (resolver, descriptors, plugin index, symbol index, UBT Query, compile
200
- database), `prompts/unreal-context.md`, `skills/ue-engine-resolution/`, `test/`.
201
-
202
- Design notes and measured internals — engine resolution algorithm, descriptor fidelity, plugin
203
- discovery, the index and compile-database pipeline — are in [docs/internals.md](docs/internals.md).
package/docs/internals.md CHANGED
@@ -17,19 +17,34 @@ compiler-free sources:
17
17
  through RiderLink, and both derive their context from the same UBT rules this layer reads.
18
18
  2. **A regex definition scanner** over the project, its enabled plugins, and the engine areas its
19
19
  modules actually depend on — `class`/`struct`/`enum`/`union`, `UFUNCTION`, `UPROPERTY`,
20
- delegates, aliases, with `Owner:Member` qualified names and forward declarations marked.
20
+ delegates, aliases, with `Owner:Member` qualified names and forward declarations marked. In an
21
+ installed engine "engine areas" is not where the gameplay types are: `Engine/Source/Runtime` is the
22
+ engine's own code, and plugin-shipped types live under `Engine/Plugins/<Category>/<Plugin>/Source/`,
23
+ so an engine-scope index takes its plugin roots from the `.uproject` (plus an explicit
24
+ `engineAreas: ["Plugins"]` dial for the rest).
21
25
 
22
26
  Artifacts live in `$PI_UNREAL_CACHE_DIR/indexes/<key>/` (`meta.json`, `defs.tsv`, `files.tsv`,
23
27
  plus `includes.tsv` in `full` mode): plain TSV — greppable, inspectable, shared across sessions
24
28
  and projects — and never inside the project tree or the engine installation.
25
29
 
30
+ Writes and whole-file passes are **streamed**, because the whole-Plugins engine index is 1.24 M rows
31
+ (~250 MB of TSV): `buildIndex` sorts rows per 40 000-row shard and k-way merges them into `defs.tsv`
32
+ (peak memory is one shard, output is identical to a single sorted pass), and the linear passes
33
+ (substring lookup, symbols-in-file, module scope recovery) read the TSV in 4 MB chunks and stop when
34
+ the answer is full. An earlier build accumulated every row in an array and died in the allocator at
35
+ 1.91 GB (`ReachJSValueFormalMemoryCommit Allocation failed: out of memory`) — which is the failure
36
+ mode of "just load the index" at engine scale.
37
+
26
38
  Measured on this box (UE 5.8.3 installed engine; Lp project with 25 enabled plugins):
27
39
 
28
40
  | Scope | Files | Definitions | Cold build |
29
41
  | --- | --- | --- | --- |
30
42
  | Project + plugins | 2,405 | 38,901 | 6.7 s |
31
43
  | Engine `Runtime` (shipped default: 279 module roots) | 18,566 | 436,729 | 57.6 s cold, 5.7 s warm |
44
+ | Engine `Runtime` + 62 enabled plugin roots | 20,966 | 476,327 | 122 s cold, 0.4 s warm |
45
+ | Engine `Runtime` + every `Engine/Plugins` source | 71,004 | 1,240,246 | ~2 min cold (1,996 roots: 279 engine-source + 1,717 engine-plugin) |
32
46
  | Engine `Runtime/Core` + `Runtime/CoreUObject` | 3,094 | 70,548 | 16 s |
47
+ | Header index (paths only, engine `Runtime` + all `Engine/Plugins`) | 37,788 headers | — | 4.5 s cold, 0.5 s warm |
33
48
  | Cached lookup | — | — | ~0.3 s |
34
49
 
35
50
  Cost discipline is the design. A lookup builds the project index (seconds) and uses an engine
@@ -43,8 +58,36 @@ Forward declarations rank last, so `unreal_symbol AActor` returns
43
58
  the `class AActor;` line in `PhysicsCollision.h`.
44
59
  `unreal_symbol LpGameModeBase` finds `ALpGameModeBase` (the UE prefix is added for you); `unreal_symbol FLpMovementDirectionThresholds:F` finds a single-letter `UPROPERTY` field; `unreal_context LpCharacterTypes.h` resolves a bare file name through the index. Engine areas are derived from module
45
60
  dependencies (`.uproject` modules → recursive `PublicDependencyModuleNames` → plugin-supplied
61
+ modules → recursive `PublicDependencyModuleNames` → plugin-supplied
46
62
  modules → `Engine/Source/<area>/*`), so the index contains what this project can compile against,
47
- not the whole engine.
63
+ not the whole engine. An **engine-scope** index is `Engine/Source/<area>` roots *plus the plugin
64
+ roots the `.uproject` enables* — in UE 5.8 most gameplay types ship as plugins (`Mover`,
65
+ `PoseSearch`, `SmartObjects`, `BlendStack`, `StateTree`, `GameplayAbilities`), and an index of
66
+ `Engine/Source/Runtime` alone reports 0 rows for `UMoverComponent` while the project index already
67
+ contains it. Engine plugins the project does **not** reference are in no root: `engineAreas:
68
+ ["Plugins"]` adds every `Engine/Plugins` source tree (~2.8× the `Runtime` scan),
69
+ `["Plugins/Animation/PhysicsControl"]` adds one.
70
+
71
+ Every answer states what it scanned: index key, root kinds (`279 engine-source root(s),
72
+ 62 engine-plugin root(s)`), and the `engineAreas` it was built with. A 0-row answer is a statement
73
+ about those roots, never about the engine, and it prints the exact next call.
74
+
75
+ ## Header index (`unreal_index` action `headers`)
76
+
77
+ "Does this header exist in this engine version?" is a path question that a definitions index cannot
78
+ answer: absence of a symbol row is not absence of a file, and the index roots exclude every plugin
79
+ the project does not enable. `buildHeaderIndex` walks file **paths** only — no content read — over
80
+ the same root assembly (`Engine/Source/<area>`, `Engine/Plugins/...`, project sources), writes
81
+ `headers.tsv` (`file \t module \t kind`) beside the symbol indexes, and answers exists / exact path
82
+ / owning module / owning plugin / `EnabledByDefault` / referenced-in-`.uproject`. Measured: 37,788
83
+ header paths in 4.5 s on a UE 5.8 install (`Runtime` + all of `Engine/Plugins`).
84
+
85
+ It is never built implicitly (a path answer must not cost a scan nobody asked for), its key encodes
86
+ its own root set, and when the name does not exist the answer lists the closest real **file** names
87
+ labelled as files. That is the discipline behind it: an invented name like `MoverState.h` must not
88
+ become a confident type suggestion — the old workflow produced `Mover/Public/InputContainerStruct.h`
89
+ (a real file, a bogus answer) for `FCharacterDefaultInputs`, and the types
90
+ `FPoseSearchBlendPose(History)` do not exist in 5.8 at all.
48
91
 
49
92
  One engine fact worth knowing: UBT Query reports `"defines": []` for every module. The effective
50
93
  preprocessor set lives in a forced include —
@@ -169,7 +212,8 @@ Field names, host types, and loading phases are transcribed from the engine's se
169
212
  candidate, not a guarantee).
170
213
  - `moduleRefs` and `includePaths` come from UnrealBuildTool Query, which needs a buildable
171
214
  target for the project; when Query fails the index still builds and says so.
172
- - The engine index is never built implicitly: scans are opt-in through `unreal_index`.
215
+ - The engine index is never built implicitly: scans are opt-in through `unreal_index`. The header
216
+ index is never built implicitly either, for the same reason: a path answer must not cost a scan.
173
217
  - `action: "compiledb"` needs UnrealBuildTool (so an engine installation) and, for the
174
218
  `clangd --check` acceptance step, a clangd binary — `CLANGD_PATH`, the Visual Studio LLVM
175
219
  toolset, the WinGet LLVM package, or `C:/Program Files/LLVM/bin`. Without clangd the database is
@@ -177,8 +221,25 @@ Field names, host types, and loading phases are transcribed from the engine's se
177
221
  - On an installed engine the compile database covers the project's translation units (engine
178
222
  modules are precompiled and contribute no compile actions). The engine-wide scope of
179
223
  `project+engine` is the source-built case, and there it is a ~1 GB artifact taking minutes.
224
+ `project+engine` now includes `Engine/Plugins` sources as well as `Engine/Source` — that is where
225
+ plugin `.cpp` bodies live, and a database without them silently drops every plugin implementation
226
+ file from clangd.
227
+ - An empty clangd answer is **evidence, not a bug to route around**. On an installed engine the
228
+ database has no engine translation units, so "clangd found no references in
229
+ `MoverComponent.cpp`" describes the setup, not a defect pi-unreal fixes: it answers declaration,
230
+ module, include-path and dependency facts by a compiler-free route, and says what it scanned.
180
231
  - Engine area selection is a cost dial, not a completeness claim. `DEFAULT_ENGINE_AREAS =
181
- ["Runtime"]` covers every gameplay type (`AActor`, `UAnimInstance`, `USceneComponent`) at
182
- 18,566 files; `Editor` and `Developer` are opt-in via `engineAreas` and roughly triple the scan.
232
+ ["Runtime"]` covers the engine's **own** gameplay code (`AActor`, `UAnimInstance`,
233
+ `USceneComponent`, 18,566 files) — in an installed engine that is not every gameplay type, because
234
+ the ones shipped by plugins (`Mover`, `PoseSearch`, `PhysicsControl`, `BlendStack`, `SmartObjects`,
235
+ `StateTree`, `GameplayAbilities`) live under `Engine/Plugins/<Category>/<Plugin>/Source/<module>`.
236
+ Engine-scope indexes add the plugin roots the `.uproject` enables; the rest are opt-in via
237
+ `engineAreas: ["Plugins"]` (~2.8× the `Runtime` scan) or `["Plugins/Animation/PhysicsControl"]`
238
+ for one plugin. `Editor` and `Developer` are opt-in and roughly triple the scan.
239
+ - pi-unreal answers C++ **declaration, module, include-path and dependency** facts. It does not
240
+ answer asset questions: UENUM index order (Blueprint enumerators are stored in index order, so
241
+ `4 = OnGround` is an asset fact), UStruct field types, UCLASS/CDO instance values, component
242
+ lists, parent chains, Blueprint graphs. Those need the `unreal-engine` editor MCP with the editor
243
+ running; a C++ scan cannot see them and must not pretend to.
183
244
  - Index reuse requires the *same root set*: an index built over two modules is never used to answer
184
245
  a `Runtime`-wide query. Partial coverage reported as an answer is worse than a rebuild.
@@ -131,12 +131,20 @@ function engineLines(resolution: EngineResolution): string[] {
131
131
  return lines;
132
132
  }
133
133
 
134
- function pluginLines(results: PluginReferenceResult[], limit: number): string[] {
134
+ /**
135
+ * One line per `.uproject` reference, plus the modules that reference buys: the status alone never
136
+ * said which C++ a plugin contributes, so 'resolved' answered nothing about `UCharacterMoverComponent`.
137
+ * `index` adds the plugins the project does NOT reference: availability and reference status are
138
+ * different facts, and conflating them is what made 'no Mover reference' read as 'no Mover'.
139
+ */
140
+ function pluginLines(results: PluginReferenceResult[], limit: number, index?: PluginIndex): string[] {
135
141
  const lines: string[] = [];
136
142
  const shown = results.slice(0, limit);
137
143
  for (const result of shown) {
138
144
  const status = result.plugin ? `${result.status} ← ${result.plugin.source}` : result.status;
139
- lines.push(` ${result.name}: ${status}`);
145
+ const modules = result.plugin?.modules ?? [];
146
+ const moduleText = modules.length ? ` · modules ${modules.map((module) => `${module.moduleName}(${module.type ?? "Runtime"})`).join(", ")}` : "";
147
+ lines.push(` ${result.name}: ${status}${moduleText}`);
140
148
  for (const note of result.notes) lines.push(` · ${note}`);
141
149
  }
142
150
  if (results.length > limit) lines.push(` … ${results.length - limit} more references (raise pluginLimit)`);
@@ -144,6 +152,12 @@ function pluginLines(results: PluginReferenceResult[], limit: number): string[]
144
152
  for (const result of results) counts.set(result.status, (counts.get(result.status) ?? 0) + 1);
145
153
  const summary = [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ");
146
154
  lines.push(` totals: ${summary} of ${results.length} references`);
155
+ if (index) {
156
+ const referenced = new Set(results.map((result) => result.name.toLowerCase()));
157
+ const free = index.entries.filter((entry) => entry.source === "Engine" && entry.enabledByDefault && !referenced.has(entry.name.toLowerCase()));
158
+ const names = [...new Set(free.map((entry) => entry.name))].sort((a, b) => a.localeCompare(b));
159
+ lines.push(` engine plugins available with NO .uproject reference (EnabledByDefault true): ${names.length ? names.join(", ") : "none"}`);
160
+ }
147
161
  return lines;
148
162
  }
149
163
 
@@ -381,7 +395,7 @@ const projectInspectTool = defineTool({
381
395
  pluginResults.push(...resolveProjectPlugins(descriptor, built.index, resolution.version));
382
396
  lines.push(`plugin index: ${built.index.scannedFiles} .uplugin over [${built.index.scannedDirs.join(" | ")}] (cache ${built.cache})`);
383
397
  lines.push("plugin references:");
384
- lines.push(...pluginLines(pluginResults, params.pluginLimit ?? 40));
398
+ lines.push(...pluginLines(pluginResults, params.pluginLimit ?? 40, pluginIndex));
385
399
  } else {
386
400
  lines.push(`plugin references: ${descriptor.plugins.length} declared, not resolved against disk`);
387
401
  }
@@ -514,7 +528,7 @@ async function reportProject(args: string, ctx: ExtensionCommandContext): Promis
514
528
  if (resolution.status !== "unresolved") {
515
529
  const built = await getIndexFor(resolution.engineRoot, projectDirOf(descriptor), descriptor, resolution.version);
516
530
  lines.push(`plugin index: ${built.index.scannedFiles} .uplugin (cache ${built.cache})`);
517
- lines.push(...pluginLines(resolveProjectPlugins(descriptor, built.index, resolution.version), 25));
531
+ lines.push(...pluginLines(resolveProjectPlugins(descriptor, built.index, resolution.version), 25, built.index));
518
532
  }
519
533
  }
520
534
  ctx.ui.notify(truncateLines(lines.join("\n"), 40).text, "info");