@veewo/gitnexus 1.4.11-rc.2 → 1.5.0-rc

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 (139) hide show
  1. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
  2. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
  3. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
  4. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
  5. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
  6. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
  7. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
  8. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
  9. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
  10. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
  12. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
  13. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
  14. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
  15. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
  16. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
  17. package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
  18. package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
  19. package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
  20. package/dist/cli/ai-context.js +6 -1
  21. package/dist/cli/eval-server.js +0 -3
  22. package/dist/cli/index.js +8 -0
  23. package/dist/cli/mcp.js +0 -3
  24. package/dist/cli/rule-lab.d.ts +42 -0
  25. package/dist/cli/rule-lab.js +157 -0
  26. package/dist/cli/rule-lab.test.d.ts +1 -0
  27. package/dist/cli/rule-lab.test.js +11 -0
  28. package/dist/cli/tool.d.ts +7 -1
  29. package/dist/cli/tool.js +6 -0
  30. package/dist/core/config/unity-config.d.ts +20 -0
  31. package/dist/core/config/unity-config.js +46 -0
  32. package/dist/core/graph/types.d.ts +1 -1
  33. package/dist/core/ingestion/pipeline.js +38 -13
  34. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  37. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  38. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  39. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  40. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  41. package/dist/core/unity/options.d.ts +4 -0
  42. package/dist/core/unity/options.js +18 -0
  43. package/dist/core/unity/options.test.js +11 -1
  44. package/dist/core/unity/resolver.js +11 -1
  45. package/dist/core/unity/resolver.test.js +62 -0
  46. package/dist/core/unity/yaml-object-graph.js +1 -1
  47. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  48. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  49. package/dist/mcp/local/derived-process-reader.js +15 -0
  50. package/dist/mcp/local/local-backend.d.ts +56 -0
  51. package/dist/mcp/local/local-backend.js +1003 -53
  52. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  53. package/dist/mcp/local/process-confidence.js +1 -1
  54. package/dist/mcp/local/process-evidence.d.ts +1 -0
  55. package/dist/mcp/local/process-evidence.js +22 -0
  56. package/dist/mcp/local/process-evidence.test.js +11 -1
  57. package/dist/mcp/local/process-ref.d.ts +24 -0
  58. package/dist/mcp/local/process-ref.js +33 -0
  59. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  60. package/dist/mcp/local/process-ref.test.js +24 -0
  61. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  62. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  63. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  64. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  65. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  68. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  69. package/dist/mcp/local/runtime-claim.js +54 -0
  70. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  71. package/dist/mcp/local/runtime-claim.test.js +27 -0
  72. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  73. package/dist/mcp/local/unity-enrichment.js +1 -1
  74. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  75. package/dist/mcp/local/unity-evidence-view.js +96 -0
  76. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  77. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  78. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  79. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  80. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  81. package/dist/mcp/local/unity-parity-cache.js +2 -6
  82. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  83. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  84. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  85. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  86. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  87. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  88. package/dist/mcp/resources.js +13 -0
  89. package/dist/mcp/tools.js +166 -13
  90. package/dist/rule-lab/analyze.d.ts +12 -0
  91. package/dist/rule-lab/analyze.js +90 -0
  92. package/dist/rule-lab/analyze.test.d.ts +1 -0
  93. package/dist/rule-lab/analyze.test.js +28 -0
  94. package/dist/rule-lab/compile.d.ts +5 -0
  95. package/dist/rule-lab/compile.js +51 -0
  96. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  97. package/dist/rule-lab/compiled-bundles.js +36 -0
  98. package/dist/rule-lab/curate.d.ts +32 -0
  99. package/dist/rule-lab/curate.js +134 -0
  100. package/dist/rule-lab/curate.test.d.ts +1 -0
  101. package/dist/rule-lab/curate.test.js +72 -0
  102. package/dist/rule-lab/discover.d.ts +13 -0
  103. package/dist/rule-lab/discover.js +74 -0
  104. package/dist/rule-lab/discover.test.d.ts +1 -0
  105. package/dist/rule-lab/discover.test.js +42 -0
  106. package/dist/rule-lab/paths.d.ts +21 -0
  107. package/dist/rule-lab/paths.js +37 -0
  108. package/dist/rule-lab/paths.test.d.ts +1 -0
  109. package/dist/rule-lab/paths.test.js +46 -0
  110. package/dist/rule-lab/promote.d.ts +26 -0
  111. package/dist/rule-lab/promote.js +314 -0
  112. package/dist/rule-lab/promote.test.d.ts +1 -0
  113. package/dist/rule-lab/promote.test.js +164 -0
  114. package/dist/rule-lab/regress.d.ts +60 -0
  115. package/dist/rule-lab/regress.js +122 -0
  116. package/dist/rule-lab/regress.test.d.ts +1 -0
  117. package/dist/rule-lab/regress.test.js +68 -0
  118. package/dist/rule-lab/review-pack.d.ts +31 -0
  119. package/dist/rule-lab/review-pack.js +125 -0
  120. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  121. package/dist/rule-lab/review-pack.test.js +49 -0
  122. package/dist/rule-lab/types.d.ts +99 -0
  123. package/dist/rule-lab/types.js +1 -0
  124. package/package.json +1 -1
  125. package/skills/_shared/unity-hydration-contract.md +11 -0
  126. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  127. package/skills/gitnexus-cli.md +11 -25
  128. package/skills/gitnexus-guide.md +2 -0
  129. package/skills/gitnexus-unity-rule-gen.md +318 -0
  130. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  131. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  132. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  133. package/dist/mcp/local/unity-lazy-config.js +0 -7
  134. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  135. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  136. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  137. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  139. /package/dist/{mcp/local/unity-lazy-config.test.d.ts → benchmark/u2-e2e/hydration-policy-repeatability-runner.test.d.ts} +0 -0
