gitnexus 1.3.5 → 1.3.7

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 (47) hide show
  1. package/dist/cli/ai-context.js +77 -23
  2. package/dist/cli/analyze.js +0 -5
  3. package/dist/cli/eval-server.d.ts +7 -0
  4. package/dist/cli/eval-server.js +16 -7
  5. package/dist/cli/index.js +6 -21
  6. package/dist/cli/mcp.js +2 -0
  7. package/dist/cli/setup.js +6 -1
  8. package/dist/config/supported-languages.d.ts +1 -0
  9. package/dist/config/supported-languages.js +1 -0
  10. package/dist/core/ingestion/call-processor.d.ts +5 -1
  11. package/dist/core/ingestion/call-processor.js +78 -0
  12. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  13. package/dist/core/ingestion/framework-detection.js +49 -2
  14. package/dist/core/ingestion/import-processor.js +90 -39
  15. package/dist/core/ingestion/parsing-processor.d.ts +12 -1
  16. package/dist/core/ingestion/parsing-processor.js +92 -51
  17. package/dist/core/ingestion/pipeline.js +21 -2
  18. package/dist/core/ingestion/process-processor.js +0 -1
  19. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
  20. package/dist/core/ingestion/tree-sitter-queries.js +80 -0
  21. package/dist/core/ingestion/utils.d.ts +5 -0
  22. package/dist/core/ingestion/utils.js +20 -0
  23. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  24. package/dist/core/ingestion/workers/parse-worker.js +481 -52
  25. package/dist/core/kuzu/csv-generator.d.ts +4 -0
  26. package/dist/core/kuzu/csv-generator.js +23 -9
  27. package/dist/core/kuzu/kuzu-adapter.js +9 -3
  28. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  29. package/dist/core/tree-sitter/parser-loader.js +12 -2
  30. package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
  31. package/dist/mcp/core/kuzu-adapter.js +79 -16
  32. package/dist/mcp/local/local-backend.d.ts +13 -0
  33. package/dist/mcp/local/local-backend.js +148 -105
  34. package/dist/mcp/server.js +26 -11
  35. package/dist/storage/git.js +4 -1
  36. package/dist/storage/repo-manager.js +16 -2
  37. package/hooks/claude/gitnexus-hook.cjs +28 -8
  38. package/hooks/claude/pre-tool-use.sh +2 -1
  39. package/package.json +14 -4
  40. package/dist/cli/claude-hooks.d.ts +0 -22
  41. package/dist/cli/claude-hooks.js +0 -97
  42. package/dist/cli/view.d.ts +0 -13
  43. package/dist/cli/view.js +0 -59
  44. package/dist/core/graph/html-graph-viewer.d.ts +0 -15
  45. package/dist/core/graph/html-graph-viewer.js +0 -542
  46. package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
  47. package/dist/core/graph/html-graph-viewer.test.js +0 -67
@@ -9,11 +9,19 @@ import CPP from 'tree-sitter-cpp';
9
9
  import CSharp from 'tree-sitter-c-sharp';
10
10
  import Go from 'tree-sitter-go';
11
11
  import Rust from 'tree-sitter-rust';
12
+ import Kotlin from 'tree-sitter-kotlin';
12
13
  import PHP from 'tree-sitter-php';
13
- import Swift from 'tree-sitter-swift';
14
+ import { createRequire } from 'node:module';
14
15
  import { SupportedLanguages } from '../../../config/supported-languages.js';
15
16
  import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
16
- import { getLanguageFromFilename } from '../utils.js';
17
+ // tree-sitter-swift is an optionalDependency — may not be installed
18
+ const _require = createRequire(import.meta.url);
19
+ let Swift = null;
20
+ try {
21
+ Swift = _require('tree-sitter-swift');
22
+ }
23
+ catch { }
24
+ import { findSiblingChild, getLanguageFromFilename } from '../utils.js';
17
25
  import { detectFrameworkFromAST } from '../framework-detection.js';
18
26
  import { generateId } from '../../../lib/utils.js';
19
27
  // ============================================================================
