@veewo/gitnexus 1.3.11 → 1.4.6-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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -0,0 +1,363 @@
1
+ import { SupportedLanguages } from '../../config/supported-languages.js';
2
+ /**
3
+ * Walk a named-binding re-export chain through NamedImportMap.
4
+ *
5
+ * When file A imports { User } from B, and B re-exports { User } from C,
6
+ * the NamedImportMap for A points to B, but B has no User definition.
7
+ * This function follows the chain: A→B→C until a definition is found.
8
+ *
9
+ * Returns the definitions found at the end of the chain, or null if the
10
+ * chain breaks (missing binding, circular reference, or depth exceeded).
11
+ * Max depth 5 to prevent infinite loops.
12
+ *
13
+ * @param allDefs Pre-computed `symbolTable.lookupFuzzy(name)` result — must be the
14
+ * complete unfiltered result. Passing a file-filtered subset will cause
15
+ * silent misses at depth=0 for non-aliased bindings.
16
+ */
17
+ export function walkBindingChain(name, currentFilePath, symbolTable, namedImportMap, allDefs) {
18
+ let lookupFile = currentFilePath;
19
+ let lookupName = name;
20
+ const visited = new Set();
21
+ for (let depth = 0; depth < 5; depth++) {
22
+ const bindings = namedImportMap.get(lookupFile);
23
+ if (!bindings)
24
+ return null;
25
+ const binding = bindings.get(lookupName);
26
+ if (!binding)
27
+ return null;
28
+ const key = `${binding.sourcePath}:${binding.exportedName}`;
29
+ if (visited.has(key))
30
+ return null; // circular
31
+ visited.add(key);
32
+ const targetName = binding.exportedName;
33
+ const resolvedDefs = targetName !== lookupName || depth > 0
34
+ ? symbolTable.lookupFuzzy(targetName).filter(def => def.filePath === binding.sourcePath)
35
+ : allDefs.filter(def => def.filePath === binding.sourcePath);
36
+ if (resolvedDefs.length > 0)
37
+ return resolvedDefs;
38
+ // No definition in source file → follow re-export chain
39
+ lookupFile = binding.sourcePath;
40
+ lookupName = targetName;
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Extract named bindings from an import AST node.
46
+ * Returns undefined if the import is not a named import (e.g., import * or default).
47
+ *
48
+ * TS: import { User, Repo as R } from './models'
49
+ * → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
50
+ *
51
+ * Python: from models import User, Repo as R
52
+ * → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
53
+ */
54
+ export function extractNamedBindings(importNode, language) {
55
+ if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
56
+ return extractTsNamedBindings(importNode);
57
+ }
58
+ if (language === SupportedLanguages.Python) {
59
+ return extractPythonNamedBindings(importNode);
60
+ }
61
+ if (language === SupportedLanguages.Kotlin) {
62
+ return extractKotlinNamedBindings(importNode);
63
+ }
64
+ if (language === SupportedLanguages.Rust) {
65
+ return extractRustNamedBindings(importNode);
66
+ }
67
+ if (language === SupportedLanguages.PHP) {
68
+ return extractPhpNamedBindings(importNode);
69
+ }
70
+ if (language === SupportedLanguages.CSharp) {
71
+ return extractCsharpNamedBindings(importNode);
72
+ }
73
+ if (language === SupportedLanguages.Java) {
74
+ return extractJavaNamedBindings(importNode);
75
+ }
76
+ return undefined;
77
+ }
78
+ export function extractTsNamedBindings(importNode) {
79
+ // import_statement > import_clause > named_imports > import_specifier*
80
+ const importClause = findChild(importNode, 'import_clause');
81
+ if (importClause) {
82
+ const namedImports = findChild(importClause, 'named_imports');
83
+ if (!namedImports)
84
+ return undefined; // default import, namespace import, or side-effect
85
+ const bindings = [];
86
+ for (let i = 0; i < namedImports.namedChildCount; i++) {
87
+ const specifier = namedImports.namedChild(i);
88
+ if (specifier?.type !== 'import_specifier')
89
+ continue;
90
+ const identifiers = [];
91
+ for (let j = 0; j < specifier.namedChildCount; j++) {
92
+ const child = specifier.namedChild(j);
93
+ if (child?.type === 'identifier')
94
+ identifiers.push(child.text);
95
+ }
96
+ if (identifiers.length === 1) {
97
+ bindings.push({ local: identifiers[0], exported: identifiers[0] });
98
+ }
99
+ else if (identifiers.length === 2) {
100
+ // import { Foo as Bar } → exported='Foo', local='Bar'
101
+ bindings.push({ local: identifiers[1], exported: identifiers[0] });
102
+ }
103
+ }
104
+ return bindings.length > 0 ? bindings : undefined;
105
+ }
106
+ // Re-export: export { X } from './y' → export_statement > export_clause > export_specifier
107
+ const exportClause = findChild(importNode, 'export_clause');
108
+ if (exportClause) {
109
+ const bindings = [];
110
+ for (let i = 0; i < exportClause.namedChildCount; i++) {
111
+ const specifier = exportClause.namedChild(i);
112
+ if (specifier?.type !== 'export_specifier')
113
+ continue;
114
+ const identifiers = [];
115
+ for (let j = 0; j < specifier.namedChildCount; j++) {
116
+ const child = specifier.namedChild(j);
117
+ if (child?.type === 'identifier')
118
+ identifiers.push(child.text);
119
+ }
120
+ if (identifiers.length === 1) {
121
+ // export { User } from './base' → re-exports User as User
122
+ bindings.push({ local: identifiers[0], exported: identifiers[0] });
123
+ }
124
+ else if (identifiers.length === 2) {
125
+ // export { Repo as Repository } from './models' → name=Repo, alias=Repository
126
+ // For re-exports, the first id is the source name, second is what's exported
127
+ // When another file imports { Repository }, they get Repo from the source
128
+ bindings.push({ local: identifiers[1], exported: identifiers[0] });
129
+ }
130
+ }
131
+ return bindings.length > 0 ? bindings : undefined;
132
+ }
133
+ return undefined;
134
+ }
135
+ export function extractPythonNamedBindings(importNode) {
136
+ // Only from import_from_statement, not plain import_statement
137
+ if (importNode.type !== 'import_from_statement')
138
+ return undefined;
139
+ const bindings = [];
140
+ for (let i = 0; i < importNode.namedChildCount; i++) {
141
+ const child = importNode.namedChild(i);
142
+ if (!child)
143
+ continue;
144
+ if (child.type === 'dotted_name') {
145
+ // Skip the module_name (first dotted_name is the source module)
146
+ const fieldName = importNode.childForFieldName?.('module_name');
147
+ if (fieldName && child.startIndex === fieldName.startIndex)
148
+ continue;
149
+ // This is an imported name: from x import User
150
+ const name = child.text;
151
+ if (name)
152
+ bindings.push({ local: name, exported: name });
153
+ }
154
+ if (child.type === 'aliased_import') {
155
+ // from x import Repo as R
156
+ const dottedName = findChild(child, 'dotted_name');
157
+ const aliasIdent = findChild(child, 'identifier');
158
+ if (dottedName && aliasIdent) {
159
+ bindings.push({ local: aliasIdent.text, exported: dottedName.text });
160
+ }
161
+ }
162
+ }
163
+ return bindings.length > 0 ? bindings : undefined;
164
+ }
165
+ export function extractKotlinNamedBindings(importNode) {
166
+ // import_header > identifier + import_alias > simple_identifier
167
+ if (importNode.type !== 'import_header')
168
+ return undefined;
169
+ const fullIdent = findChild(importNode, 'identifier');
170
+ if (!fullIdent)
171
+ return undefined;
172
+ const fullText = fullIdent.text;
173
+ const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
174
+ const importAlias = findChild(importNode, 'import_alias');
175
+ if (importAlias) {
176
+ // Aliased: import com.example.User as U
177
+ const aliasIdent = findChild(importAlias, 'simple_identifier');
178
+ if (!aliasIdent)
179
+ return undefined;
180
+ return [{ local: aliasIdent.text, exported: exportedName }];
181
+ }
182
+ // Non-aliased: import com.example.User → local="User", exported="User"
183
+ // Skip wildcard imports (ending in *)
184
+ if (fullText.endsWith('.*') || fullText.endsWith('*'))
185
+ return undefined;
186
+ // Skip lowercase last segments — those are member/function imports (e.g.,
187
+ // import util.OneArg.writeAudit), not class imports. Multiple member imports
188
+ // with the same function name would collide in NamedImportMap, breaking
189
+ // arity-based disambiguation.
190
+ if (exportedName[0] && exportedName[0] === exportedName[0].toLowerCase())
191
+ return undefined;
192
+ return [{ local: exportedName, exported: exportedName }];
193
+ }
194
+ export function extractRustNamedBindings(importNode) {
195
+ // use_declaration may contain use_as_clause at any depth
196
+ if (importNode.type !== 'use_declaration')
197
+ return undefined;
198
+ const bindings = [];
199
+ collectRustBindings(importNode, bindings);
200
+ return bindings.length > 0 ? bindings : undefined;
201
+ }
202
+ function collectRustBindings(node, bindings) {
203
+ if (node.type === 'use_as_clause') {
204
+ // First identifier = exported name, second identifier = local alias
205
+ const idents = [];
206
+ for (let i = 0; i < node.namedChildCount; i++) {
207
+ const child = node.namedChild(i);
208
+ if (child?.type === 'identifier')
209
+ idents.push(child.text);
210
+ // For scoped_identifier, extract the last segment
211
+ if (child?.type === 'scoped_identifier') {
212
+ const nameNode = child.childForFieldName?.('name');
213
+ if (nameNode)
214
+ idents.push(nameNode.text);
215
+ }
216
+ }
217
+ if (idents.length === 2) {
218
+ bindings.push({ local: idents[1], exported: idents[0] });
219
+ }
220
+ return;
221
+ }
222
+ // Terminal identifier in a use_list: use crate::models::{User, Repo}
223
+ if (node.type === 'identifier' && node.parent?.type === 'use_list') {
224
+ bindings.push({ local: node.text, exported: node.text });
225
+ return;
226
+ }
227
+ // Skip scoped_identifier that serves as path prefix in scoped_use_list
228
+ // e.g. use crate::models::{User, Repo} — the path node "crate::models" is not an importable symbol
229
+ if (node.type === 'scoped_identifier' && node.parent?.type === 'scoped_use_list') {
230
+ return; // path prefix — the use_list sibling handles the actual symbols
231
+ }
232
+ // Terminal scoped_identifier: use crate::models::User;
233
+ // Only extract if this is a leaf (no deeper use_list/use_as_clause/scoped_use_list)
234
+ if (node.type === 'scoped_identifier') {
235
+ let hasDeeper = false;
236
+ for (let i = 0; i < node.namedChildCount; i++) {
237
+ const child = node.namedChild(i);
238
+ if (child?.type === 'use_list' || child?.type === 'use_as_clause' || child?.type === 'scoped_use_list') {
239
+ hasDeeper = true;
240
+ break;
241
+ }
242
+ }
243
+ if (!hasDeeper) {
244
+ const nameNode = node.childForFieldName?.('name');
245
+ if (nameNode) {
246
+ bindings.push({ local: nameNode.text, exported: nameNode.text });
247
+ }
248
+ return;
249
+ }
250
+ }
251
+ // Recurse into children
252
+ for (let i = 0; i < node.namedChildCount; i++) {
253
+ const child = node.namedChild(i);
254
+ if (child)
255
+ collectRustBindings(child, bindings);
256
+ }
257
+ }
258
+ export function extractPhpNamedBindings(importNode) {
259
+ // namespace_use_declaration > namespace_use_clause* (flat)
260
+ // namespace_use_declaration > namespace_use_group > namespace_use_clause* (grouped)
261
+ if (importNode.type !== 'namespace_use_declaration')
262
+ return undefined;
263
+ const bindings = [];
264
+ // Collect all clauses — from direct children AND from namespace_use_group
265
+ const clauses = [];
266
+ for (let i = 0; i < importNode.namedChildCount; i++) {
267
+ const child = importNode.namedChild(i);
268
+ if (child?.type === 'namespace_use_clause') {
269
+ clauses.push(child);
270
+ }
271
+ else if (child?.type === 'namespace_use_group') {
272
+ for (let j = 0; j < child.namedChildCount; j++) {
273
+ const groupChild = child.namedChild(j);
274
+ if (groupChild?.type === 'namespace_use_clause')
275
+ clauses.push(groupChild);
276
+ }
277
+ }
278
+ }
279
+ for (const clause of clauses) {
280
+ // Flat imports: qualified_name + name (alias)
281
+ let qualifiedName = null;
282
+ const names = [];
283
+ for (let j = 0; j < clause.namedChildCount; j++) {
284
+ const child = clause.namedChild(j);
285
+ if (child?.type === 'qualified_name')
286
+ qualifiedName = child;
287
+ else if (child?.type === 'name')
288
+ names.push(child);
289
+ }
290
+ if (qualifiedName && names.length > 0) {
291
+ // Flat aliased import: use App\Models\Repo as R;
292
+ const fullText = qualifiedName.text;
293
+ const exportedName = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
294
+ bindings.push({ local: names[0].text, exported: exportedName });
295
+ }
296
+ else if (qualifiedName && names.length === 0) {
297
+ // Flat non-aliased import: use App\Models\User;
298
+ const fullText = qualifiedName.text;
299
+ const lastSegment = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
300
+ bindings.push({ local: lastSegment, exported: lastSegment });
301
+ }
302
+ else if (!qualifiedName && names.length >= 2) {
303
+ // Grouped aliased import: {Repo as R} — first name = exported, second = alias
304
+ bindings.push({ local: names[1].text, exported: names[0].text });
305
+ }
306
+ else if (!qualifiedName && names.length === 1) {
307
+ // Grouped non-aliased import: {User} in use App\Models\{User, Repo as R}
308
+ bindings.push({ local: names[0].text, exported: names[0].text });
309
+ }
310
+ }
311
+ return bindings.length > 0 ? bindings : undefined;
312
+ }
313
+ export function extractCsharpNamedBindings(importNode) {
314
+ // using_directive with identifier (alias) + qualified_name (target)
315
+ if (importNode.type !== 'using_directive')
316
+ return undefined;
317
+ let aliasIdent = null;
318
+ let qualifiedName = null;
319
+ for (let i = 0; i < importNode.namedChildCount; i++) {
320
+ const child = importNode.namedChild(i);
321
+ if (child?.type === 'identifier' && !aliasIdent)
322
+ aliasIdent = child;
323
+ else if (child?.type === 'qualified_name')
324
+ qualifiedName = child;
325
+ }
326
+ if (!aliasIdent || !qualifiedName)
327
+ return undefined;
328
+ const fullText = qualifiedName.text;
329
+ const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
330
+ return [{ local: aliasIdent.text, exported: exportedName }];
331
+ }
332
+ export function extractJavaNamedBindings(importNode) {
333
+ // import_declaration > scoped_identifier "com.example.models.User"
334
+ // Wildcard imports (.*) don't produce named bindings
335
+ if (importNode.type !== 'import_declaration')
336
+ return undefined;
337
+ // Check for asterisk (wildcard import) — skip those
338
+ for (let i = 0; i < importNode.childCount; i++) {
339
+ const child = importNode.child(i);
340
+ if (child?.type === 'asterisk')
341
+ return undefined;
342
+ }
343
+ const scopedId = findChild(importNode, 'scoped_identifier');
344
+ if (!scopedId)
345
+ return undefined;
346
+ const fullText = scopedId.text;
347
+ const lastDot = fullText.lastIndexOf('.');
348
+ if (lastDot === -1)
349
+ return undefined;
350
+ const className = fullText.slice(lastDot + 1);
351
+ // Skip lowercase names — those are package imports, not class imports
352
+ if (className[0] && className[0] === className[0].toLowerCase())
353
+ return undefined;
354
+ return [{ local: className, exported: className }];
355
+ }
356
+ function findChild(node, type) {
357
+ for (let i = 0; i < node.namedChildCount; i++) {
358
+ const child = node.namedChild(i);
359
+ if (child?.type === type)
360
+ return child;
361
+ }
362
+ return null;
363
+ }
@@ -2,13 +2,16 @@ import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { SymbolTable } from './symbol-table.js';
3
3
  import { ASTCache } from './ast-cache.js';
4
4
  import { WorkerPool } from './workers/worker-pool.js';
5
- import type { ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js';
5
+ import type { ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
6
6
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
7
7
  export interface WorkerExtractedData {
8
8
  imports: ExtractedImport[];
9
9
  calls: ExtractedCall[];
10
10
  heritage: ExtractedHeritage[];
11
+ routes: ExtractedRoute[];
12
+ constructorBindings: FileConstructorBindings[];
11
13
  }
14
+ export { isNodeExported } from './export-detection.js';
12
15
  export declare const processParsing: (graph: KnowledgeGraph, files: {
13
16
  path: string;
14
17
  content: string;
@@ -1,103 +1,15 @@
1
1
  import Parser from 'tree-sitter';
2
- import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
2
+ import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
3
3
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
4
  import { generateId } from '../../lib/utils.js';
5
- import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
6
- // ============================================================================
7
- // EXPORT DETECTION - Language-specific visibility detection
8
- // ============================================================================
9
- /**
10
- * Check if a symbol (function, class, etc.) is exported/public
11
- * Handles all 9 supported languages with explicit logic
12
- *
13
- * @param node - The AST node for the symbol name
14
- * @param name - The symbol name
15
- * @param language - The programming language
16
- * @returns true if the symbol is exported/public
17
- */
18
- const isNodeExported = (node, name, language) => {
19
- let current = node;
20
- switch (language) {
21
- // JavaScript/TypeScript: Check for export keyword in ancestors
22
- case 'javascript':
23
- case 'typescript':
24
- while (current) {
25
- const type = current.type;
26
- if (type === 'export_statement' ||
27
- type === 'export_specifier' ||
28
- type === 'lexical_declaration' && current.parent?.type === 'export_statement') {
29
- return true;
30
- }
31
- // Also check if text starts with 'export '
32
- if (current.text?.startsWith('export ')) {
33
- return true;
34
- }
35
- current = current.parent;
36
- }
37
- return false;
38
- // Python: Public if no leading underscore (convention)
39
- case 'python':
40
- return !name.startsWith('_');
41
- // Java: Check for 'public' modifier
42
- // In tree-sitter Java, modifiers are siblings of the name node, not parents
43
- case 'java':
44
- while (current) {
45
- // Check if this node or any sibling is a 'modifiers' node containing 'public'
46
- if (current.parent) {
47
- const parent = current.parent;
48
- // Check all children of the parent for modifiers
49
- for (let i = 0; i < parent.childCount; i++) {
50
- const child = parent.child(i);
51
- if (child?.type === 'modifiers' && child.text?.includes('public')) {
52
- return true;
53
- }
54
- }
55
- // Also check if the parent's text starts with 'public' (fallback)
56
- if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') {
57
- if (parent.text?.trimStart().startsWith('public')) {
58
- return true;
59
- }
60
- }
61
- }
62
- current = current.parent;
63
- }
64
- return false;
65
- // C#: Check for 'public' modifier in ancestors
66
- case 'csharp':
67
- while (current) {
68
- if (current.type === 'modifier' || current.type === 'modifiers') {
69
- if (current.text?.includes('public'))
70
- return true;
71
- }
72
- current = current.parent;
73
- }
74
- return false;
75
- // Go: Uppercase first letter = exported
76
- case 'go':
77
- if (name.length === 0)
78
- return false;
79
- const first = name[0];
80
- // Must be uppercase letter (not a number or symbol)
81
- return first === first.toUpperCase() && first !== first.toLowerCase();
82
- // Rust: Check for 'pub' visibility modifier
83
- case 'rust':
84
- while (current) {
85
- if (current.type === 'visibility_modifier') {
86
- if (current.text?.includes('pub'))
87
- return true;
88
- }
89
- current = current.parent;
90
- }
91
- return false;
92
- // C/C++: No native export concept at language level
93
- // Entry points will be detected via name patterns (main, etc.)
94
- case 'c':
95
- case 'cpp':
96
- return false;
97
- default:
98
- return false;
99
- }
100
- };
5
+ import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
6
+ import { isNodeExported } from './export-detection.js';
7
+ import { detectFrameworkFromAST } from './framework-detection.js';
8
+ import { typeConfigs } from './type-extractors/index.js';
9
+ import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
10
+ // isNodeExported imported from ./export-detection.js (shared module)
11
+ // Re-export for backward compatibility with any external consumers
12
+ export { isNodeExported } from './export-detection.js';
101
13
  // ============================================================================
102
14
  // Worker-based parallel parsing
103
15
  // ============================================================================
@@ -110,7 +22,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
110
22
  parseableFiles.push({ path: file.path, content: file.content });
111
23
  }
112
24
  if (parseableFiles.length === 0)
113
- return { imports: [], calls: [], heritage: [] };
25
+ return { imports: [], calls: [], heritage: [], routes: [], constructorBindings: [] };
114
26
  const total = files.length;
115
27
  // Dispatch to worker pool — pool handles splitting into chunks and sub-batching
116
28
  const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
@@ -120,6 +32,8 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
120
32
  const allImports = [];
121
33
  const allCalls = [];
122
34
  const allHeritage = [];
35
+ const allRoutes = [];
36
+ const allConstructorBindings = [];
123
37
  for (const result of chunkResults) {
124
38
  for (const node of result.nodes) {
125
39
  graph.addNode({
@@ -132,15 +46,34 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
132
46
  graph.addRelationship(rel);
133
47
  }
134
48
  for (const sym of result.symbols) {
135
- symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type);
49
+ symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
50
+ parameterCount: sym.parameterCount,
51
+ returnType: sym.returnType,
52
+ ownerId: sym.ownerId,
53
+ });
136
54
  }
137
55
  allImports.push(...result.imports);
138
56
  allCalls.push(...result.calls);
139
57
  allHeritage.push(...result.heritage);
58
+ allRoutes.push(...result.routes);
59
+ allConstructorBindings.push(...result.constructorBindings);
60
+ }
61
+ // Merge and log skipped languages from workers
62
+ const skippedLanguages = new Map();
63
+ for (const result of chunkResults) {
64
+ for (const [lang, count] of Object.entries(result.skippedLanguages)) {
65
+ skippedLanguages.set(lang, (skippedLanguages.get(lang) || 0) + count);
66
+ }
67
+ }
68
+ if (skippedLanguages.size > 0) {
69
+ const summary = Array.from(skippedLanguages.entries())
70
+ .map(([lang, count]) => `${lang}: ${count}`)
71
+ .join(', ');
72
+ console.warn(` Skipped unsupported languages: ${summary}`);
140
73
  }
141
74
  // Final progress
142
75
  onFileProgress?.(total, total, 'done');
143
- return { imports: allImports, calls: allCalls, heritage: allHeritage };
76
+ return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes, constructorBindings: allConstructorBindings };
144
77
  };