@@ -2,13 +2,15 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { describe, expect, it } from 'vitest';
5
- import { verifyRuntimeChainOnDemand } from './runtime-chain-verify.js';
5
+ import { verifyRuntimeChainOnDemand, verifyRuntimeClaimOnDemand } from './runtime-chain-verify.js';
6
+ import { promoteCuratedRules } from '../../rule-lab/promote.js';
6
7
  async function makeTempRepo() {
7
8
  const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-verify-'));
8
9
  await fs.mkdir(path.join(repoPath, 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb'), { recursive: true });
9
10
  await fs.mkdir(path.join(repoPath, 'Assets/NEON/Graphs/PlayerGun/Gungraph_use'), { recursive: true });
10
11
  await fs.mkdir(path.join(repoPath, 'Assets/NEON/Code/Game/Graph/Nodes/Reloads'), { recursive: true });
11
12
  await fs.writeFile(path.join(repoPath, 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset'), 'gungraph: {guid: 69199acacbf8a7e489ad4aa872efcabd}\n');
13
+ await fs.writeFile(path.join(repoPath, 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset.meta'), 'guid: 69199acacbf8a7e489ad4aa872efcabd\n');
12
14
  await fs.writeFile(path.join(repoPath, 'Assets/NEON/Graphs/PlayerGun/Gungraph_use/1_weapon_orb_key.asset'), 'ResultRPM: GunOutput.RPM\n');
13
15
  await fs.writeFile(path.join(repoPath, 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs.meta'), 'guid: bd387039cacb475381a86f156b54bac2\n');
14
16
  await fs.mkdir(path.join(repoPath, 'Assets/NEON/Code/Game/PowerUps'), { recursive: true });
@@ -18,39 +20,564 @@ async function makeTempRepo() {
18
20
  await fs.writeFile(path.join(repoPath, 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs'), 'void GetValue() {}\n');
19
21
  return repoPath;
20
22
  }
23
+ async function fileExists(filePath) {
24
+ try {
25
+ await fs.access(filePath);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ async function resolveWorkspaceRulesRoot() {
33
+ const candidates = [
34
+ path.resolve('.gitnexus/rules'),
35
+ path.resolve('../.gitnexus/rules'),
36
+ ];
37
+ for (const candidate of candidates) {
38
+ if (await fileExists(path.join(candidate, 'catalog.json'))) {
39
+ return candidate;
40
+ }
41
+ }
42
+ return candidates[0];
43
+ }
44
+ function makeExecuteParameterized(repoPath) {
45
+ return async (query, params) => {
46
+ const q = String(query || '');
47
+ const weaponPath = 'Assets/NEON/Code/Game/PowerUps/WeaponPowerUp.cs';
48
+ const gunGraphPath = 'Assets/NEON/Code/Game/Core/GunGraph.cs';
49
+ const reloadBasePath = 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs';
50
+ const symbolRows = {
51
+ WeaponPowerUp: {
52
+ id: `Class:${weaponPath}:WeaponPowerUp`,
53
+ name: 'WeaponPowerUp',
54
+ type: 'Class',
55
+ filePath: weaponPath,
56
+ startLine: 1,
57
+ },
58
+ GunGraph: {
59
+ id: `Class:${gunGraphPath}:GunGraph`,
60
+ name: 'GunGraph',
61
+ type: 'Class',
62
+ filePath: gunGraphPath,
63
+ startLine: 1,
64
+ },
65
+ ReloadBase: {
66
+ id: `Class:${reloadBasePath}:ReloadBase`,
67
+ name: 'ReloadBase',
68
+ type: 'Class',
69
+ filePath: reloadBasePath,
70
+ startLine: 1,
71
+ },
72
+ };
73
+ const reloadBasePresent = await fileExists(path.join(repoPath, reloadBasePath));
74
+ if (q.includes('WHERE n.filePath = $filePath')) {
75
+ const filePath = String(params?.filePath || '');
76
+ const symbolName = String(params?.symbolName || '');
77
+ const candidates = Object.values(symbolRows).filter((row) => row.filePath === filePath && (row.name !== 'ReloadBase' || reloadBasePresent));
78
+ if (!symbolName)
79
+ return candidates;
80
+ return candidates.filter((row) => row.name === symbolName);
81
+ }
82
+ if (q.includes('WHERE n.name IN $symbolNames')) {
83
+ const names = Array.isArray(params?.symbolNames) ? params?.symbolNames : [];
84
+ return names
85
+ .map((name) => symbolRows[String(name)])
86
+ .filter((row) => row && (row.name !== 'ReloadBase' || reloadBasePresent))
87
+ .filter(Boolean);
88
+ }
89
+ if (q.includes("MATCH (s {id: $symbolId})-[r:CodeRelation {type: 'CALLS'}]->(t)")) {
90
+ const symbolId = String(params?.symbolId || '');
91
+ if (symbolId === symbolRows.ReloadBase.id && reloadBasePresent) {
92
+ return [{
93
+ sourceId: symbolRows.ReloadBase.id,
94
+ sourceName: 'ReloadBase',
95
+ sourceFilePath: reloadBasePath,
96
+ sourceStartLine: 1,
97
+ targetId: `Method:${reloadBasePath}:CheckReload`,
98
+ targetName: 'CheckReload',
99
+ targetFilePath: reloadBasePath,
100
+ targetStartLine: 12,
101
+ }];
102
+ }
103
+ if (symbolId === symbolRows.WeaponPowerUp.id) {
104
+ return [{
105
+ sourceId: symbolRows.WeaponPowerUp.id,
106
+ sourceName: 'WeaponPowerUp',
107
+ sourceFilePath: weaponPath,
108
+ sourceStartLine: 1,
109
+ targetId: `Method:${weaponPath}:Equip`,
110
+ targetName: 'Equip',
111
+ targetFilePath: weaponPath,
112
+ targetStartLine: 1,
113
+ }];
114
+ }
115
+ return [];
116
+ }
117
+ if (q.includes("MATCH (s)-[r:CodeRelation {type: 'CALLS'}]->(t {id: $symbolId})")) {
118
+ const symbolId = String(params?.symbolId || '');
119
+ if (symbolId === symbolRows.ReloadBase.id && reloadBasePresent) {
120
+ return [{
121
+ sourceId: `Method:${weaponPath}:Equip`,
122
+ sourceName: 'Equip',
123
+ sourceFilePath: weaponPath,
124
+ sourceStartLine: 1,
125
+ targetId: symbolRows.ReloadBase.id,
126
+ targetName: 'ReloadBase',
127
+ targetFilePath: reloadBasePath,
128
+ targetStartLine: 1,
129
+ }];
130
+ }
131
+ return [];
132
+ }
133
+ if (q.includes("MATCH (n {id: $symbolId})-[:CodeRelation {type: 'HAS_METHOD'}]->(m)")
134
+ && q.includes("MATCH (m)-[r:CodeRelation {type: 'CALLS'}]->(t)")) {
135
+ const symbolId = String(params?.symbolId || '');
136
+ if (symbolId === symbolRows.ReloadBase.id && reloadBasePresent) {
137
+ return [{
138
+ sourceId: `Method:${reloadBasePath}:OnEquip`,
139
+ sourceName: 'OnEquip',
140
+ sourceFilePath: reloadBasePath,
141
+ sourceStartLine: 5,
142
+ targetId: `Method:${reloadBasePath}:CheckReload`,
143
+ targetName: 'CheckReload',
144
+ targetFilePath: reloadBasePath,
145
+ targetStartLine: 12,
146
+ }];
147
+ }
148
+ return [];
149
+ }
150
+ if (q.includes("MATCH (n {id: $symbolId})-[:CodeRelation {type: 'HAS_METHOD'}]->(m)")
151
+ && q.includes("MATCH (s)-[r:CodeRelation {type: 'CALLS'}]->(m)")) {
152
+ const symbolId = String(params?.symbolId || '');
153
+ if (symbolId === symbolRows.ReloadBase.id && reloadBasePresent) {
154
+ return [{
155
+ sourceId: `Method:${weaponPath}:Equip`,
156
+ sourceName: 'Equip',
157
+ sourceFilePath: weaponPath,
158
+ sourceStartLine: 1,
159
+ targetId: `Method:${reloadBasePath}:OnEquip`,
160
+ targetName: 'OnEquip',
161
+ targetFilePath: reloadBasePath,
162
+ targetStartLine: 5,
163
+ }];
164
+ }
165
+ return [];
166
+ }
167
+ if (q.includes("r.reason STARTS WITH 'unity-rule-'") && q.includes('r.reason CONTAINS $ruleId')) {
168
+ const ruleId = String(params?.ruleId || '');
169
+ if (ruleId && reloadBasePresent) {
170
+ return [{
171
+ sourceName: 'unity-runtime-root',
172
+ sourceFilePath: '',
173
+ sourceStartLine: 1,
174
+ targetName: 'ReloadBase',
175
+ targetFilePath: reloadBasePath,
176
+ targetStartLine: 1,
177
+ reason: `unity-rule-resource-load:${ruleId}`,
178
+ }];
179
+ }
180
+ return [];
181
+ }
182
+ return [];
183
+ };
184
+ }
185
+ function hasBalancedShellQuotes(command) {
186
+ let inSingle = false;
187
+ let inDouble = false;
188
+ let escaped = false;
189
+ for (const ch of String(command || '')) {
190
+ if (escaped) {
191
+ escaped = false;
192
+ continue;
193
+ }
194
+ if (ch === '\\') {
195
+ escaped = true;
196
+ continue;
197
+ }
198
+ if (!inDouble && ch === '\'') {
199
+ inSingle = !inSingle;
200
+ continue;
201
+ }
202
+ if (!inSingle && ch === '"') {
203
+ inDouble = !inDouble;
204
+ }
205
+ }
206
+ return !inSingle && !inDouble && !escaped;
207
+ }
208
+ async function writeRules(repoPath, ruleYamlByFile, rootDirName = 'rules') {
209
+ const rulesRoot = path.join(repoPath, '.gitnexus', rootDirName);
210
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
211
+ const entries = Object.entries(ruleYamlByFile).map(([file, content]) => {
212
+ const id = String(content.match(/^id:\s*(.+)$/m)?.[1] || '').trim();
213
+ const version = String(content.match(/^version:\s*(.+)$/m)?.[1] || '').trim();
214
+ return { id, version, file };
215
+ });
216
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({ rules: entries }, null, 2), 'utf-8');
217
+ for (const [file, content] of Object.entries(ruleYamlByFile)) {
218
+ await fs.writeFile(path.join(rulesRoot, file), content, 'utf-8');
219
+ }
220
+ return rulesRoot;
221
+ }
21
222
  describe('runtime chain verify', () => {
22
- it('v1 runtime chain verify on demand builds reload chain hops', async () => {
223
+ it('does not run reload fallback when no rule is matched', async () => {
23
224
  const repoPath = await makeTempRepo();
24
225
  const out = await verifyRuntimeChainOnDemand({
25
226
  repoPath,
26
227
  queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
27
- executeParameterized: async (query, params) => {
28
- if (String(params?.filePathPattern || '').includes('WeaponPowerUp.cs')) {
29
- return [{ filePath: 'Assets/NEON/Code/Game/PowerUps/WeaponPowerUp.cs', startLine: 1 }];
30
- }
31
- if (String(params?.filePathPattern || '').includes('GunGraph')) {
32
- return [{ filePath: 'Assets/NEON/Code/Game/Core/GunGraph.cs', startLine: 1 }];
33
- }
34
- if (String(params?.filePathPattern || '').includes('ReloadBase.cs')) {
35
- return [{ filePath: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs', startLine: 1 }];
36
- }
37
- return [];
38
- },
228
+ executeParameterized: makeExecuteParameterized(repoPath),
39
229
  resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
40
230
  });
41
- expect(out?.evidence_level).toBe('verified_chain');
42
- expect(out?.hops.some((hop) => hop.hop_type === 'guid_map')).toBe(true);
43
- const loader = out?.hops.find((hop) => hop.hop_type === 'code_loader');
44
- expect(loader?.snippet || '').toMatch(/CurGunGraph\s*=/i);
231
+ expect(out).toBeUndefined();
232
+ await fs.rm(repoPath, { recursive: true, force: true });
45
233
  });
46
- it('v1 runtime chain gaps are actionable', async () => {
234
+ it('runtime chain gaps are actionable under matched rule execution', async () => {
47
235
  const out = await verifyRuntimeChainOnDemand({
48
236
  repoPath: await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-gaps-')),
49
237
  queryText: 'Reload',
50
238
  executeParameterized: async () => [],
51
239
  resourceBindings: [],
240
+ rule: {
241
+ id: 'demo.reload.strict.v2',
242
+ version: '2.0.0',
243
+ trigger_family: 'reload',
244
+ resource_types: ['asset'],
245
+ host_base_type: ['ReloadBase'],
246
+ required_hops: ['resource', 'guid_map', 'code_loader', 'code_runtime'],
247
+ guarantees: ['strict_chain_closed'],
248
+ non_guarantees: ['strict_no_runtime_execution'],
249
+ next_action: 'node strict',
250
+ file_path: '.gitnexus/rules/approved/demo.reload.strict.v2.yaml',
251
+ },
252
+ requiredHops: ['resource', 'guid_map', 'code_loader', 'code_runtime'],
52
253
  });
53
254
  expect(out?.gaps.length).toBeGreaterThan(0);
54
255
  expect(out?.gaps.every((gap) => !!gap.next_command)).toBe(true);
55
256
  });
257
+ it('accepts seed-to-mapped resource equivalence for resource hop verification', async () => {
258
+ const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-mapped-resource-'));
259
+ const out = await verifyRuntimeChainOnDemand({
260
+ repoPath,
261
+ queryText: 'EnergyByAttackCount Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/0_初始武器/1_weapon_0_james_new.asset',
262
+ resourceSeedPath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/0_初始武器/1_weapon_0_james_new.asset',
263
+ mappedSeedTargets: ['Assets/NEON/Graphs/PlayerGun/Gungraph_use/1_weapon_0_james1.asset'],
264
+ executeParameterized: async () => [],
265
+ resourceBindings: [{ resourcePath: 'Assets/NEON/Graphs/PlayerGun/Gungraph_use/1_weapon_0_james1.asset' }],
266
+ rule: {
267
+ id: 'demo.energy.seed-map.v1',
268
+ version: '1.0.0',
269
+ trigger_family: 'energy',
270
+ resource_types: ['asset'],
271
+ host_base_type: ['GunGraphNode'],
272
+ required_hops: ['resource'],
273
+ guarantees: ['seed_mapped_resource_is_accepted'],
274
+ non_guarantees: ['does_not_verify_full_runtime_order'],
275
+ next_action: 'node mapped-resource',
276
+ file_path: '.gitnexus/rules/approved/demo.energy.seed-map.v1.yaml',
277
+ },
278
+ requiredHops: ['resource'],
279
+ });
280
+ expect(out?.status).toBe('verified_full');
281
+ expect(out?.gaps.length).toBe(0);
282
+ expect(out?.hops[0]?.note || '').toContain('mapped resource equivalence');
283
+ await fs.rm(repoPath, { recursive: true, force: true });
284
+ });
285
+ it('phase2 runtime claim returns explicit rule_not_matched', async () => {
286
+ const workspaceRulesRoot = await resolveWorkspaceRulesRoot();
287
+ const out = await verifyRuntimeClaimOnDemand({
288
+ repoPath: path.resolve('.'),
289
+ queryText: 'CompletelyUnrelatedChain',
290
+ executeParameterized: async () => [],
291
+ resourceBindings: [],
292
+ rulesRoot: workspaceRulesRoot,
293
+ });
294
+ expect(out.status).toBe('failed');
295
+ expect(out.reason).toBe('rule_not_matched');
296
+ expect(out.next_action).toBeTruthy();
297
+ });
298
+ it('phase2 runtime claim maps missing catalog to rule_not_matched', async () => {
299
+ const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-missing-catalog-'));
300
+ const out = await verifyRuntimeClaimOnDemand({
301
+ repoPath,
302
+ queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
303
+ executeParameterized: async () => [],
304
+ resourceBindings: [],
305
+ rulesRoot: path.join(repoPath, '.gitnexus', 'rules'),
306
+ });
307
+ expect(out.status).toBe('failed');
308
+ expect(out.reason).toBe('rule_not_matched');
309
+ expect(out.next_action).toBeTruthy();
310
+ await fs.rm(repoPath, { recursive: true, force: true });
311
+ });
312
+ it('phase2 runtime claim maps missing rule file to rule_not_matched', async () => {
313
+ const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-missing-rule-file-'));
314
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
315
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
316
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
317
+ rules: [{ id: 'demo.reload.rule.v1', version: '1.0.0', file: 'approved/demo.reload.rule.v1.yaml' }],
318
+ }), 'utf-8');
319
+ const out = await verifyRuntimeClaimOnDemand({
320
+ repoPath,
321
+ queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
322
+ executeParameterized: async () => [],
323
+ resourceBindings: [],
324
+ rulesRoot,
325
+ });
326
+ expect(out.status).toBe('failed');
327
+ expect(out.reason).toBe('rule_not_matched');
328
+ expect(out.next_action).toBeTruthy();
329
+ await fs.rm(repoPath, { recursive: true, force: true });
330
+ });
331
+ it('phase2 runtime claim uses bootstrap reload rule metadata', async () => {
332
+ const repoPath = await makeTempRepo();
333
+ const workspaceRulesRoot = await resolveWorkspaceRulesRoot();
334
+ const out = await verifyRuntimeClaimOnDemand({
335
+ repoPath,
336
+ queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
337
+ executeParameterized: makeExecuteParameterized(repoPath),
338
+ resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
339
+ rulesRoot: workspaceRulesRoot,
340
+ });
341
+ expect(out.rule_id).toBe('unity.gungraph.reload.output-getvalue.v1');
342
+ expect(out.rule_version).toBe('1.0.0');
343
+ });
344
+ it('phase2 next_action remains shell-parsable when unmatched', async () => {
345
+ const workspaceRulesRoot = await resolveWorkspaceRulesRoot();
346
+ const out = await verifyRuntimeClaimOnDemand({
347
+ repoPath: path.resolve('.'),
348
+ queryText: 'CompletelyUnrelatedChain',
349
+ executeParameterized: async () => [],
350
+ resourceBindings: [],
351
+ rulesRoot: workspaceRulesRoot,
352
+ });
353
+ expect(out.reason).toBe('rule_not_matched');
354
+ expect(typeof out.next_action).toBe('string');
355
+ expect(hasBalancedShellQuotes(String(out.next_action || ''))).toBe(true);
356
+ });
357
+ it('phase2 runtime claim required_hops are rule-driven', async () => {
358
+ const repoPath = await makeTempRepo();
359
+ const executeParameterized = makeExecuteParameterized(repoPath);
360
+ const strictExecuteParameterized = async (query, params) => {
361
+ const q = String(query || '');
362
+ if (q.includes("MATCH (n {id: $symbolId})-[:CodeRelation {type: 'HAS_METHOD'}]->(m)")) {
363
+ return [];
364
+ }
365
+ return executeParameterized(query, params);
366
+ };
367
+ const strictRulesRoot = await writeRules(repoPath, {
368
+ 'approved/demo.reload.strict.v1.yaml': [
369
+ 'id: demo.reload.strict.v1',
370
+ 'version: 1.0.0',
371
+ 'trigger_family: reload',
372
+ 'resource_types:',
373
+ ' - asset',
374
+ 'host_base_type:',
375
+ ' - ReloadBase',
376
+ 'required_hops:',
377
+ ' - resource',
378
+ ' - guid_map',
379
+ ' - code_loader',
380
+ ' - code_runtime',
381
+ 'guarantees:',
382
+ ' - strict_chain_closed',
383
+ 'non_guarantees:',
384
+ ' - strict_no_runtime_execution',
385
+ 'next_action: node strict',
386
+ ].join('\n'),
387
+ }, 'rules-strict');
388
+ const relaxedRulesRoot = await writeRules(repoPath, {
389
+ 'approved/demo.reload.relaxed.v1.yaml': [
390
+ 'id: demo.reload.relaxed.v1',
391
+ 'version: 1.0.0',
392
+ 'trigger_family: reload',
393
+ 'resource_types:',
394
+ ' - asset',
395
+ 'host_base_type:',
396
+ ' - ReloadBase',
397
+ 'required_hops:',
398
+ ' - resource',
399
+ ' - guid_map',
400
+ ' - code_loader',
401
+ 'guarantees:',
402
+ ' - relaxed_chain_closed',
403
+ 'non_guarantees:',
404
+ ' - relaxed_no_runtime_execution',
405
+ 'next_action: node relaxed',
406
+ ].join('\n'),
407
+ }, 'rules-relaxed');
408
+ const strict = await verifyRuntimeClaimOnDemand({
409
+ repoPath,
410
+ queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
411
+ executeParameterized: strictExecuteParameterized,
412
+ resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
413
+ rulesRoot: strictRulesRoot,
414
+ });
415
+ const relaxed = await verifyRuntimeClaimOnDemand({
416
+ repoPath,
417
+ queryText: 'Reload NEON.Game.Graph.Nodes.Reloads',
418
+ executeParameterized,
419
+ resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
420
+ rulesRoot: relaxedRulesRoot,
421
+ });
422
+ expect(strict.status).toBe('verified_partial');
423
+ expect(relaxed.status).toBe('verified_full');
424
+ await fs.rm(repoPath, { recursive: true, force: true });
425
+ });
426
+ it('phase2 runtime claim guarantees/non_guarantees come from matched rule', async () => {
427
+ const repoPath = await makeTempRepo();
428
+ const rulesRoot = await writeRules(repoPath, {
429
+ 'approved/demo.reload.claim-semantics.v1.yaml': [
430
+ 'id: demo.reload.claim-semantics.v1',
431
+ 'version: 2.0.0',
432
+ 'trigger_family: reload',
433
+ 'resource_types:',
434
+ ' - asset',
435
+ 'host_base_type:',
436
+ ' - ReloadBase',
437
+ 'required_hops:',
438
+ ' - resource',
439
+ ' - guid_map',
440
+ ' - code_loader',
441
+ ' - code_runtime',
442
+ 'guarantees:',
443
+ ' - custom_guarantee_from_rule',
444
+ 'non_guarantees:',
445
+ ' - custom_non_guarantee_from_rule',
446
+ 'next_action: node claim-semantics',
447
+ 'match:',
448
+ ' trigger_tokens:',
449
+ ' - reload',
450
+ 'topology:',
451
+ ' - hop: resource',
452
+ ' from:',
453
+ ' entity: resource',
454
+ ' to:',
455
+ ' entity: script',
456
+ ' edge:',
457
+ ' kind: binds_script',
458
+ 'closure:',
459
+ ' required_hops:',
460
+ ' - resource',
461
+ ' - guid_map',
462
+ ' - code_loader',
463
+ ' - code_runtime',
464
+ ' failure_map:',
465
+ ' missing_evidence: rule_matched_but_evidence_missing',
466
+ 'claims:',
467
+ ' guarantees:',
468
+ ' - custom_guarantee_from_rule',
469
+ ' non_guarantees:',
470
+ ' - custom_non_guarantee_from_rule',
471
+ ' next_action: node claim-semantics',
472
+ ].join('\n'),
473
+ });
474
+ const out = await verifyRuntimeClaimOnDemand({
475
+ repoPath,
476
+ queryText: 'Reload runtime start sequence',
477
+ executeParameterized: makeExecuteParameterized(repoPath),
478
+ resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
479
+ rulesRoot,
480
+ });
481
+ expect(out.rule_id).toBe('demo.reload.claim-semantics.v1');
482
+ expect(out.rule_version).toBe('2.0.0');
483
+ expect(out.guarantees).toEqual(['custom_guarantee_from_rule']);
484
+ expect(out.non_guarantees).toEqual(['custom_non_guarantee_from_rule']);
485
+ await fs.rm(repoPath, { recursive: true, force: true });
486
+ });
487
+ it('phase2 non-reload trigger family executes rule-driven verifier', async () => {
488
+ const repoPath = await makeTempRepo();
489
+ const rulesRoot = await writeRules(repoPath, {
490
+ 'approved/demo.startup.v1.yaml': [
491
+ 'id: demo.startup.v1',
492
+ 'version: 1.0.0',
493
+ 'trigger_family: startup',
494
+ 'resource_types:',
495
+ ' - asset',
496
+ 'host_base_type:',
497
+ ' - StartupNode',
498
+ 'required_hops:',
499
+ ' - resource',
500
+ 'guarantees:',
501
+ ' - startup_chain_closed',
502
+ 'non_guarantees:',
503
+ ' - startup_not_executed',
504
+ 'next_action: node startup',
505
+ ].join('\n'),
506
+ });
507
+ const out = await verifyRuntimeClaimOnDemand({
508
+ repoPath,
509
+ queryText: 'Startup Graph Trigger',
510
+ executeParameterized: makeExecuteParameterized(repoPath),
511
+ resourceBindings: [{ resourcePath: 'Assets/Custom/Rules/startup.asset' }],
512
+ rulesRoot,
513
+ });
514
+ expect(out.rule_id).toBe('demo.startup.v1');
515
+ expect(out.status).toBe('verified_full');
516
+ expect(out.evidence_level).toBe('verified_segment');
517
+ expect(out.reason).toBeUndefined();
518
+ await fs.rm(repoPath, { recursive: true, force: true });
519
+ });
520
+ it('phase2 rule_not_matched does not leak first rule next_action', async () => {
521
+ const repoPath = await makeTempRepo();
522
+ const rulesRoot = await writeRules(repoPath, {
523
+ 'approved/demo.startup.v1.yaml': [
524
+ 'id: demo.startup.v1',
525
+ 'version: 1.0.0',
526
+ 'trigger_family: startup',
527
+ 'resource_types:',
528
+ ' - asset',
529
+ 'host_base_type:',
530
+ ' - StartupNode',
531
+ 'required_hops:',
532
+ ' - resource',
533
+ 'guarantees:',
534
+ ' - startup_chain_closed',
535
+ 'non_guarantees:',
536
+ ' - startup_not_executed',
537
+ 'next_action: node startup-only-action',
538
+ ].join('\n'),
539
+ });
540
+ const out = await verifyRuntimeClaimOnDemand({
541
+ repoPath,
542
+ queryText: 'Reload runtime start sequence',
543
+ executeParameterized: makeExecuteParameterized(repoPath),
544
+ resourceBindings: [{ resourcePath: 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset' }],
545
+ rulesRoot,
546
+ });
547
+ expect(out.reason).toBe('rule_not_matched');
548
+ expect(String(out.next_action || '')).not.toContain('startup-only-action');
549
+ expect(String(out.next_action || '')).toContain('Reload runtime start sequence');
550
+ await fs.rm(repoPath, { recursive: true, force: true });
551
+ });
552
+ it('phase5 rule-lab promoted rule is loadable', async () => {
553
+ const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-chain-rule-lab-promote-'));
554
+ const sliceDir = path.join(repoPath, '.gitnexus', 'rules', 'lab', 'runs', 'run-x', 'slices', 'slice-a');
555
+ await fs.mkdir(sliceDir, { recursive: true });
556
+ await fs.writeFile(path.join(sliceDir, 'curated.json'), JSON.stringify({
557
+ run_id: 'run-x',
558
+ slice_id: 'slice-a',
559
+ curated: [
560
+ {
561
+ id: 'candidate-startup-1',
562
+ rule_id: 'demo.startup.v1',
563
+ title: 'startup startup graph',
564
+ confirmed_chain: {
565
+ steps: [{ hop_type: 'resource', anchor: 'Assets/Rules/startup.asset:1', snippet: 'Startup Graph Trigger' }],
566
+ },
567
+ guarantees: ['startup trigger matching is confirmed'],
568
+ non_guarantees: ['does not prove full runtime ordering'],
569
+ },
570
+ ],
571
+ }, null, 2), 'utf-8');
572
+ await promoteCuratedRules({ repoPath, runId: 'run-x', sliceId: 'slice-a', version: '1.0.0' });
573
+ const out = await verifyRuntimeClaimOnDemand({
574
+ repoPath,
575
+ queryText: 'Startup Graph Trigger',
576
+ executeParameterized: async () => [],
577
+ resourceBindings: [{ resourcePath: 'Assets/Rules/startup.asset' }],
578
+ });
579
+ expect(out.rule_id).toBe('demo.startup.v1');
580
+ expect(out.reason).toBeUndefined();
581
+ await fs.rm(repoPath, { recursive: true, force: true });
582
+ });
56
583
  });
@@ -0,0 +1,63 @@
1
+ import type { UnityResourceBinding, LifecycleOverrides } from '../../rule-lab/types.js';
2
+ export interface RuntimeClaimRuleCatalogEntry {
3
+ id: string;
4
+ version: string;
5
+ file?: string;
6
+ enabled?: boolean;
7
+ family?: 'analyze_rules' | 'verification_rules';
8
+ }
9
+ export interface RuntimeClaimRule {
10
+ id: string;
11
+ version: string;
12
+ trigger_family: string;
13
+ resource_types: string[];
14
+ host_base_type: string[];
15
+ match?: {
16
+ trigger_tokens: string[];
17
+ symbol_kind?: string[];
18
+ module_scope?: string[];
19
+ resource_types?: string[];
20
+ host_base_type?: string[];
21
+ };
22
+ required_hops: string[];
23
+ guarantees: string[];
24
+ non_guarantees: string[];
25
+ next_action?: string;
26
+ family?: 'analyze_rules' | 'verification_rules';
27
+ resource_bindings?: UnityResourceBinding[];
28
+ lifecycle_overrides?: LifecycleOverrides;
29
+ file_path: string;
30
+ topology?: Array<{
31
+ hop: string;
32
+ from: Record<string, unknown>;
33
+ to: Record<string, unknown>;
34
+ edge: {
35
+ kind: string;
36
+ };
37
+ constraints?: Record<string, unknown>;
38
+ }>;
39
+ closure?: {
40
+ required_hops: string[];
41
+ failure_map: Record<string, string>;
42
+ };
43
+ claims?: {
44
+ guarantees: string[];
45
+ non_guarantees: string[];
46
+ next_action: string;
47
+ };
48
+ }
49
+ export interface RuntimeClaimRuleRegistry {
50
+ repoPath: string;
51
+ rulesRoot: string;
52
+ catalogPath: string;
53
+ activeRules: RuntimeClaimRule[];
54
+ }
55
+ export type RuleRegistryLoadErrorCode = 'rule_catalog_missing' | 'rule_catalog_invalid' | 'rule_file_missing';
56
+ export declare class RuleRegistryLoadError extends Error {
57
+ code: RuleRegistryLoadErrorCode;
58
+ details?: Record<string, string>;
59
+ constructor(code: RuleRegistryLoadErrorCode, message: string, details?: Record<string, string>);
60
+ }
61
+ export declare function parseRuleYaml(raw: string, filePath: string): RuntimeClaimRule;
62
+ export declare function loadRuleRegistry(repoPath: string, rulesRoot?: string): Promise<RuntimeClaimRuleRegistry>;
63
+ export declare function loadAnalyzeRules(repoPath: string, rulesRoot?: string): Promise<RuntimeClaimRule[]>;