@veewo/gitnexus 1.5.0-rc → 1.5.0-rc.3
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/dist/core/ingestion/unity-runtime-binding-rules.js +80 -2
- package/dist/mcp/local/runtime-claim-rule-registry.d.ts +1 -0
- package/dist/mcp/local/runtime-claim-rule-registry.js +1 -0
- package/dist/rule-lab/types.d.ts +8 -1
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +3 -0
- package/skills/gitnexus-unity-rule-gen.md +37 -2
|
@@ -51,10 +51,23 @@ export function applyUnityRuntimeBindingRules(graph, rules, _config) {
|
|
|
51
51
|
else if (rel.type === 'UNITY_COMPONENT_INSTANCE')
|
|
52
52
|
componentInstances.push(rel);
|
|
53
53
|
}
|
|
54
|
+
// Pre-build scene file index: lowercase scene name → fileId[]
|
|
55
|
+
const sceneFilesByName = new Map();
|
|
56
|
+
for (const node of graph.iterNodes()) {
|
|
57
|
+
if (node.label !== 'File')
|
|
58
|
+
continue;
|
|
59
|
+
const filePath = String(node.properties.filePath ?? '');
|
|
60
|
+
if (!filePath.endsWith('.unity'))
|
|
61
|
+
continue;
|
|
62
|
+
const fileName = (filePath.split('/').pop() ?? '').replace(/\.unity$/, '').toLowerCase();
|
|
63
|
+
const list = sceneFilesByName.get(fileName) ?? [];
|
|
64
|
+
list.push(node.id);
|
|
65
|
+
sceneFilesByName.set(fileName, list);
|
|
66
|
+
}
|
|
54
67
|
for (const rule of rules) {
|
|
55
68
|
let ruleEdges = 0;
|
|
56
69
|
for (const binding of rule.resource_bindings ?? []) {
|
|
57
|
-
ruleEdges += processBinding(binding, rule.id, assetGuidRefs, componentInstances, methodsByClassId, classNodes, addSyntheticEdge);
|
|
70
|
+
ruleEdges += processBinding(binding, rule.id, assetGuidRefs, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addSyntheticEdge);
|
|
58
71
|
}
|
|
59
72
|
if (rule.lifecycle_overrides?.additional_entry_points?.length) {
|
|
60
73
|
ruleEdges += processLifecycleOverrides(rule, methodsByClassId, classNodes, addSyntheticEdge);
|
|
@@ -78,13 +91,19 @@ function findMethodsOnResource(resourceFileId, componentInstances, methodsByClas
|
|
|
78
91
|
}
|
|
79
92
|
return results;
|
|
80
93
|
}
|
|
81
|
-
function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, addEdge) {
|
|
94
|
+
function processBinding(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge) {
|
|
82
95
|
if (binding.kind === 'asset_ref_loads_components') {
|
|
83
96
|
return processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge);
|
|
84
97
|
}
|
|
85
98
|
if (binding.kind === 'method_triggers_field_load') {
|
|
86
99
|
return processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, classNodes, addEdge);
|
|
87
100
|
}
|
|
101
|
+
if (binding.kind === 'method_triggers_scene_load') {
|
|
102
|
+
return processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge);
|
|
103
|
+
}
|
|
104
|
+
if (binding.kind === 'method_triggers_method') {
|
|
105
|
+
return processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNodes, addEdge);
|
|
106
|
+
}
|
|
88
107
|
return 0;
|
|
89
108
|
}
|
|
90
109
|
function processAssetRefLoadsComponents(binding, ruleId, assetGuidRefs, componentInstances, methodsByClassId, addEdge) {
|
|
@@ -157,6 +176,65 @@ function processMethodTriggersFieldLoad(binding, ruleId, assetGuidRefs, componen
|
|
|
157
176
|
}
|
|
158
177
|
return count;
|
|
159
178
|
}
|
|
179
|
+
function processMethodTriggersSceneLoad(binding, ruleId, componentInstances, methodsByClassId, classNodes, sceneFilesByName, addEdge) {
|
|
180
|
+
const classPattern = binding.host_class_pattern ? new RegExp(binding.host_class_pattern) : null;
|
|
181
|
+
const loaderMethodNames = new Set(binding.loader_methods ?? []);
|
|
182
|
+
const sceneName = binding.scene_name;
|
|
183
|
+
const defaultEntryPoints = ['OnEnable', 'Awake', 'Start'];
|
|
184
|
+
const entryPoints = (binding.target_entry_points ?? []).length > 0
|
|
185
|
+
? binding.target_entry_points
|
|
186
|
+
: defaultEntryPoints;
|
|
187
|
+
if (!classPattern || loaderMethodNames.size === 0 || !sceneName)
|
|
188
|
+
return 0;
|
|
189
|
+
const sceneFileIds = sceneFilesByName.get(sceneName.toLowerCase()) ?? [];
|
|
190
|
+
if (sceneFileIds.length === 0)
|
|
191
|
+
return 0;
|
|
192
|
+
let count = 0;
|
|
193
|
+
for (const cls of classNodes) {
|
|
194
|
+
if (!classPattern.test(cls.properties.name))
|
|
195
|
+
continue;
|
|
196
|
+
const methods = methodsByClassId.get(cls.id) ?? [];
|
|
197
|
+
const loaders = methods.filter(m => loaderMethodNames.has(m.properties.name));
|
|
198
|
+
if (loaders.length === 0)
|
|
199
|
+
continue;
|
|
200
|
+
for (const sceneFileId of sceneFileIds) {
|
|
201
|
+
const targetMethods = findMethodsOnResource(sceneFileId, componentInstances, methodsByClassId, entryPoints);
|
|
202
|
+
for (const loader of loaders) {
|
|
203
|
+
for (const target of targetMethods) {
|
|
204
|
+
if (addEdge(loader.id, target.id, `unity-rule-scene-load:${ruleId}`))
|
|
205
|
+
count++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
function processMethodTriggersMethod(binding, ruleId, methodsByClassId, classNodes, addEdge) {
|
|
213
|
+
const { source_class_pattern, source_method, target_class_pattern, target_method } = binding;
|
|
214
|
+
if (!source_class_pattern || !source_method || !target_class_pattern || !target_method)
|
|
215
|
+
return 0;
|
|
216
|
+
const srcPattern = new RegExp(source_class_pattern);
|
|
217
|
+
const tgtPattern = new RegExp(target_class_pattern);
|
|
218
|
+
let sourceMethodId;
|
|
219
|
+
let targetMethodId;
|
|
220
|
+
for (const cls of classNodes) {
|
|
221
|
+
if (!sourceMethodId && srcPattern.test(cls.properties.name)) {
|
|
222
|
+
const match = (methodsByClassId.get(cls.id) ?? []).find(m => m.properties.name === source_method);
|
|
223
|
+
if (match)
|
|
224
|
+
sourceMethodId = match.id;
|
|
225
|
+
}
|
|
226
|
+
if (!targetMethodId && tgtPattern.test(cls.properties.name)) {
|
|
227
|
+
const match = (methodsByClassId.get(cls.id) ?? []).find(m => m.properties.name === target_method);
|
|
228
|
+
if (match)
|
|
229
|
+
targetMethodId = match.id;
|
|
230
|
+
}
|
|
231
|
+
if (sourceMethodId && targetMethodId)
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (!sourceMethodId || !targetMethodId)
|
|
235
|
+
return 0;
|
|
236
|
+
return addEdge(sourceMethodId, targetMethodId, `unity-rule-method-bridge:${ruleId}`) ? 1 : 0;
|
|
237
|
+
}
|
|
160
238
|
function processLifecycleOverrides(rule, methodsByClassId, classNodes, addEdge) {
|
|
161
239
|
const overrides = rule.lifecycle_overrides;
|
|
162
240
|
if (!overrides?.additional_entry_points?.length)
|
|
@@ -173,6 +173,7 @@ export function parseRuleYaml(raw, filePath) {
|
|
|
173
173
|
binding.host_class_pattern = scalar('host_class_pattern');
|
|
174
174
|
binding.field_name = scalar('field_name');
|
|
175
175
|
binding.loader_methods = list('loader_methods');
|
|
176
|
+
binding.scene_name = scalar('scene_name');
|
|
176
177
|
resource_bindings.push(binding);
|
|
177
178
|
}
|
|
178
179
|
if (resource_bindings.length === 0)
|
package/dist/rule-lab/types.d.ts
CHANGED
|
@@ -76,12 +76,18 @@ export interface RuleDslClaims {
|
|
|
76
76
|
next_action: string;
|
|
77
77
|
}
|
|
78
78
|
export interface UnityResourceBinding {
|
|
79
|
-
kind: 'asset_ref_loads_components' | 'method_triggers_field_load';
|
|
79
|
+
kind: 'asset_ref_loads_components' | 'method_triggers_field_load' | 'method_triggers_scene_load' | 'method_triggers_method';
|
|
80
|
+
description?: string;
|
|
80
81
|
ref_field_pattern?: string;
|
|
81
82
|
target_entry_points?: string[];
|
|
82
83
|
host_class_pattern?: string;
|
|
83
84
|
field_name?: string;
|
|
84
85
|
loader_methods?: string[];
|
|
86
|
+
scene_name?: string;
|
|
87
|
+
source_class_pattern?: string;
|
|
88
|
+
source_method?: string;
|
|
89
|
+
target_class_pattern?: string;
|
|
90
|
+
target_method?: string;
|
|
85
91
|
}
|
|
86
92
|
export interface LifecycleOverrides {
|
|
87
93
|
additional_entry_points?: string[];
|
|
@@ -90,6 +96,7 @@ export interface LifecycleOverrides {
|
|
|
90
96
|
export interface RuleDslDraft {
|
|
91
97
|
id: string;
|
|
92
98
|
version: string;
|
|
99
|
+
description?: string;
|
|
93
100
|
match: RuleDslMatch;
|
|
94
101
|
topology: RuleDslTopologyHop[];
|
|
95
102
|
closure: RuleDslClosure;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veewo/gitnexus",
|
|
3
|
-
"version": "1.5.0-rc",
|
|
3
|
+
"version": "1.5.0-rc.3",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
package/skills/gitnexus-cli.md
CHANGED
|
@@ -48,8 +48,11 @@ Run from the project root. This parses all source files, builds the knowledge gr
|
|
|
48
48
|
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
|
49
49
|
| `--extensions <ext>` | Limit parsing to specific file types (e.g., `--extensions ".cs .meta"` for Unity) |
|
|
50
50
|
| `--scope-prefix <prefix>` | Limit analysis to a path prefix (e.g., `--scope-prefix Assets/` for Unity) |
|
|
51
|
+
| `--scope-manifest <file>` | Read scope rules from a manifest file (e.g., `.gitnexus/sync-manifest.txt`) |
|
|
51
52
|
| `--skills` | Generate repo-specific skill files from detected code communities |
|
|
52
53
|
|
|
54
|
+
**Scope manifest syntax:** Each line is a **path prefix** (not glob). `Assets/Code` matches all files under `Assets/Code/`. Trailing `*` is a wildcard prefix (`Packages/com.veewo.*` matches `Packages/com.veewo.stat/...`). Lines starting with `#` are comments. Do **not** use glob patterns like `**/*.cs` — they will match nothing. Use `--extensions` for file type filtering instead.
|
|
55
|
+
|
|
53
56
|
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook runs `analyze` automatically after `git commit` and `git merge`, preserving embeddings if previously generated.
|
|
54
57
|
|
|
55
58
|
**Unity projects:** Add `--extensions ".cs .meta"` to ensure Unity asset edges (`UNITY_ASSET_GUID_REF`, `UNITY_COMPONENT_INSTANCE`) are parsed. Add `--scope-prefix Assets/` to limit scope if all code lives under `Assets/`.
|
|
@@ -59,6 +59,7 @@ loop:
|
|
|
59
59
|
| 目标入口方法 | 加载的资源上哪些方法会被触发? | OnEnable, Awake |
|
|
60
60
|
| 持有字段的类 | 哪些类持有触发加载的字段? | WeaponPowerUp |
|
|
61
61
|
| 加载方法 | 哪些方法触发资源加载? | Equip |
|
|
62
|
+
| 动态跳转 | 链路中是否有事件派发或回调(C# Action/SyncList/delegate)? | `NetEventHub.OnPickUpItem → OnClientPickItUp` |
|
|
62
63
|
| 额外 lifecycle | 项目有自定义入口方法吗? | Init |
|
|
63
64
|
| lifecycle 范围 | 自定义入口方法的作用范围? | Assets/Code/Graph |
|
|
64
65
|
|
|
@@ -111,6 +112,8 @@ Grep: pattern="void Init\b|void Setup\b" path=<Assets目录>
|
|
|
111
112
|
|---------|-------------|------|
|
|
112
113
|
| asset GUID 引用 → 目标资源组件激活 | `asset_ref_loads_components` | 序列化字段引用 asset,加载时触发组件 lifecycle |
|
|
113
114
|
| 方法调用 → 字段引用的资源加载 | `method_triggers_field_load` | 特定方法触发序列化字段引用的资源加载 |
|
|
115
|
+
| 方法调用 → SceneManager.LoadScene → 场景组件激活 | `method_triggers_scene_load` | 特定方法触发场景加载,场景中组件 lifecycle 被触发 |
|
|
116
|
+
| 动态跳转(事件派发/回调/delegate,静态分析不可见) | `method_triggers_method` | 声明"方法 A 动态触发方法 B",注入合成 CALLS 边桥接 gap |
|
|
114
117
|
| 项目自定义入口方法 | `lifecycle_overrides` | 非标准 Unity lifecycle 的自定义入口 |
|
|
115
118
|
|
|
116
119
|
### 1.5 生成规则 YAML
|
|
@@ -121,6 +124,9 @@ Grep: pattern="void Init\b|void Setup\b" path=<Assets目录>
|
|
|
121
124
|
id: unity.<scenario-name>.v2
|
|
122
125
|
version: 2.0.0
|
|
123
126
|
family: analyze_rules
|
|
127
|
+
description: >-
|
|
128
|
+
(可选)描述该规则覆盖的业务场景和调用链背景,
|
|
129
|
+
包括动态跳转的机制说明(事件派发/回调绑定等)。
|
|
124
130
|
trigger_family: <scenario-name>
|
|
125
131
|
resource_types:
|
|
126
132
|
- asset
|
|
@@ -153,6 +159,30 @@ resource_bindings:
|
|
|
153
159
|
loader_methods:
|
|
154
160
|
- <method_name>
|
|
155
161
|
|
|
162
|
+
# 类型 C(可选):方法触发场景加载,场景中组件 lifecycle 被触发
|
|
163
|
+
- kind: method_triggers_scene_load
|
|
164
|
+
host_class_pattern: "<class_pattern>"
|
|
165
|
+
loader_methods:
|
|
166
|
+
- <method_name>
|
|
167
|
+
scene_name: "<scene_name>" # 匹配 .unity 文件名(不含扩展名)
|
|
168
|
+
target_entry_points:
|
|
169
|
+
- Awake
|
|
170
|
+
- Start
|
|
171
|
+
- OnEnable
|
|
172
|
+
|
|
173
|
+
# 类型 D(可选):声明静态分析无法捕获的动态跳转(事件派发/回调/delegate)
|
|
174
|
+
# 适用场景:C# Action/UnityEvent 事件派发、Mirror SyncList 回调、delegate 绑定等
|
|
175
|
+
# 注入一条 source_method → target_method 的合成 CALLS 边(精确匹配,一条边)
|
|
176
|
+
- kind: method_triggers_method
|
|
177
|
+
description: >-
|
|
178
|
+
说明动态跳转的机制:例如"A 通过 EventHub.OnXxx?.Invoke() 触发,
|
|
179
|
+
B 在初始化时订阅该事件",或"A 调用 SyncList.Add(),
|
|
180
|
+
触发 SyncList.Callback 回调到 B"
|
|
181
|
+
source_class_pattern: "<source_class_regex>" # 例如 "^PlayerActor$"
|
|
182
|
+
source_method: "<source_method_name>" # 例如 "ProcessInteractables"
|
|
183
|
+
target_class_pattern: "<target_class_regex>" # 例如 "^NetPlayer$"
|
|
184
|
+
target_method: "<target_method_name>" # 例如 "OnClientPickItUp"
|
|
185
|
+
|
|
156
186
|
lifecycle_overrides:
|
|
157
187
|
additional_entry_points:
|
|
158
188
|
- <custom_entry>
|
|
@@ -286,13 +316,15 @@ mcp__gitnexus__cypher:
|
|
|
286
316
|
WHEN r.reason CONTAINS 'resource-load' THEN 'resource-load'
|
|
287
317
|
WHEN r.reason CONTAINS 'lifecycle-override' THEN 'lifecycle-override'
|
|
288
318
|
WHEN r.reason CONTAINS 'loader-bridge' THEN 'loader-bridge'
|
|
319
|
+
WHEN r.reason CONTAINS 'scene-load' THEN 'scene-load'
|
|
320
|
+
WHEN r.reason CONTAINS 'method-bridge' THEN 'method-bridge'
|
|
289
321
|
ELSE 'other'
|
|
290
322
|
END AS edgeKind,
|
|
291
323
|
count(*) AS cnt
|
|
292
324
|
repo: <repo-name>
|
|
293
325
|
```
|
|
294
326
|
|
|
295
|
-
**PASS**:
|
|
327
|
+
**PASS**: 规则涉及的边类型均有产出(`method_triggers_method` 对应 `method-bridge`)。
|
|
296
328
|
|
|
297
329
|
---
|
|
298
330
|
|
|
@@ -302,9 +334,12 @@ mcp__gitnexus__cypher:
|
|
|
302
334
|
|------|---------|---------|
|
|
303
335
|
| 验证 1 失败(0 合成边) | 规则未被 compile 或 family 不对 | 检查 catalog.json + compiled bundle |
|
|
304
336
|
| 验证 1 部分(只有 resource-load) | method_triggers_field_load 参数错误 | 检查 host_class_pattern / loader_methods |
|
|
337
|
+
| 验证 1 部分(无 scene-load) | method_triggers_scene_load 参数错误 | 检查 scene_name 是否匹配 .unity 文件名 |
|
|
338
|
+
| 验证 1 部分(无 method-bridge) | method_triggers_method 类名/方法名不匹配 | 检查 source/target_class_pattern 和 source/target_method 是否与图谱中节点名一致 |
|
|
305
339
|
| 验证 2 失败(rule_not_matched) | trigger_tokens 未匹配查询文本 | 调整 match.trigger_tokens |
|
|
306
340
|
| 验证 2 失败(verification_failed) | 合成边 reason 中的 ruleId 不匹配 | 检查规则 ID 一致性 |
|
|
307
341
|
| 验证 3 失败(无 Process) | 合成边 confidence 过低 | 检查 RULE_EDGE_CONFIDENCE(应为 0.75) |
|
|
342
|
+
| 验证 4 链路断裂(中间有动态跳转) | 事件派发/回调/delegate 无静态 CALLS 边 | 添加 `method_triggers_method` binding 桥接动态跳转 |
|
|
308
343
|
| lifecycle_overrides 无效 | scope 值不是文件路径前缀 | scope 应匹配 filePath 而非类名 |
|
|
309
344
|
|
|
310
345
|
---
|
|
@@ -314,5 +349,5 @@ mcp__gitnexus__cypher:
|
|
|
314
349
|
- 设计文档:`docs/plans/2026-04-04-unity-rule-gen-skill-design.md`
|
|
315
350
|
- YAML 格式定义:设计文档 section 3.3
|
|
316
351
|
- 注入逻辑:`gitnexus/src/core/ingestion/unity-runtime-binding-rules.ts`
|
|
317
|
-
- 规则类型定义:`gitnexus/src/rule-lab/types.ts:90-
|
|
352
|
+
- 规则类型定义:`gitnexus/src/rule-lab/types.ts:90-114`
|
|
318
353
|
- 编译命令:`gitnexus rule-lab compile --repo-path <path>`
|