145
78
  // ============================================================================
146
79
  // Sequential fallback (original implementation)
@@ -148,6 +81,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
148
81
  const processParsingSequential = async (graph, files, symbolTable, astCache, onFileProgress) => {
149
82
  const parser = await loadParser();
150
83
  const total = files.length;
84
+ const skippedLanguages = new Map();
151
85
  for (let i = 0; i < files.length; i++) {
152
86
  const file = files[i];
153
87
  onFileProgress?.(i + 1, total, file.path);
@@ -156,13 +90,23 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
156
90
  const language = getLanguageFromFilename(file.path);
157
91
  if (!language)
158
92
  continue;
159
- // Skip very large files they can crash tree-sitter or cause OOM
160
- if (file.content.length > 512 * 1024)
93
+ // Skip unsupported languages (e.g. Swift when tree-sitter-swift not installed)
94
+ if (!isLanguageAvailable(language)) {
95
+ skippedLanguages.set(language, (skippedLanguages.get(language) || 0) + 1);
161
96
  continue;
162
- await loadLanguage(language, file.path);
97
+ }
98
+ // Skip files larger than the max tree-sitter buffer (32 MB)
99
+ if (file.content.length > TREE_SITTER_MAX_BUFFER)
100
+ continue;
101
+ try {
102
+ await loadLanguage(language, file.path);
103
+ }
104
+ catch {
105
+ continue; // parser unavailable — safety net
106
+ }
163
107
  let tree;
164
108
  try {
165
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
109
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
166
110
  }
167
111
  catch (parseError) {
168
112
  console.warn(`Skipping unparseable file: ${file.path}`);
@@ -196,9 +140,10 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
196
140
  return;
197
141
  }
198
142
  const nameNode = captureMap['name'];
199
- if (!nameNode)
143
+ // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
144
+ if (!nameNode && !captureMap['definition.constructor'])
200
145
  return;
201
- const nodeName = nameNode.text;
146
+ const nodeName = nameNode ? nameNode.text : 'init';
202
147
  let nodeLabel = 'CodeElement';
203
148
  if (captureMap['definition.function'])
204
149
  nodeLabel = 'Function';
@@ -244,7 +189,27 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
244
189
  nodeLabel = 'Constructor';
245
190
  else if (captureMap['definition.template'])
246
191
  nodeLabel = 'Template';
192
+ const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap);
193
+ const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
247
194
  const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
195
+ const definitionNode = getDefinitionNodeFromCaptures(captureMap);
196
+ const frameworkHint = definitionNode
197
+ ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
198
+ : null;
199
+ // Extract method signature for Method/Constructor nodes
200
+ const methodSig = (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
201
+ ? extractMethodSignature(definitionNode)
202
+ : undefined;
203
+ // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
204
+ // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
205
+ if (methodSig && (!methodSig.returnType || methodSig.returnType === 'array' || methodSig.returnType === 'iterable') && definitionNode) {
206
+ const tc = typeConfigs[language];
207
+ if (tc?.extractReturnType) {
208
+ const docReturn = tc.extractReturnType(definitionNode);
209
+ if (docReturn)
210
+ methodSig.returnType = docReturn;
211
+ }
212
+ }
248
213
  const node = {
249
214
  id: nodeId,
250
215
  label: nodeLabel,
@@ -254,11 +219,27 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
254
219
  startLine: nameNode.startPosition.row + 1,
255
220
  endLine: nameNode.endPosition.row + 1,
256
221
  language: language,
257
- isExported: isNodeExported(nameNode, nodeName, language),
258
- }
222
+ isExported: isNodeExported(nameNode || definitionNodeForRange, nodeName, language),
223
+ ...(frameworkHint ? {
224
+ astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
225
+ astFrameworkReason: frameworkHint.reason,
226
+ } : {}),
227
+ ...(methodSig ? {
228
+ parameterCount: methodSig.parameterCount,
229
+ returnType: methodSig.returnType,
230
+ } : {}),
231
+ },
259
232
  };
