@kodus/kodus-graph 0.2.7 → 0.2.9

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/dist/analysis/blast-radius.d.ts +2 -0
  4. package/dist/analysis/blast-radius.js +57 -0
  5. package/dist/analysis/communities.d.ts +28 -0
  6. package/dist/analysis/communities.js +100 -0
  7. package/dist/analysis/context-builder.d.ts +34 -0
  8. package/dist/analysis/context-builder.js +83 -0
  9. package/dist/analysis/diff.d.ts +35 -0
  10. package/dist/analysis/diff.js +140 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +98 -0
  13. package/dist/analysis/flows.d.ts +27 -0
  14. package/dist/analysis/flows.js +86 -0
  15. package/dist/analysis/inheritance.d.ts +3 -0
  16. package/dist/analysis/inheritance.js +31 -0
  17. package/dist/analysis/prompt-formatter.d.ts +2 -0
  18. package/dist/analysis/prompt-formatter.js +166 -0
  19. package/dist/analysis/risk-score.d.ts +4 -0
  20. package/dist/analysis/risk-score.js +51 -0
  21. package/dist/analysis/search.d.ts +11 -0
  22. package/dist/analysis/search.js +64 -0
  23. package/dist/analysis/test-gaps.d.ts +2 -0
  24. package/dist/analysis/test-gaps.js +14 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +208 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +114 -0
  29. package/dist/commands/communities.d.ts +8 -0
  30. package/dist/commands/communities.js +9 -0
  31. package/dist/commands/context.d.ts +12 -0
  32. package/dist/commands/context.js +130 -0
  33. package/dist/commands/diff.d.ts +9 -0
  34. package/dist/commands/diff.js +89 -0
  35. package/dist/commands/flows.d.ts +8 -0
  36. package/dist/commands/flows.js +9 -0
  37. package/dist/commands/parse.d.ts +10 -0
  38. package/dist/commands/parse.js +101 -0
  39. package/dist/commands/search.d.ts +12 -0
  40. package/dist/commands/search.js +27 -0
  41. package/dist/commands/update.d.ts +7 -0
  42. package/dist/commands/update.js +154 -0
  43. package/dist/graph/builder.d.ts +2 -0
  44. package/dist/graph/builder.js +216 -0
  45. package/dist/graph/edges.d.ts +19 -0
  46. package/dist/graph/edges.js +105 -0
  47. package/dist/graph/json-writer.d.ts +9 -0
  48. package/dist/graph/json-writer.js +38 -0
  49. package/dist/graph/loader.d.ts +13 -0
  50. package/dist/graph/loader.js +101 -0
  51. package/dist/graph/merger.d.ts +7 -0
  52. package/dist/graph/merger.js +18 -0
  53. package/dist/graph/types.d.ts +249 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +4 -0
  56. package/dist/parser/batch.js +78 -0
  57. package/dist/parser/discovery.d.ts +7 -0
  58. package/dist/parser/discovery.js +61 -0
  59. package/dist/parser/extractor.d.ts +4 -0
  60. package/dist/parser/extractor.js +33 -0
  61. package/dist/parser/extractors/generic.d.ts +8 -0
  62. package/dist/parser/extractors/generic.js +471 -0
  63. package/dist/parser/extractors/python.d.ts +8 -0
  64. package/dist/parser/extractors/python.js +133 -0
  65. package/dist/parser/extractors/ruby.d.ts +8 -0
  66. package/dist/parser/extractors/ruby.js +153 -0
  67. package/dist/parser/extractors/typescript.d.ts +10 -0
  68. package/dist/parser/extractors/typescript.js +365 -0
  69. package/dist/parser/languages.d.ts +32 -0
  70. package/dist/parser/languages.js +303 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/import-map.d.ts +12 -0
  74. package/dist/resolver/import-map.js +21 -0
  75. package/dist/resolver/import-resolver.d.ts +19 -0
  76. package/dist/resolver/import-resolver.js +212 -0
  77. package/dist/resolver/languages/csharp.d.ts +1 -0
  78. package/dist/resolver/languages/csharp.js +31 -0
  79. package/dist/resolver/languages/go.d.ts +3 -0
  80. package/dist/resolver/languages/go.js +196 -0
  81. package/dist/resolver/languages/java.d.ts +1 -0
  82. package/dist/resolver/languages/java.js +108 -0
  83. package/dist/resolver/languages/php.d.ts +3 -0
  84. package/dist/resolver/languages/php.js +54 -0
  85. package/dist/resolver/languages/python.d.ts +11 -0
  86. package/dist/resolver/languages/python.js +51 -0
  87. package/dist/resolver/languages/ruby.d.ts +9 -0
  88. package/dist/resolver/languages/ruby.js +59 -0
  89. package/dist/resolver/languages/rust.d.ts +1 -0
  90. package/dist/resolver/languages/rust.js +196 -0
  91. package/dist/resolver/languages/typescript.d.ts +27 -0
  92. package/dist/resolver/languages/typescript.js +240 -0
  93. package/dist/resolver/re-export-resolver.d.ts +24 -0
  94. package/dist/resolver/re-export-resolver.js +57 -0
  95. package/dist/resolver/symbol-table.d.ts +17 -0
  96. package/dist/resolver/symbol-table.js +60 -0
  97. package/dist/shared/extract-calls.d.ts +26 -0
  98. package/dist/shared/extract-calls.js +57 -0
  99. package/dist/shared/file-hash.d.ts +3 -0
  100. package/dist/shared/file-hash.js +10 -0
  101. package/dist/shared/filters.d.ts +3 -0
  102. package/dist/shared/filters.js +240 -0
  103. package/dist/shared/logger.d.ts +6 -0
  104. package/dist/shared/logger.js +17 -0
  105. package/dist/shared/qualified-name.d.ts +1 -0
  106. package/dist/shared/qualified-name.js +9 -0
  107. package/dist/shared/safe-path.d.ts +6 -0
  108. package/dist/shared/safe-path.js +29 -0
  109. package/dist/shared/schemas.d.ts +43 -0
  110. package/dist/shared/schemas.js +30 -0
  111. package/dist/shared/temp.d.ts +11 -0
  112. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  113. package/package.json +20 -6
  114. package/src/analysis/blast-radius.ts +0 -54
  115. package/src/analysis/communities.ts +0 -135
  116. package/src/analysis/context-builder.ts +0 -130
  117. package/src/analysis/diff.ts +0 -131
  118. package/src/analysis/enrich.ts +0 -110
  119. package/src/analysis/flows.ts +0 -112
  120. package/src/analysis/inheritance.ts +0 -34
  121. package/src/analysis/prompt-formatter.ts +0 -175
  122. package/src/analysis/risk-score.ts +0 -62
  123. package/src/analysis/search.ts +0 -76
  124. package/src/analysis/test-gaps.ts +0 -21
  125. package/src/cli.ts +0 -207
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -139
  129. package/src/commands/diff.ts +0 -96
  130. package/src/commands/flows.ts +0 -19
  131. package/src/commands/parse.ts +0 -124
  132. package/src/commands/search.ts +0 -41
  133. package/src/commands/update.ts +0 -166
  134. package/src/graph/builder.ts +0 -209
  135. package/src/graph/edges.ts +0 -101
  136. package/src/graph/json-writer.ts +0 -43
  137. package/src/graph/loader.ts +0 -113
  138. package/src/graph/merger.ts +0 -25
  139. package/src/graph/types.ts +0 -283
  140. package/src/parser/batch.ts +0 -82
  141. package/src/parser/discovery.ts +0 -75
  142. package/src/parser/extractor.ts +0 -37
  143. package/src/parser/extractors/generic.ts +0 -132
  144. package/src/parser/extractors/python.ts +0 -133
  145. package/src/parser/extractors/ruby.ts +0 -147
  146. package/src/parser/extractors/typescript.ts +0 -350
  147. package/src/parser/languages.ts +0 -122
  148. package/src/resolver/call-resolver.ts +0 -244
  149. package/src/resolver/import-map.ts +0 -27
  150. package/src/resolver/import-resolver.ts +0 -72
  151. package/src/resolver/languages/csharp.ts +0 -7
  152. package/src/resolver/languages/go.ts +0 -7
  153. package/src/resolver/languages/java.ts +0 -7
  154. package/src/resolver/languages/php.ts +0 -7
  155. package/src/resolver/languages/python.ts +0 -35
  156. package/src/resolver/languages/ruby.ts +0 -21
  157. package/src/resolver/languages/rust.ts +0 -7
  158. package/src/resolver/languages/typescript.ts +0 -168
  159. package/src/resolver/re-export-resolver.ts +0 -66
  160. package/src/resolver/symbol-table.ts +0 -67
  161. package/src/shared/extract-calls.ts +0 -75
  162. package/src/shared/file-hash.ts +0 -12
  163. package/src/shared/filters.ts +0 -243
  164. package/src/shared/logger.ts +0 -14
  165. package/src/shared/qualified-name.ts +0 -5
  166. package/src/shared/safe-path.ts +0 -31
  167. package/src/shared/schemas.ts +0 -32