@@ -31,8 +39,9 @@ const languageMap = {
31
39
  [SupportedLanguages.CSharp]: CSharp,
32
40
  [SupportedLanguages.Go]: Go,
33
41
  [SupportedLanguages.Rust]: Rust,
42
+ [SupportedLanguages.Kotlin]: Kotlin,
34
43
  [SupportedLanguages.PHP]: PHP.php_only,
35
- [SupportedLanguages.Swift]: Swift,
44
+ ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
36
45
  };
37
46
  const setLanguage = (language, filePath) => {
38
47
  const key = language === SupportedLanguages.TypeScript && filePath.endsWith('.tsx')
@@ -108,18 +117,26 @@ const isNodeExported = (node, name, language) => {
108
117
  current = current.parent;
109
118
  }
110
119
  return false;
111
- case 'c':
112
- case 'cpp':
113
- return false;
114
- case 'swift':
120
+ // Kotlin: Default visibility is public (unlike Java)
121
+ // visibility_modifier is inside modifiers, a sibling of the name node within the declaration
122
+ case 'kotlin':
115
123
  while (current) {
116
- if (current.type === 'modifiers' || current.type === 'visibility_modifier') {
117
- const text = current.text || '';
118
- if (text.includes('public') || text.includes('open'))
119
- return true;
124
+ if (current.parent) {
125
+ const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier');
126
+ if (visMod) {
127
+ const text = visMod.text;
128
+ if (text === 'private' || text === 'internal' || text === 'protected')
129
+ return false;
130
+ if (text === 'public')
131
+ return true;
132
+ }
120
133
  }
121
134
  current = current.parent;
122
135
  }
136
+ // No visibility modifier = public (Kotlin default)
137
+ return true;
138
+ case 'c':
139
+ case 'cpp':
123
140
  return false;
124
141
  case 'php':
125
142
  // Top-level classes/interfaces/traits are always accessible
@@ -138,6 +155,16 @@ const isNodeExported = (node, name, language) => {
138
155
  }
139
156
  // Top-level functions (no parent class) are globally accessible
140
157
  return true;
158
+ case 'swift':
159
+ while (current) {
160
+ if (current.type === 'modifiers' || current.type === 'visibility_modifier') {
161
+ const text = current.text || '';
162
+ if (text.includes('public') || text.includes('open'))
163
+ return true;
164
+ }
165
+ current = current.parent;
166
+ }
167
+ return false;
141
168
  default:
142
169
  return false;
143
170
  }
@@ -151,8 +178,12 @@ const FUNCTION_NODE_TYPES = new Set([
151
178
  'function_definition', 'async_function_declaration', 'async_arrow_function',
152
179
  'method_declaration', 'constructor_declaration',
153
180
  'local_function_statement', 'function_item', 'impl_item',
154
- 'anonymous_function_creation_expression', // PHP anonymous functions
155
- 'init_declaration', 'deinit_declaration', // Swift initializers/deinitializers
181
+ // Kotlin
182
+ 'lambda_literal',
183
+ // PHP
184
+ 'anonymous_function',
185
+ // Swift initializers/deinitializers
186
+ 'init_declaration', 'deinit_declaration',
156
187
  ]);
157
188
  /** Walk up AST to find enclosing function, return its generateId or null for top-level */
158
189
  const findEnclosingFunctionId = (node, filePath) => {
@@ -234,6 +265,22 @@ const BUILT_INS = new Set([
234
265
  'open', 'read', 'write', 'close', 'append', 'extend', 'update',
235
266
  'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
236
267
  'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
268
+ // Kotlin stdlib (IMPORTANT: keep in sync with call-processor.ts BUILT_IN_NAMES)
269
+ 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error',
270
+ 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf',
271
+ 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless',
272
+ 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet',
273
+ 'repeat', 'synchronized',
274
+ // Kotlin coroutine builders & scope functions
275
+ 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope',
276
+ 'supervisorScope', 'delay',
277
+ // Kotlin Flow operators
278
+ 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch',
279
+ 'buffer', 'conflate', 'distinctUntilChanged',
280
+ 'flatMapLatest', 'flatMapMerge', 'combine',
281
+ 'stateIn', 'shareIn', 'launchIn',
282
+ // Kotlin infix stdlib functions
283
+ 'to', 'until', 'downTo', 'step',
237
284
  // C/C++ standard library
238
285
  'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
239
286
  'scanf', 'fscanf', 'sscanf',
@@ -359,37 +406,49 @@ const getLabelFromCaptures = (captureMap) => {
359
406
  return 'Template';
360
407
  return 'CodeElement';
361
408
  };
409
+ const DEFINITION_CAPTURE_KEYS = [
410
+ 'definition.function',
411
+ 'definition.class',
412
+ 'definition.interface',
413
+ 'definition.method',
414
+ 'definition.struct',
415
+ 'definition.enum',
416
+ 'definition.namespace',
417
+ 'definition.module',
418
+ 'definition.trait',
419
+ 'definition.impl',
420
+ 'definition.type',
421
+ 'definition.const',
422
+ 'definition.static',
423
+ 'definition.typedef',
424
+ 'definition.macro',
425
+ 'definition.union',
426
+ 'definition.property',
427
+ 'definition.record',
428
+ 'definition.delegate',
429
+ 'definition.annotation',
430
+ 'definition.constructor',
431
+ 'definition.template',
432
+ ];
362
433
  const getDefinitionNodeFromCaptures = (captureMap) => {
363
- const definitionKeys = [
364
- 'definition.function',
365
- 'definition.class',
366
- 'definition.interface',
367
- 'definition.method',
368
- 'definition.struct',
369
- 'definition.enum',
370
- 'definition.namespace',
371
- 'definition.module',
372
- 'definition.trait',
373
- 'definition.impl',
374
- 'definition.type',
375
- 'definition.const',
376
- 'definition.static',
377
- 'definition.typedef',
378
- 'definition.macro',
379
- 'definition.union',
380
- 'definition.property',
381
- 'definition.record',
382
- 'definition.delegate',
383
- 'definition.annotation',
384
- 'definition.constructor',
385
- 'definition.template',
386
- ];
387
- for (const key of definitionKeys) {
434
+ for (const key of DEFINITION_CAPTURE_KEYS) {
388
435
  if (captureMap[key])
389
436
  return captureMap[key];
390
437
  }
391
438
  return null;
392
439
  };
440
+ /**
441
+ * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
442
+ * Pure function — returns a new string without mutating the input.
443
+ */
444
+ const appendKotlinWildcard = (importPath, importNode) => {
445
+ for (let i = 0; i < importNode.childCount; i++) {
446
+ if (importNode.child(i)?.type === 'wildcard_import') {
447
+ return importPath.endsWith('.*') ? importPath : `${importPath}.*`;
448
+ }
449
+ }
450
+ return importPath;
451
+ };
393
452
  // ============================================================================
394
453
  // Process a batch of files
395
454
  // ============================================================================
@@ -401,6 +460,7 @@ const processBatch = (files, onProgress) => {
401
460
  imports: [],
402
461
  calls: [],
403
462
  heritage: [],
463
+ routes: [],
404
464
  fileCount: 0,
405
465
  };
406
466
  // Group by language to minimize setLanguage calls
@@ -448,13 +508,23 @@ const processBatch = (files, onProgress) => {
448
508
  }
449
509
  // Process regular files for this language
450
510
  if (regularFiles.length > 0) {
451
- setLanguage(language, regularFiles[0].path);
452
- processFileGroup(regularFiles, language, queryString, result, onFileProcessed);
511
+ try {
512
+ setLanguage(language, regularFiles[0].path);
513
+ processFileGroup(regularFiles, language, queryString, result, onFileProcessed);
514
+ }
515
+ catch {
516
+ // parser unavailable — skip this language group
517
+ }
453
518
  }
454
519
  // Process tsx files separately (different grammar)
455
520
  if (tsxFiles.length > 0) {
456
- setLanguage(language, tsxFiles[0].path);
457
- processFileGroup(tsxFiles, language, queryString, result, onFileProcessed);
521
+ try {
522
+ setLanguage(language, tsxFiles[0].path);
523
+ processFileGroup(tsxFiles, language, queryString, result, onFileProcessed);
524
+ }
525
+ catch {
526
+ // parser unavailable — skip this language group
527
+ }
458
528
  }
459
529
  }
460
530
  return result;
@@ -563,6 +633,353 @@ function extractEloquentRelationDescription(methodNode) {
563
633
  return relType;
564
634
  return null;
565
635
  }
636
+ const ROUTE_HTTP_METHODS = new Set([
637
+ 'get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match',
638
+ ]);
639
+ const ROUTE_RESOURCE_METHODS = new Set(['resource', 'apiResource']);
640
+ const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
641
+ const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
642
+ /** Check if node is a scoped_call_expression with object 'Route' */
643
+ function isRouteStaticCall(node) {
644
+ if (node.type !== 'scoped_call_expression')
645
+ return false;
646
+ const obj = node.childForFieldName?.('object') ?? node.children?.[0];
647
+ return obj?.text === 'Route';
648
+ }
649
+ /** Get the method name from a scoped_call_expression or member_call_expression */
650
+ function getCallMethodName(node) {
651
+ const nameNode = node.childForFieldName?.('name') ??
652
+ node.children?.find((c) => c.type === 'name');
653
+ return nameNode?.text ?? null;
654
+ }
655
+ /** Get the arguments node from a call expression */
656
+ function getArguments(node) {
657
+ return node.children?.find((c) => c.type === 'arguments') ?? null;
658
+ }
659
+ /** Find the closure body inside arguments */
660
+ function findClosureBody(argsNode) {
661
+ if (!argsNode)
662
+ return null;
663
+ for (const child of argsNode.children ?? []) {
664
+ if (child.type === 'argument') {
665
+ for (const inner of child.children ?? []) {
666
+ if (inner.type === 'anonymous_function' ||
667
+ inner.type === 'arrow_function') {
668
+ return inner.childForFieldName?.('body') ??
669
+ inner.children?.find((c) => c.type === 'compound_statement');
670
+ }
671
+ }
672
+ }
673
+ if (child.type === 'anonymous_function' ||
674
+ child.type === 'arrow_function') {
675
+ return child.childForFieldName?.('body') ??
676
+ child.children?.find((c) => c.type === 'compound_statement');
677
+ }
678
+ }
679
+ return null;
680
+ }
681
+ /** Extract first string argument from arguments node */
682
+ function extractFirstStringArg(argsNode) {
683
+ if (!argsNode)
684
+ return null;
685
+ for (const child of argsNode.children ?? []) {
686
+ const target = child.type === 'argument' ? child.children?.[0] : child;
687
+ if (!target)
688
+ continue;
689
+ if (target.type === 'string' || target.type === 'encapsed_string') {
690
+ return extractStringContent(target);
691
+ }
692
+ }
693
+ return null;
694
+ }
695
+ /** Extract middleware from arguments — handles string or array */
696
+ function extractMiddlewareArg(argsNode) {
697
+ if (!argsNode)
698
+ return [];
699
+ for (const child of argsNode.children ?? []) {
700
+ const target = child.type === 'argument' ? child.children?.[0] : child;
701
+ if (!target)
702
+ continue;
703
+ if (target.type === 'string' || target.type === 'encapsed_string') {
704
+ const val = extractStringContent(target);
705
+ return val ? [val] : [];
706
+ }
707
+ if (target.type === 'array_creation_expression') {
708
+ const items = [];
709
+ for (const el of target.children ?? []) {
710
+ if (el.type === 'array_element_initializer') {
711
+ const str = el.children?.find((c) => c.type === 'string' || c.type === 'encapsed_string');
712
+ const val = str ? extractStringContent(str) : null;
713
+ if (val)
714
+ items.push(val);
715
+ }
716
+ }
717
+ return items;
718
+ }
719
+ }
720
+ return [];
721
+ }
722
+ /** Extract Controller::class from arguments */
723
+ function extractClassArg(argsNode) {
724
+ if (!argsNode)
725
+ return null;
726
+ for (const child of argsNode.children ?? []) {
727
+ const target = child.type === 'argument' ? child.children?.[0] : child;
728
+ if (target?.type === 'class_constant_access_expression') {
729
+ return target.children?.find((c) => c.type === 'name')?.text ?? null;
730
+ }
731
+ }
732
+ return null;
733
+ }
734
+ /** Extract controller class name from arguments: [Controller::class, 'method'] or 'Controller@method' */
735
+ function extractControllerTarget(argsNode) {
736
+ if (!argsNode)
737
+ return { controller: null, method: null };
738
+ const args = [];
739
+ for (const child of argsNode.children ?? []) {
740
+ if (child.type === 'argument')
741
+ args.push(child.children?.[0]);
742
+ else if (child.type !== '(' && child.type !== ')' && child.type !== ',')
743
+ args.push(child);
744
+ }
745
+ // Second arg is the handler
746
+ const handlerNode = args[1];
747
+ if (!handlerNode)
748
+ return { controller: null, method: null };
749
+ // Array syntax: [UserController::class, 'index']
750
+ if (handlerNode.type === 'array_creation_expression') {
751
+ let controller = null;
752
+ let method = null;
753
+ const elements = [];
754
+ for (const el of handlerNode.children ?? []) {
755
+ if (el.type === 'array_element_initializer')
756
+ elements.push(el);
757
+ }
758
+ if (elements[0]) {
759
+ const classAccess = findDescendant(elements[0], 'class_constant_access_expression');
760
+ if (classAccess) {
761
+ controller = classAccess.children?.find((c) => c.type === 'name')?.text ?? null;
762
+ }
763
+ }
764
+ if (elements[1]) {
765
+ const str = findDescendant(elements[1], 'string');
766
+ method = str ? extractStringContent(str) : null;
767
+ }
768
+ return { controller, method };
769
+ }
770
+ // String syntax: 'UserController@index'
771
+ if (handlerNode.type === 'string' || handlerNode.type === 'encapsed_string') {
772
+ const text = extractStringContent(handlerNode);
773
+ if (text?.includes('@')) {
774
+ const [controller, method] = text.split('@');
775
+ return { controller, method };
776
+ }
777
+ }
778
+ // Class reference: UserController::class (invokable controller)
779
+ if (handlerNode.type === 'class_constant_access_expression') {
780
+ const controller = handlerNode.children?.find((c) => c.type === 'name')?.text ?? null;
781
+ return { controller, method: '__invoke' };
782
+ }
783
+ return { controller: null, method: null };
784
+ }
785
+ /**
786
+ * Unwrap a chained call like Route::middleware('auth')->prefix('api')->group(fn)
787
+ */
788
+ function unwrapRouteChain(node) {
789
+ if (node.type !== 'member_call_expression')
790
+ return null;
791
+ const terminalMethod = getCallMethodName(node);
792
+ if (!terminalMethod)
793
+ return null;
794
+ const terminalArgs = getArguments(node);
795
+ const attributes = [];
796
+ let current = node.children?.[0];
797
+ while (current) {
798
+ if (current.type === 'member_call_expression') {
799
+ const method = getCallMethodName(current);
800
+ const args = getArguments(current);
801
+ if (method)
802
+ attributes.unshift({ method, argsNode: args });
803
+ current = current.children?.[0];
804
+ }
805
+ else if (current.type === 'scoped_call_expression') {
806
+ const obj = current.childForFieldName?.('object') ?? current.children?.[0];
807
+ if (obj?.text !== 'Route')
808
+ return null;
809
+ const method = getCallMethodName(current);
810
+ const args = getArguments(current);
811
+ if (method)
812
+ attributes.unshift({ method, argsNode: args });
813
+ return { isRouteFacade: true, terminalMethod, attributes, terminalArgs, node };
814
+ }
815
+ else {
816
+ break;
817
+ }
818
+ }
819
+ return null;
820
+ }
821
+ /** Parse Route::group(['middleware' => ..., 'prefix' => ...], fn) array syntax */
822
+ function parseArrayGroupArgs(argsNode) {
823
+ const ctx = { middleware: [], prefix: null, controller: null };
824
+ if (!argsNode)
825
+ return ctx;
826
+ for (const child of argsNode.children ?? []) {
827
+ const target = child.type === 'argument' ? child.children?.[0] : child;
828
+ if (target?.type === 'array_creation_expression') {
829
+ for (const el of target.children ?? []) {
830
+ if (el.type !== 'array_element_initializer')
831
+ continue;
832
+ const children = el.children ?? [];
833
+ const arrowIdx = children.findIndex((c) => c.type === '=>');
834
+ if (arrowIdx === -1)
835
+ continue;
836
+ const key = extractStringContent(children[arrowIdx - 1]);
837
+ const val = children[arrowIdx + 1];
838
+ if (key === 'middleware') {
839
+ if (val?.type === 'string') {
840
+ const s = extractStringContent(val);
841
+ if (s)
842
+ ctx.middleware.push(s);
843
+ }
844
+ else if (val?.type === 'array_creation_expression') {
845
+ for (const item of val.children ?? []) {
846
+ if (item.type === 'array_element_initializer') {
847
+ const str = item.children?.find((c) => c.type === 'string');
848
+ const s = str ? extractStringContent(str) : null;
849
+ if (s)
850
+ ctx.middleware.push(s);
851
+ }
852
+ }
853
+ }
854
+ }
855
+ else if (key === 'prefix') {
856
+ ctx.prefix = extractStringContent(val) ?? null;
857
+ }
858
+ else if (key === 'controller') {
859
+ if (val?.type === 'class_constant_access_expression') {
860
+ ctx.controller = val.children?.find((c) => c.type === 'name')?.text ?? null;
861
+ }
862
+ }
863
+ }
864
+ }
865
+ }
866
+ return ctx;
867
+ }
868
+ function extractLaravelRoutes(tree, filePath) {
869
+ const routes = [];
870
+ function resolveStack(stack) {
871
+ const middleware = [];
872
+ let prefix = null;
873
+ let controller = null;
874
+ for (const ctx of stack) {
875
+ middleware.push(...ctx.middleware);
876
+ if (ctx.prefix)
877
+ prefix = prefix ? `${prefix}/${ctx.prefix}`.replace(/\/+/g, '/') : ctx.prefix;
878
+ if (ctx.controller)
879
+ controller = ctx.controller;
880
+ }
881
+ return { middleware, prefix, controller };
882
+ }
883
+ function emitRoute(httpMethod, argsNode, lineNumber, groupStack, chainAttrs) {
884
+ const effective = resolveStack(groupStack);
885
+ for (const attr of chainAttrs) {
886
+ if (attr.method === 'middleware')
887
+ effective.middleware.push(...extractMiddlewareArg(attr.argsNode));
888
+ if (attr.method === 'prefix') {
889
+ const p = extractFirstStringArg(attr.argsNode);
890
+ if (p)
891
+ effective.prefix = effective.prefix ? `${effective.prefix}/${p}` : p;
892
+ }
893
+ if (attr.method === 'controller') {
894
+ const cls = extractClassArg(attr.argsNode);
895
+ if (cls)
896
+ effective.controller = cls;
897
+ }
898
+ }
899
+ const routePath = extractFirstStringArg(argsNode);
900
+ if (ROUTE_RESOURCE_METHODS.has(httpMethod)) {
901
+ const target = extractControllerTarget(argsNode);
902
+ const actions = httpMethod === 'apiResource' ? API_RESOURCE_ACTIONS : RESOURCE_ACTIONS;
903
+ for (const action of actions) {
904
+ routes.push({
905
+ filePath, httpMethod, routePath,
906
+ controllerName: target.controller ?? effective.controller,
907
+ methodName: action,
908
+ middleware: [...effective.middleware],
909
+ prefix: effective.prefix,
910
+ lineNumber,
911
+ });
912
+ }
913
+ }
914
+ else {
915
+ const target = extractControllerTarget(argsNode);
916
+ routes.push({
917
+ filePath, httpMethod, routePath,
918
+ controllerName: target.controller ?? effective.controller,
919
+ methodName: target.method,
920
+ middleware: [...effective.middleware],
921
+ prefix: effective.prefix,
922
+ lineNumber,
923
+ });
924
+ }
925
+ }
926
+ function walk(node, groupStack) {
927
+ // Case 1: Simple Route::get(...), Route::post(...), etc.
928
+ if (isRouteStaticCall(node)) {
929
+ const method = getCallMethodName(node);
930
+ if (method && (ROUTE_HTTP_METHODS.has(method) || ROUTE_RESOURCE_METHODS.has(method))) {
931
+ emitRoute(method, getArguments(node), node.startPosition.row, groupStack, []);
932
+ return;
933
+ }
934
+ if (method === 'group') {
935
+ const argsNode = getArguments(node);
936
+ const groupCtx = parseArrayGroupArgs(argsNode);
937
+ const body = findClosureBody(argsNode);
938
+ if (body) {
939
+ groupStack.push(groupCtx);
940
+ walkChildren(body, groupStack);
941
+ groupStack.pop();
942
+ }
943
+ return;
944
+ }
945
+ }
946
+ // Case 2: Fluent chain — Route::middleware(...)->group(...) or Route::middleware(...)->get(...)
947
+ const chain = unwrapRouteChain(node);
948
+ if (chain) {
949
+ if (chain.terminalMethod === 'group') {
950
+ const groupCtx = { middleware: [], prefix: null, controller: null };
951
+ for (const attr of chain.attributes) {
952
+ if (attr.method === 'middleware')
953
+ groupCtx.middleware.push(...extractMiddlewareArg(attr.argsNode));
954
+ if (attr.method === 'prefix')
955
+ groupCtx.prefix = extractFirstStringArg(attr.argsNode);
956
+ if (attr.method === 'controller')
957
+ groupCtx.controller = extractClassArg(attr.argsNode);
958
+ }
959
+ const body = findClosureBody(chain.terminalArgs);
960
+ if (body) {
961
+ groupStack.push(groupCtx);
962
+ walkChildren(body, groupStack);
963
+ groupStack.pop();
964
+ }
965
+ return;
966
+ }
967
+ if (ROUTE_HTTP_METHODS.has(chain.terminalMethod) || ROUTE_RESOURCE_METHODS.has(chain.terminalMethod)) {
968
+ emitRoute(chain.terminalMethod, chain.terminalArgs, node.startPosition.row, groupStack, chain.attributes);
969
+ return;
970
+ }
971
+ }
972
+ // Default: recurse into children
973
+ walkChildren(node, groupStack);
974
+ }
975
+ function walkChildren(node, groupStack) {
976
+ for (const child of node.children ?? []) {
977
+ walk(child, groupStack);
978
+ }
979
+ }
980
+ walk(tree.rootNode, []);
981
+ return routes;
982
+ }
566
983
  const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
567
984
  let query;
568
985
  try {
@@ -599,7 +1016,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
599
1016
  }
600
1017
  // Extract import paths before skipping
601
1018
  if (captureMap['import'] && captureMap['import.source']) {
602
- const rawImportPath = captureMap['import.source'].text.replace(/['"<>]/g, '');
1019
+ const rawImportPath = language === SupportedLanguages.Kotlin
1020
+ ? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
1021
+ : captureMap['import.source'].text.replace(/['"<>]/g, '');
603
1022
  result.imports.push({
604
1023
  filePath: file.path,
605
1024
  rawImportPath,
@@ -655,8 +1074,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
655
1074
  if (!nodeLabel)
656
1075
  continue;
657
1076
  const nameNode = captureMap['name'];
658
- const nodeName = nameNode.text;
659
- const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
1077
+ // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
1078
+ if (!nameNode && nodeLabel !== 'Constructor')
1079
+ continue;
1080
+ const nodeName = nameNode ? nameNode.text : 'init';
1081
+ const definitionNode = getDefinitionNodeFromCaptures(captureMap);
1082
+ const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
1083
+ const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`);
660
1084
  let description;
661
1085
  if (language === SupportedLanguages.PHP) {
662
1086
  if (nodeLabel === 'Property' && captureMap['definition.property']) {
@@ -666,9 +1090,8 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
666
1090
  description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
667
1091
  }
668
1092
  }
669
- const definitionNode = getDefinitionNodeFromCaptures(captureMap);
670
1093
  const frameworkHint = definitionNode
671
- ? detectFrameworkFromAST(language, definitionNode.text || '')
1094
+ ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
672
1095
  : null;
673
1096
  result.nodes.push({
674
1097
  id: nodeId,
@@ -676,10 +1099,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
676
1099
  properties: {
677
1100
  name: nodeName,
678
1101
  filePath: file.path,
679
- startLine: nameNode.startPosition.row,
680
- endLine: nameNode.endPosition.row,
1102
+ startLine: definitionNode ? definitionNode.startPosition.row : startLine,
1103
+ endLine: definitionNode ? definitionNode.endPosition.row : startLine,
681
1104
  language: language,
682
- isExported: isNodeExported(nameNode, nodeName, language),
1105
+ isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
683
1106
  ...(frameworkHint ? {
684
1107
  astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
685
1108
  astFrameworkReason: frameworkHint.reason,
@@ -704,6 +1127,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
704
1127
  reason: '',
705
1128
  });
706
1129
  }
1130
+ // Extract Laravel routes from route files via procedural AST walk
1131
+ if (language === SupportedLanguages.PHP && (file.path.includes('/routes/') || file.path.startsWith('routes/')) && file.path.endsWith('.php')) {
1132
+ const extractedRoutes = extractLaravelRoutes(tree, file.path);
1133
+ result.routes.push(...extractedRoutes);
1134
+ }
707
1135
  }
708
1136
  };
709
1137
  // ============================================================================
@@ -712,7 +1140,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
712
1140
  /** Accumulated result across sub-batches */
713
1141
  let accumulated = {
714
1142
  nodes: [], relationships: [], symbols: [],
715
- imports: [], calls: [], heritage: [], fileCount: 0,
1143
+ imports: [], calls: [], heritage: [], routes: [], fileCount: 0,
716
1144
  };
717
1145
  let cumulativeProcessed = 0;
718
1146
  const mergeResult = (target, src) => {
@@ -722,6 +1150,7 @@ const mergeResult = (target, src) => {
722
1150
  target.imports.push(...src.imports);
723
1151
  target.calls.push(...src.calls);
724
1152
  target.heritage.push(...src.heritage);
1153
+ target.routes.push(...src.routes);
725
1154
  target.fileCount += src.fileCount;
726
1155
  };
727
1156
  parentPort.on('message', (msg) => {
@@ -741,7 +1170,7 @@ parentPort.on('message', (msg) => {
741
1170
  if (msg && msg.type === 'flush') {
742
1171
  parentPort.postMessage({ type: 'result', data: accumulated });
743
1172
  // Reset for potential reuse
744
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], fileCount: 0 };
1173
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], fileCount: 0 };
745
1174
  cumulativeProcessed = 0;
746
1175
  return;
747
1176
  }
@@ -13,6 +13,10 @@
13
13
  */
14
14
  import { KnowledgeGraph } from '../graph/types.js';
15
15
  import { NodeTableName } from './schema.js';
16
+ export declare const sanitizeUTF8: (str: string) => string;
17
+ export declare const escapeCSVField: (value: string | number | undefined | null) => string;
18
+ export declare const escapeCSVNumber: (value: number | undefined | null, defaultValue?: number) => string;
19
+ export declare const isBinaryContent: (content: string) => boolean;
16
20
  export interface StreamedCSVResult {
17
21
  nodeFiles: Map<NodeTableName, {
18
22
  csvPath: string;