260
233
  graph.addNode(node);
261
- symbolTable.add(file.path, nodeName, nodeId, nodeLabel);
234
+ // Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
235
+ // Function is included because Kotlin/Rust/Python capture class methods as Function nodes
236
+ const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
237
+ const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
238
+ symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
239
+ parameterCount: methodSig?.parameterCount,
240
+ returnType: methodSig?.returnType,
241
+ ownerId: enclosingClassId ?? undefined,
242
+ });
262
243
  const fileId = generateId('File', file.path);
263
244
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
264
245
  const relationship = {
@@ -270,8 +251,25 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
270
251
  reason: '',
271
252
  };
272
253
  graph.addRelationship(relationship);
254
+ // ── HAS_METHOD: link method/constructor/property to enclosing class ──
255
+ if (enclosingClassId) {
256
+ graph.addRelationship({
257
+ id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
258
+ sourceId: enclosingClassId,
259
+ targetId: nodeId,
260
+ type: 'HAS_METHOD',
261
+ confidence: 1.0,
262
+ reason: '',
263
+ });
264
+ }
273
265
  });
274
266
  }
267
+ if (skippedLanguages.size > 0) {
268
+ const summary = Array.from(skippedLanguages.entries())
269
+ .map(([lang, count]) => `${lang}: ${count}`)
270
+ .join(', ');
271
+ console.warn(` Skipped unsupported languages: ${summary}`);
272
+ }
275
273
  };
276
274
  // ============================================================================
277
275
  // Public API