@@ -0,0 +1,133 @@
1
+ import { extractCalls } from '../../shared/extract-calls';
2
+ import { computeContentHash } from '../../shared/file-hash';
3
+ import { LANG_KINDS } from '../languages';
4
+ export function extractPython(root, fp, seen, graph) {
5
+ const kinds = LANG_KINDS.python;
6
+ const rootNode = root.root();
7
+ // ── Classes ──
8
+ for (const node of rootNode.findAll({ rule: { kind: kinds.class } })) {
9
+ const name = node.field('name')?.text();
10
+ if (!name || seen.has(`c:${fp}:${name}`)) {
11
+ continue;
12
+ }
13
+ seen.add(`c:${fp}:${name}`);
14
+ const argList = node.field('superclasses') || node.children().find((c) => c.kind() === 'argument_list');
15
+ const extendsName = argList
16
+ ?.children()
17
+ .find((c) => c.kind() === 'identifier')
18
+ ?.text() || '';
19
+ graph.classes.push({
20
+ name,
21
+ file: fp,
22
+ line_start: node.range().start.line,
23
+ line_end: node.range().end.line,
24
+ extends: extendsName,
25
+ implements: [],
26
+ ast_kind: String(node.kind()),
27
+ qualified: `${fp}::${name}`,
28
+ content_hash: computeContentHash(node.text()),
29
+ });
30
+ }
31
+ // ── Functions / Methods ──
32
+ for (const node of rootNode.findAll({ rule: { kind: kinds.function } })) {
33
+ const name = node.field('name')?.text();
34
+ if (!name) {
35
+ continue;
36
+ }
37
+ const line = node.range().start.line;
38
+ if (seen.has(`m:${fp}:${name}:${line}`)) {
39
+ continue;
40
+ }
41
+ seen.add(`m:${fp}:${name}:${line}`);
42
+ const classAncestor = node.ancestors().find((a) => a.kind() === kinds.class);
43
+ const className = classAncestor?.field('name')?.text() || '';
44
+ const retType = node
45
+ .field('return_type')
46
+ ?.text()
47
+ ?.replace(/^->\s*/, '') || '';
48
+ const isTest = name.startsWith('test_');
49
+ if (isTest) {
50
+ graph.tests.push({
51
+ name,
52
+ file: fp,
53
+ line_start: line,
54
+ line_end: node.range().end.line,
55
+ ast_kind: String(node.kind()),
56
+ qualified: `${fp}::test:${name}`,
57
+ content_hash: computeContentHash(node.text()),
58
+ });
59
+ }
60
+ graph.functions.push({
61
+ name,
62
+ file: fp,
63
+ line_start: line,
64
+ line_end: node.range().end.line,
65
+ params: node.field('parameters')?.text() || '()',
66
+ returnType: retType,
67
+ kind: name === '__init__' ? 'Constructor' : className ? 'Method' : 'Function',
68
+ ast_kind: String(node.kind()),
69
+ className,
70
+ qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
71
+ content_hash: computeContentHash(node.text()),
72
+ });
73
+ }
74
+ // ── Imports (from X import Y) ──
75
+ for (const node of rootNode.findAll({ rule: { kind: kinds.import } })) {
76
+ const modNode = node
77
+ .children()
78
+ .find((c) => c.kind() === 'dotted_name' || c.kind() === 'relative_import');
79
+ const modulePath = modNode?.text() || '';
80
+ if (!modulePath) {
81
+ continue;
82
+ }
83
+ const names = [];
84
+ for (const child of node.children()) {
85
+ if (child.kind() === 'dotted_name' && child !== modNode) {
86
+ names.push(child.text());
87
+ }
88
+ if (child.kind() === 'identifier' && child !== modNode) {
89
+ names.push(child.text());
90
+ }
91
+ }
92
+ graph.imports.push({
93
+ module: modulePath,
94
+ file: fp,
95
+ line: node.range().start.line,
96
+ names,
97
+ lang: 'python',
98
+ });
99
+ }
100
+ // ── Regular imports (import X) ──
101
+ for (const node of rootNode.findAll({ rule: { kind: kinds.importRegular } })) {
102
+ const modNode = node.children().find((c) => c.kind() === 'dotted_name');
103
+ if (modNode) {
104
+ graph.imports.push({
105
+ module: modNode.text(),
106
+ file: fp,
107
+ line: node.range().start.line,
108
+ names: [modNode.text()],
109
+ lang: 'python',
110
+ });
111
+ }
112
+ }
113
+ }
114
+ /** Python-specific call extraction config for shared extractCalls(). */
115
+ const PYTHON_CALL_CONFIG = {
116
+ selfPrefixes: ['self.'],
117
+ superPrefixes: ['super().'],
118
+ findEnclosingClass: (node) => node.ancestors().find((a) => a.kind() === 'class_definition') ?? null,
119
+ getParentClass: (classNode) => {
120
+ const argList = classNode.field('superclasses') || classNode.children().find((c) => c.kind() === 'argument_list');
121
+ return argList
122
+ ?.children()
123
+ .find((c) => c.kind() === 'identifier')
124
+ ?.text();
125
+ },
126
+ };
127
+ /**
128
+ * Extract raw call sites from a Python AST.
129
+ * Detects self.X() and super().X() to preserve class resolution context.
130
+ */
131
+ export function extractCallsFromPython(root, fp, calls) {
132
+ extractCalls(root.root(), fp, PYTHON_CALL_CONFIG, calls);
133
+ }
@@ -0,0 +1,8 @@
1
+ import type { SgRoot } from '@ast-grep/napi';
2
+ import type { RawCallSite, RawGraph } from '../../graph/types';
3
+ export declare function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph): void;
4
+ /**
5
+ * Extract raw call sites from a Ruby AST.
6
+ * Detects self.X() and super() to preserve class resolution context.
7
+ */
8
+ export declare function extractCallsFromRuby(root: SgRoot, fp: string, calls: RawCallSite[]): void;
@@ -0,0 +1,153 @@
1
+ import { extractCalls } from '../../shared/extract-calls';
2
+ import { computeContentHash } from '../../shared/file-hash';
3
+ import { log } from '../../shared/logger';
4
+ import { LANG_KINDS } from '../languages';
5
+ export function extractRuby(root, fp, seen, graph) {
6
+ const kinds = LANG_KINDS.ruby;
7
+ const rootNode = root.root();
8
+ // ── Classes ──
9
+ for (const node of rootNode.findAll({ rule: { kind: kinds.class } })) {
10
+ const name = node.field('name')?.text();
11
+ if (!name || seen.has(`c:${fp}:${name}`)) {
12
+ continue;
13
+ }
14
+ seen.add(`c:${fp}:${name}`);
15
+ const superclass = node.field('superclass')?.text() || '';
16
+ graph.classes.push({
17
+ name,
18
+ file: fp,
19
+ line_start: node.range().start.line,
20
+ line_end: node.range().end.line,
21
+ extends: superclass,
22
+ implements: [],
23
+ ast_kind: String(node.kind()),
24
+ qualified: `${fp}::${name}`,
25
+ content_hash: computeContentHash(node.text()),
26
+ });
27
+ }
28
+ // ── Modules ──
29
+ for (const node of rootNode.findAll({ rule: { kind: kinds.module } })) {
30
+ const name = node.field('name')?.text();
31
+ if (!name || seen.has(`c:${fp}:${name}`)) {
32
+ continue;
33
+ }
34
+ seen.add(`c:${fp}:${name}`);
35
+ graph.classes.push({
36
+ name,
37
+ file: fp,
38
+ line_start: node.range().start.line,
39
+ line_end: node.range().end.line,
40
+ extends: '',
41
+ implements: [],
42
+ ast_kind: String(node.kind()),
43
+ qualified: `${fp}::${name}`,
44
+ content_hash: computeContentHash(node.text()),
45
+ });
46
+ }
47
+ // ── Methods ──
48
+ for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
49
+ const name = node.field('name')?.text();
50
+ if (!name) {
51
+ continue;
52
+ }
53
+ const line = node.range().start.line;
54
+ if (seen.has(`m:${fp}:${name}:${line}`)) {
55
+ continue;
56
+ }
57
+ seen.add(`m:${fp}:${name}:${line}`);
58
+ const classAncestor = node
59
+ .ancestors()
60
+ .find((a) => a.kind() === kinds.class || a.kind() === kinds.module);
61
+ const className = classAncestor?.field('name')?.text() || '';
62
+ graph.functions.push({
63
+ name,
64
+ file: fp,
65
+ line_start: line,
66
+ line_end: node.range().end.line,
67
+ params: node.field('parameters')?.text() || '()',
68
+ returnType: '',
69
+ kind: className ? 'Method' : 'Function',
70
+ ast_kind: String(node.kind()),
71
+ className,
72
+ qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
73
+ content_hash: computeContentHash(node.text()),
74
+ });
75
+ }
76
+ // ── Tests (RSpec: describe/it/context) ──
77
+ for (const p of [
78
+ "describe '$NAME' do $$$BODY end",
79
+ 'describe "$NAME" do $$$BODY end',
80
+ "it '$NAME' do $$$BODY end",
81
+ 'it "$NAME" do $$$BODY end',
82
+ "context '$NAME' do $$$BODY end",
83
+ 'context "$NAME" do $$$BODY end',
84
+ ]) {
85
+ try {
86
+ for (const m of rootNode.findAll(p)) {
87
+ const name = m.getMatch('NAME')?.text();
88
+ if (!name) {
89
+ continue;
90
+ }
91
+ const key = `t:${fp}:${name}:${m.range().start.line}`;
92
+ if (seen.has(key)) {
93
+ continue;
94
+ }
95
+ seen.add(key);
96
+ graph.tests.push({
97
+ name,
98
+ file: fp,
99
+ line_start: m.range().start.line,
100
+ line_end: m.range().end.line,
101
+ ast_kind: String(m.kind()),
102
+ qualified: `${fp}::test:${name}`,
103
+ content_hash: computeContentHash(m.text()),
104
+ });
105
+ }
106
+ }
107
+ catch (err) {
108
+ log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
109
+ }
110
+ }
111
+ // ── Imports (require/require_relative) ──
112
+ for (const p of [
113
+ "require '$MODULE'",
114
+ 'require "$MODULE"',
115
+ "require_relative '$MODULE'",
116
+ 'require_relative "$MODULE"',
117
+ ]) {
118
+ try {
119
+ for (const m of rootNode.findAll(p)) {
120
+ const mod = m.getMatch('MODULE')?.text();
121
+ if (mod) {
122
+ graph.imports.push({
123
+ module: mod,
124
+ file: fp,
125
+ line: m.range().start.line,
126
+ names: [],
127
+ lang: 'ruby',
128
+ });
129
+ }
130
+ }
131
+ }
132
+ catch (err) {
133
+ log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
134
+ }
135
+ }
136
+ }
137
+ /** Ruby-specific call extraction config for shared extractCalls(). */
138
+ function createRubyCallConfig() {
139
+ const kinds = LANG_KINDS.ruby;
140
+ return {
141
+ selfPrefixes: ['self.'],
142
+ superPrefixes: ['super'],
143
+ findEnclosingClass: (node) => node.ancestors().find((a) => a.kind() === kinds.class || a.kind() === kinds.module) ?? null,
144
+ getParentClass: (classNode) => classNode.field('superclass')?.text(),
145
+ };
146
+ }
147
+ /**
148
+ * Extract raw call sites from a Ruby AST.
149
+ * Detects self.X() and super() to preserve class resolution context.
150
+ */
151
+ export function extractCallsFromRuby(root, fp, calls) {
152
+ extractCalls(root.root(), fp, createRubyCallConfig(), calls);
153
+ }
@@ -0,0 +1,10 @@
1
+ import type { SgRoot } from '@ast-grep/napi';
2
+ import { Lang } from '@ast-grep/napi';
3
+ import type { RawCallSite, RawGraph } from '../../graph/types';
4
+ export declare function extractTypeScript(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph, lang?: Lang | string): void;
5
+ /**
6
+ * Extract raw call sites from a TypeScript/JavaScript AST.
7
+ * Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
8
+ * Filters NOISE. Does NOT resolve — just collects raw sites.
9
+ */
10
+ export declare function extractCallsFromTypeScript(root: SgRoot, fp: string, calls: RawCallSite[]): void;
@@ -0,0 +1,365 @@
1
+ import { Lang } from '@ast-grep/napi';
2
+ import { extractCalls } from '../../shared/extract-calls';
3
+ import { computeContentHash } from '../../shared/file-hash';
4
+ import { NOISE } from '../../shared/filters';
5
+ import { LANG_KINDS } from '../languages';
6
+ export function extractTypeScript(root, fp, seen, graph, lang = Lang.TypeScript) {
7
+ const kinds = LANG_KINDS.typescript;
8
+ const rootNode = root.root();
9
+ const isTS = lang === Lang.TypeScript || lang === Lang.Tsx;
10
+ // ── Classes ──
11
+ const classKinds = isTS ? [kinds.class, kinds.abstractClass] : [kinds.class];
12
+ for (const kind of classKinds) {
13
+ for (const node of rootNode.findAll({ rule: { kind } })) {
14
+ const name = node.field('name')?.text();
15
+ if (!name || seen.has(`c:${fp}:${name}`)) {
16
+ continue;
17
+ }
18
+ seen.add(`c:${fp}:${name}`);
19
+ let extendsName = '';
20
+ let implementsNames = [];
21
+ const heritage = node.children().find((c) => c.kind() === 'class_heritage');
22
+ if (heritage) {
23
+ const ext = heritage.children().find((c) => c.kind() === 'extends_clause');
24
+ extendsName =
25
+ ext
26
+ ?.children()
27
+ .find((c) => c.kind() === 'identifier' ||
28
+ c.kind() === 'type_identifier' ||
29
+ c.kind() === 'member_expression')
30
+ ?.text() || '';
31
+ const impl = heritage.children().find((c) => c.kind() === 'implements_clause');
32
+ implementsNames =
33
+ impl
34
+ ?.children()
35
+ .filter((c) => c.kind() === 'type_identifier' || c.kind() === 'identifier')
36
+ .map((c) => c.text()) ?? [];
37
+ }
38
+ graph.classes.push({
39
+ name,
40
+ file: fp,
41
+ line_start: node.range().start.line,
42
+ line_end: node.range().end.line,
43
+ extends: extendsName,
44
+ implements: implementsNames,
45
+ ast_kind: String(node.kind()),
46
+ qualified: `${fp}::${name}`,
47
+ content_hash: computeContentHash(node.text()),
48
+ });
49
+ }
50
+ }
51
+ // ── Methods (kind-based: catches constructor, async, getters/setters) ──
52
+ for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
53
+ const name = node.field('name')?.text();
54
+ if (!name) {
55
+ continue;
56
+ }
57
+ const line = node.range().start.line;
58
+ if (seen.has(`m:${fp}:${name}:${line}`)) {
59
+ continue;
60
+ }
61
+ seen.add(`m:${fp}:${name}:${line}`);
62
+ const classAncestor = node
63
+ .ancestors()
64
+ .find((a) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass));
65
+ const className = classAncestor?.field('name')?.text() || '';
66
+ const params = node.field('parameters');
67
+ const retType = node.field('return_type')?.text()?.replace(/^:\s*/, '') || '';
68
+ if (name === 'constructor' && className) {
69
+ // Constructor DI extraction
70
+ const fieldTypeMap = new Map();
71
+ if (params) {
72
+ for (const p of params.children()) {
73
+ if (p.kind() !== 'required_parameter') {
74
+ continue;
75
+ }
76
+ if (!p.children().some((c) => c.kind() === 'accessibility_modifier')) {
77
+ continue;
78
+ }
79
+ const ident = p.children().find((c) => c.kind() === 'identifier');
80
+ const typeAnn = p.children().find((c) => c.kind() === 'type_annotation');
81
+ if (ident && typeAnn) {
82
+ const typeNode = typeAnn
83
+ .children()
84
+ .find((c) => c.kind() === 'type_identifier' ||
85
+ c.kind() === 'identifier' ||
86
+ c.kind() === 'generic_type');
87
+ if (typeNode) {
88
+ const typeName = typeNode.kind() === 'generic_type'
89
+ ? typeNode
90
+ .children()
91
+ .find((c) => c.kind() === 'type_identifier')
92
+ ?.text() || typeNode.text()
93
+ : typeNode.text();
94
+ fieldTypeMap.set(ident.text(), typeName);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ if (fieldTypeMap.size > 0) {
100
+ graph.diMaps.set(fp, fieldTypeMap);
101
+ }
102
+ graph.functions.push({
103
+ name: `${className}.constructor`,
104
+ file: fp,
105
+ line_start: line,
106
+ line_end: node.range().end.line,
107
+ params: params?.text() || '()',
108
+ returnType: '',
109
+ kind: 'Constructor',
110
+ ast_kind: String(node.kind()),
111
+ className,
112
+ qualified: `${fp}::${className}.constructor`,
113
+ content_hash: computeContentHash(node.text()),
114
+ });
115
+ }
116
+ else {
117
+ graph.functions.push({
118
+ name,
119
+ file: fp,
120
+ line_start: line,
121
+ line_end: node.range().end.line,
122
+ params: params?.text() || '()',
123
+ returnType: retType,
124
+ kind: className ? 'Method' : 'Function',
125
+ ast_kind: String(node.kind()),
126
+ className,
127
+ qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
128
+ content_hash: computeContentHash(node.text()),
129
+ });
130
+ }
131
+ }
132
+ // ── Standalone functions ──
133
+ for (const node of rootNode.findAll({ rule: { kind: kinds.function } })) {
134
+ const name = node.field('name')?.text();
135
+ if (!name) {
136
+ continue;
137
+ }
138
+ const line = node.range().start.line;
139
+ if (seen.has(`f:${fp}:${name}:${line}`)) {
140
+ continue;
141
+ }
142
+ if (node.ancestors().some((a) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass))) {
143
+ continue;
144
+ }
145
+ seen.add(`f:${fp}:${name}:${line}`);
146
+ graph.functions.push({
147
+ name,
148
+ file: fp,
149
+ line_start: line,
150
+ line_end: node.range().end.line,
151
+ params: node.field('parameters')?.text() || '()',
152
+ returnType: node.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
153
+ kind: 'Function',
154
+ ast_kind: String(node.kind()),
155
+ className: '',
156
+ qualified: `${fp}::${name}`,
157
+ content_hash: computeContentHash(node.text()),
158
+ });
159
+ }
160
+ // ── Arrow functions ──
161
+ for (const node of rootNode.findAll({
162
+ rule: { kind: kinds.arrowContainer, has: { kind: kinds.arrowFunction } },
163
+ })) {
164
+ const name = node.field('name')?.text();
165
+ if (!name) {
166
+ continue;
167
+ }
168
+ const line = node.range().start.line;
169
+ if (seen.has(`f:${fp}:${name}:${line}`)) {
170
+ continue;
171
+ }
172
+ seen.add(`f:${fp}:${name}:${line}`);
173
+ const arrow = node.children().find((c) => c.kind() === kinds.arrowFunction);
174
+ graph.functions.push({
175
+ name,
176
+ file: fp,
177
+ line_start: line,
178
+ line_end: node.range().end.line,
179
+ params: arrow?.field('parameters')?.text() || '()',
180
+ returnType: arrow?.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
181
+ kind: 'Function',
182
+ ast_kind: 'arrow_function',
183
+ className: '',
184
+ qualified: `${fp}::${name}`,
185
+ content_hash: computeContentHash(node.text()),
186
+ });
187
+ }
188
+ // ── Interfaces (TS only — JS grammar has no interface_declaration) ──
189
+ if (isTS) {
190
+ for (const node of rootNode.findAll({ rule: { kind: kinds.interface } })) {
191
+ const name = node.field('name')?.text();
192
+ if (!name || seen.has(`i:${fp}:${name}`)) {
193
+ continue;
194
+ }
195
+ seen.add(`i:${fp}:${name}`);
196
+ const methods = [];
197
+ const body = node.field('body');
198
+ if (body) {
199
+ for (const child of body.findAll({ rule: { kind: kinds.methodSignature } })) {
200
+ const mn = child.field('name')?.text();
201
+ if (mn) {
202
+ methods.push(mn);
203
+ }
204
+ }
205
+ }
206
+ graph.interfaces.push({
207
+ name,
208
+ file: fp,
209
+ line_start: node.range().start.line,
210
+ line_end: node.range().end.line,
211
+ methods,
212
+ ast_kind: String(node.kind()),
213
+ qualified: `${fp}::${name}`,
214
+ content_hash: computeContentHash(node.text()),
215
+ });
216
+ }
217
+ }
218
+ // ── Enums (TS only — JS grammar has no enum_declaration) ──
219
+ if (isTS) {
220
+ for (const node of rootNode.findAll({ rule: { kind: kinds.enum } })) {
221
+ const name = node.field('name')?.text();
222
+ if (!name || seen.has(`e:${fp}:${name}`)) {
223
+ continue;
224
+ }
225
+ seen.add(`e:${fp}:${name}`);
226
+ graph.enums.push({
227
+ name,
228
+ file: fp,
229
+ line_start: node.range().start.line,
230
+ line_end: node.range().end.line,
231
+ ast_kind: String(node.kind()),
232
+ qualified: `${fp}::${name}`,
233
+ content_hash: computeContentHash(node.text()),
234
+ });
235
+ }
236
+ }
237
+ // ── Imports ──
238
+ for (const node of rootNode.findAll({ rule: { kind: kinds.import } })) {
239
+ const sourceNode = node.children().find((c) => c.kind() === 'string');
240
+ const frag = sourceNode?.children().find((c) => c.kind() === 'string_fragment');
241
+ const modulePath = frag?.text() || sourceNode?.text()?.replace(/['"]/g, '') || '';
242
+ if (!modulePath) {
243
+ continue;
244
+ }
245
+ const names = [];
246
+ const importClause = node.children().find((c) => c.kind() === 'import_clause');
247
+ if (importClause) {
248
+ for (const child of importClause.children()) {
249
+ if (child.kind() === 'identifier') {
250
+ names.push(child.text());
251
+ }
252
+ else if (child.kind() === 'named_imports') {
253
+ for (const spec of child.findAll({ rule: { kind: 'import_specifier' } })) {
254
+ const n = spec.field('name')?.text() ||
255
+ spec
256
+ .children()
257
+ .find((c) => c.kind() === 'identifier')
258
+ ?.text();
259
+ if (n) {
260
+ names.push(n);
261
+ }
262
+ }
263
+ }
264
+ else if (child.kind() === 'namespace_import') {
265
+ const alias = child.children().find((c) => c.kind() === 'identifier');
266
+ if (alias) {
267
+ names.push(alias.text());
268
+ }
269
+ }
270
+ }
271
+ }
272
+ graph.imports.push({
273
+ module: modulePath,
274
+ file: fp,
275
+ line: node.range().start.line,
276
+ names,
277
+ lang: 'ts',
278
+ });
279
+ }
280
+ // ── Re-exports ──
281
+ for (const node of rootNode.findAll({ rule: { kind: kinds.export } })) {
282
+ const src = node.children().find((c) => c.kind() === 'string');
283
+ if (src) {
284
+ const frag = src.children().find((c) => c.kind() === 'string_fragment');
285
+ graph.reExports.push({
286
+ module: frag?.text() || src.text().replace(/['"]/g, ''),
287
+ file: fp,
288
+ line: node.range().start.line,
289
+ });
290
+ }
291
+ }
292
+ // ── Tests (pattern-based) ──
293
+ for (const p of [
294
+ 'describe("$NAME", $$$BODY)',
295
+ "describe('$NAME', $$$BODY)",
296
+ 'it("$NAME", $$$BODY)',
297
+ "it('$NAME', $$$BODY)",
298
+ 'test("$NAME", $$$BODY)',
299
+ "test('$NAME', $$$BODY)",
300
+ ]) {
301
+ for (const m of rootNode.findAll(p)) {
302
+ const name = m.getMatch('NAME')?.text();
303
+ if (!name) {
304
+ continue;
305
+ }
306
+ const key = `t:${fp}:${name}:${m.range().start.line}`;
307
+ if (seen.has(key)) {
308
+ continue;
309
+ }
310
+ seen.add(key);
311
+ graph.tests.push({
312
+ name,
313
+ file: fp,
314
+ line_start: m.range().start.line,
315
+ line_end: m.range().end.line,
316
+ ast_kind: String(m.kind()),
317
+ qualified: `${fp}::test:${name}`,
318
+ content_hash: computeContentHash(m.text()),
319
+ });
320
+ }
321
+ }
322
+ }
323
+ /** TypeScript-specific call extraction config for shared extractCalls(). */
324
+ const TS_CALL_CONFIG = {
325
+ selfPrefixes: ['this.'],
326
+ superPrefixes: ['super.'],
327
+ findEnclosingClass: (node) => {
328
+ const kinds = LANG_KINDS.typescript;
329
+ return (node.ancestors().find((a) => a.kind() === kinds.class || a.kind() === kinds.abstractClass) ?? null);
330
+ },
331
+ getParentClass: (classNode) => {
332
+ const heritage = classNode.children().find((c) => c.kind() === 'class_heritage');
333
+ const ext = heritage?.children().find((c) => c.kind() === 'extends_clause');
334
+ return ext
335
+ ?.children()
336
+ .find((c) => c.kind() === 'identifier' || c.kind() === 'type_identifier' || c.kind() === 'member_expression')
337
+ ?.text();
338
+ },
339
+ // Skip this.field.method — already handled by the DI pattern above
340
+ skipCallee: (callee) => callee.startsWith('this.') && callee.substring(5).includes('.'),
341
+ };
342
+ /**
343
+ * Extract raw call sites from a TypeScript/JavaScript AST.
344
+ * Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
345
+ * Filters NOISE. Does NOT resolve — just collects raw sites.
346
+ */
347
+ export function extractCallsFromTypeScript(root, fp, calls) {
348
+ const rootNode = root.root();
349
+ // DI pattern: this.$FIELD.$METHOD($$$ARGS)
350
+ for (const m of rootNode.findAll('this.$FIELD.$METHOD($$$ARGS)')) {
351
+ const field = m.getMatch('FIELD')?.text();
352
+ const method = m.getMatch('METHOD')?.text();
353
+ if (!method || NOISE.has(method)) {
354
+ continue;
355
+ }
356
+ calls.push({
357
+ source: fp,
358
+ callName: method,
359
+ line: m.range().start.line,
360
+ diField: field,
361
+ });
362
+ }
363
+ // Direct calls + self/super detection via shared function
364
+ extractCalls(rootNode, fp, TS_CALL_CONFIG, calls);
365
+ }