codegraph-brain 0.6.0__py3-none-any.whl

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 (78) hide show
  1. cgis/__init__.py +10 -0
  2. cgis/__main__.py +16 -0
  3. cgis/api/.gitkeep +0 -0
  4. cgis/api/__init__.py +1 -0
  5. cgis/api/mcp_server.py +550 -0
  6. cgis/cli.py +1516 -0
  7. cgis/core/.gitkeep +0 -0
  8. cgis/core/models.py +140 -0
  9. cgis/extractors/.gitkeep +0 -0
  10. cgis/extractors/_python_ast.py +194 -0
  11. cgis/extractors/_python_classes.py +126 -0
  12. cgis/extractors/_python_functions.py +312 -0
  13. cgis/extractors/_python_imports.py +188 -0
  14. cgis/extractors/_python_types.py +84 -0
  15. cgis/extractors/base.py +42 -0
  16. cgis/extractors/python_extractor.py +235 -0
  17. cgis/extractors/typescript_extractor.py +310 -0
  18. cgis/guardian/__init__.py +1 -0
  19. cgis/guardian/bench.py +199 -0
  20. cgis/guardian/chunked.py +240 -0
  21. cgis/guardian/chunker.py +148 -0
  22. cgis/guardian/collector.py +309 -0
  23. cgis/guardian/core.py +120 -0
  24. cgis/guardian/diff_index.py +151 -0
  25. cgis/guardian/findings.py +70 -0
  26. cgis/guardian/github_poster.py +133 -0
  27. cgis/guardian/metrics.py +108 -0
  28. cgis/guardian/prompts.py +217 -0
  29. cgis/guardian/providers/__init__.py +1 -0
  30. cgis/guardian/providers/base.py +112 -0
  31. cgis/guardian/providers/gemini.py +83 -0
  32. cgis/guardian/providers/mistral.py +83 -0
  33. cgis/guardian/providers/ollama.py +99 -0
  34. cgis/guardian/recording.py +82 -0
  35. cgis/guardian/render.py +107 -0
  36. cgis/guardian/runner.py +295 -0
  37. cgis/guardian/skeptic.py +208 -0
  38. cgis/pipeline.py +252 -0
  39. cgis/py.typed +0 -0
  40. cgis/query/analysis/__init__.py +0 -0
  41. cgis/query/analysis/analyzer.py +241 -0
  42. cgis/query/analysis/anomaly.py +34 -0
  43. cgis/query/analysis/cohesion.py +277 -0
  44. cgis/query/analysis/health.py +128 -0
  45. cgis/query/analysis/suggest_service.py +221 -0
  46. cgis/query/context/__init__.py +0 -0
  47. cgis/query/context/audit.py +134 -0
  48. cgis/query/context/context_service.py +129 -0
  49. cgis/query/context/prompt.py +184 -0
  50. cgis/query/context/snippet.py +96 -0
  51. cgis/query/drift/__init__.py +0 -0
  52. cgis/query/drift/_scc.py +90 -0
  53. cgis/query/drift/drift.py +867 -0
  54. cgis/query/drift/drift_service.py +217 -0
  55. cgis/query/drift/fingerprint.py +255 -0
  56. cgis/query/drift/fractal.py +292 -0
  57. cgis/query/drift/ontology_init.py +470 -0
  58. cgis/query/drift/quotient.py +75 -0
  59. cgis/query/drift/triads.py +234 -0
  60. cgis/query/engine.py +170 -0
  61. cgis/query/fqn.py +65 -0
  62. cgis/query/render/__init__.py +0 -0
  63. cgis/query/render/graph_json.py +42 -0
  64. cgis/query/render/mermaid.py +235 -0
  65. cgis/query/render/metrics.py +346 -0
  66. cgis/resolver/.gitkeep +0 -0
  67. cgis/resolver/__init__.py +1 -0
  68. cgis/resolver/engine.py +145 -0
  69. cgis/resolver/indices.py +184 -0
  70. cgis/resolver/symbols.py +179 -0
  71. cgis/resolver/uplift.py +263 -0
  72. cgis/storage/.gitkeep +0 -0
  73. cgis/storage/sqlite_store.py +589 -0
  74. codegraph_brain-0.6.0.dist-info/METADATA +240 -0
  75. codegraph_brain-0.6.0.dist-info/RECORD +78 -0
  76. codegraph_brain-0.6.0.dist-info/WHEEL +4 -0
  77. codegraph_brain-0.6.0.dist-info/entry_points.txt +3 -0
  78. codegraph_brain-0.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,235 @@
1
+ """Implements Python Extractor."""
2
+
3
+ import tree_sitter_python as tspython
4
+ from tree_sitter import Language, Parser
5
+ from tree_sitter import Node as BaseNode
6
+
7
+ from cgis.core.models import Edge, Node, NodeType
8
+ from cgis.extractors._python_ast import extract_decorator_names, is_module_level_assignment
9
+ from cgis.extractors._python_ast import file_path_to_module_fqn as _file_path_to_module_fqn
10
+ from cgis.extractors._python_classes import ClassHandler
11
+ from cgis.extractors._python_functions import FunctionHandler
12
+ from cgis.extractors._python_imports import ImportHandler
13
+ from cgis.extractors._python_types import TypeResolver
14
+ from cgis.extractors.base import BaseExtractor
15
+
16
+
17
+ def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> str:
18
+ """Convert a file path to a dot-separated module namespace.
19
+
20
+ Examples:
21
+ src/cgis/pipeline.py -> src.cgis.pipeline
22
+ src/cgis/__init__.py -> src.cgis
23
+ /abs/path/mod.py -> abs.path.mod
24
+ C:\\path\\to\\mod.py -> path.to.mod
25
+
26
+ With source_root="src":
27
+ src/cgis/pipeline.py -> cgis.pipeline
28
+ """
29
+ return _file_path_to_module_fqn(file_path, source_root)
30
+
31
+
32
+ class PythonExtractor(BaseExtractor):
33
+ """
34
+ A concrete extractor for Python source code using tree-sitter.
35
+
36
+ Acts as a thin orchestrator: the recursive AST walk dispatches each node to
37
+ a focused collaborator (ImportHandler, FunctionHandler, ClassHandler,
38
+ TypeResolver) which owns the actual node/edge extraction.
39
+ """
40
+
41
+ LANG: str = "python"
42
+
43
+ def __init__(self, source_roots: list[str] | None = None) -> None:
44
+ """Initialise the tree-sitter Python parser and the extraction handlers."""
45
+ super().__init__(source_roots=source_roots)
46
+ self._parser = Parser(Language(tspython.language()))
47
+ self._imports = ImportHandler()
48
+ self._types = TypeResolver(self._pick_source_root)
49
+ self._functions = FunctionHandler(self._pick_source_root, self._types)
50
+ self._classes = ClassHandler(self._pick_source_root)
51
+
52
+ def parse(self, code: str, file_path: str) -> tuple[list[Node], list[Edge]]:
53
+ """
54
+ Extracts structural nodes and edges (Functions, Classes, Imports).
55
+ """
56
+ code_bytes = code.encode("utf8")
57
+ tree = self._parser.parse(code_bytes)
58
+ root_node: BaseNode = tree.root_node
59
+
60
+ nodes: list[Node] = []
61
+ edges: list[Edge] = []
62
+ import_map: dict[str, str] = {}
63
+ module_fqn = file_path_to_module_fqn(file_path, self._pick_source_root(file_path))
64
+ local_types_acc: dict[str, dict[str, str]] = {}
65
+
66
+ self._walk(
67
+ root_node,
68
+ code_bytes,
69
+ file_path,
70
+ nodes,
71
+ edges,
72
+ import_map=import_map,
73
+ module_fqn=module_fqn,
74
+ local_types_acc=local_types_acc,
75
+ )
76
+
77
+ # Apply accumulated local types from assignments and param annotations
78
+ nodes_by_id = {n.id: i for i, n in enumerate(nodes)}
79
+ for func_id, lt in local_types_acc.items():
80
+ if func_id in nodes_by_id:
81
+ i = nodes_by_id[func_id]
82
+ nodes[i] = nodes[i].model_copy(
83
+ update={"metadata": {**nodes[i].metadata, "local_types": lt}}
84
+ )
85
+
86
+ file_node = Node(
87
+ id=module_fqn,
88
+ type=NodeType.FILE,
89
+ name=file_path.rsplit("/", maxsplit=1)[-1].rsplit("\\", maxsplit=1)[-1],
90
+ file_path=file_path,
91
+ start_line=1,
92
+ end_line=root_node.end_point.row + 1,
93
+ metadata={"import_map": import_map},
94
+ )
95
+ nodes.insert(0, file_node)
96
+
97
+ return nodes, edges
98
+
99
+ def _walk(
100
+ self,
101
+ node: BaseNode,
102
+ code_bytes: bytes,
103
+ file_path: str,
104
+ nodes: list[Node],
105
+ edges: list[Edge],
106
+ current_func_node: Node | None = None,
107
+ import_map: dict[str, str] | None = None,
108
+ module_fqn: str | None = None,
109
+ local_types_acc: dict[str, dict[str, str]] | None = None,
110
+ ) -> None:
111
+ """
112
+ Recursive AST walker that dispatches each node to its handler.
113
+ """
114
+ if node.type in ("import_statement", "import_from_statement"):
115
+ self._imports.handle(node, code_bytes, file_path, import_map, module_fqn, edges)
116
+ return # never recurse into import nodes
117
+
118
+ if node.type == "decorated_definition":
119
+ self._handle_decorated_definition(
120
+ node,
121
+ code_bytes,
122
+ file_path,
123
+ nodes,
124
+ edges,
125
+ import_map,
126
+ module_fqn,
127
+ local_types_acc,
128
+ )
129
+ return # prevent double-processing the inner definition
130
+
131
+ next_func_node = current_func_node
132
+ if node.type in ("function_definition", "async_function_definition"):
133
+ next_func_node = self._functions.process_function_node(
134
+ node, code_bytes, file_path, nodes, edges, module_fqn or ""
135
+ )
136
+ elif node.type == "class_definition":
137
+ self._classes.process_class_node(
138
+ node, code_bytes, file_path, nodes, edges, module_fqn or ""
139
+ )
140
+ next_func_node = None
141
+ elif node.type == "call" and current_func_node:
142
+ self._functions.process_call_node(
143
+ node, code_bytes, file_path, current_func_node.id, edges
144
+ )
145
+ elif node.type == "assignment" and current_func_node and local_types_acc is not None:
146
+ self._functions.collect_assignment_type(
147
+ node, code_bytes, import_map, current_func_node, local_types_acc
148
+ )
149
+ elif is_module_level_assignment(node, code_bytes, current_func_node):
150
+ # True module level: not in a function (current_func_node) and not
151
+ # in a class body (get_fqn_prefix). Class-body DI aliases are out
152
+ # of scope (spec §6).
153
+ self._functions.process_module_assignment(
154
+ node, code_bytes, file_path, nodes, edges, module_fqn or ""
155
+ )
156
+ elif (
157
+ node.type in ("typed_parameter", "typed_default_parameter")
158
+ and current_func_node
159
+ and local_types_acc is not None
160
+ ):
161
+ self._functions.collect_param_type(
162
+ node, code_bytes, import_map, current_func_node, local_types_acc, edges
163
+ )
164
+
165
+ for child in node.children:
166
+ self._walk(
167
+ child,
168
+ code_bytes,
169
+ file_path,
170
+ nodes,
171
+ edges,
172
+ next_func_node,
173
+ import_map=import_map,
174
+ module_fqn=module_fqn,
175
+ local_types_acc=local_types_acc,
176
+ )
177
+
178
+ def _handle_decorated_definition(
179
+ self,
180
+ node: BaseNode,
181
+ code_bytes: bytes,
182
+ file_path: str,
183
+ nodes: list[Node],
184
+ edges: list[Edge],
185
+ import_map: dict[str, str] | None,
186
+ module_fqn: str | None,
187
+ local_types_acc: dict[str, dict[str, str]] | None,
188
+ ) -> None:
189
+ """Process a decorated function or class definition, forwarding decorator names."""
190
+ raw_decorators = extract_decorator_names(node, code_bytes)
191
+ for child in node.children:
192
+ if child.type in ("function_definition", "async_function_definition"):
193
+ inner = self._functions.process_function_node(
194
+ child,
195
+ code_bytes,
196
+ file_path,
197
+ nodes,
198
+ edges,
199
+ module_fqn or "",
200
+ decorators=raw_decorators,
201
+ )
202
+ for grandchild in child.children:
203
+ self._walk(
204
+ grandchild,
205
+ code_bytes,
206
+ file_path,
207
+ nodes,
208
+ edges,
209
+ inner,
210
+ import_map=import_map,
211
+ module_fqn=module_fqn,
212
+ local_types_acc=local_types_acc,
213
+ )
214
+ elif child.type == "class_definition":
215
+ self._classes.process_class_node(
216
+ child,
217
+ code_bytes,
218
+ file_path,
219
+ nodes,
220
+ edges,
221
+ module_fqn or "",
222
+ decorators=raw_decorators,
223
+ )
224
+ for grandchild in child.children:
225
+ self._walk(
226
+ grandchild,
227
+ code_bytes,
228
+ file_path,
229
+ nodes,
230
+ edges,
231
+ None,
232
+ import_map=import_map,
233
+ module_fqn=module_fqn,
234
+ local_types_acc=local_types_acc,
235
+ )
@@ -0,0 +1,310 @@
1
+ """Implements TypeScript/TSX Extractor using tree-sitter."""
2
+
3
+ import tree_sitter_typescript as tsts
4
+ from tree_sitter import Language, Parser
5
+ from tree_sitter import Node as TSNode
6
+
7
+ from cgis.core.models import Edge, EdgeType, Node, NodeNamespace, NodeType
8
+ from cgis.extractors.base import BaseExtractor
9
+
10
+ _RAW_CALL_PREFIX = "raw_call:"
11
+ _EXPORT_UNWRAP_TYPES = frozenset(
12
+ {
13
+ "class_declaration",
14
+ "abstract_class_declaration",
15
+ "function_declaration",
16
+ "lexical_declaration",
17
+ }
18
+ )
19
+
20
+
21
+ def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> str:
22
+ """Convert a TS/TSX file path to a dot-separated module namespace.
23
+
24
+ Examples:
25
+ src/api/handler.ts -> src.api.handler
26
+ src/components/index.tsx -> src.components
27
+ C:\\path\\to\\mod.ts -> path.to.mod
28
+
29
+ With source_root="src":
30
+ src/api/handler.ts -> api.handler
31
+ """
32
+ clean = file_path
33
+ if len(clean) >= 2 and clean[1] == ":" and clean[0].isalpha():
34
+ clean = clean[2:]
35
+ clean = clean.replace("\\", "/").lstrip("/")
36
+ if source_root:
37
+ sr = source_root.replace("\\", "/").strip("/") + "/"
38
+ if clean.startswith(sr):
39
+ clean = clean[len(sr) :]
40
+ for ext in (".tsx", ".ts", ".jsx", ".js"):
41
+ if clean.endswith(ext):
42
+ clean = clean[: -len(ext)]
43
+ break
44
+ if clean.endswith("/index"):
45
+ clean = clean[:-6]
46
+ return clean.replace("/", ".")
47
+
48
+
49
+ def _node_text(node: TSNode | None) -> str:
50
+ """Return decoded text of a tree-sitter node, or empty string."""
51
+ if node is None or node.text is None:
52
+ return ""
53
+ return node.text.decode("utf-8")
54
+
55
+
56
+ def _get_name(node: TSNode) -> str:
57
+ """Extract identifier/property_identifier/type_identifier text from a node."""
58
+ child = node.child_by_field_name("name")
59
+ if child is not None:
60
+ return _node_text(child)
61
+ for child in node.children:
62
+ if child.type in ("identifier", "property_identifier", "type_identifier"):
63
+ return _node_text(child)
64
+ return ""
65
+
66
+
67
+ def _make_node(fqn: str, name: str, node_type: NodeType, file_path: str, ts_node: TSNode) -> Node:
68
+ """Construct an internal Node from a tree-sitter node's position."""
69
+ return Node(
70
+ id=fqn,
71
+ name=name,
72
+ type=node_type,
73
+ file_path=file_path,
74
+ start_line=ts_node.start_point[0] + 1,
75
+ end_line=ts_node.end_point[0] + 1,
76
+ namespace=NodeNamespace.INTERNAL,
77
+ )
78
+
79
+
80
+ def _resolve_relative_import(namespace: str, raw_source: str) -> str:
81
+ """Resolve a relative import path to a dot-separated module FQN.
82
+
83
+ Walks segments so that ``./utils``, ``../api``, and ``../../lib`` all
84
+ produce the correct absolute FQN relative to *namespace*.
85
+ """
86
+ dir_parts = namespace.split(".")[:-1]
87
+ for segment in raw_source.replace("\\", "/").split("/"):
88
+ if segment == ".." and dir_parts:
89
+ dir_parts.pop()
90
+ elif segment and segment not in (".", ".."):
91
+ dir_parts.append(segment)
92
+ if dir_parts and dir_parts[-1] == "index":
93
+ dir_parts.pop()
94
+ return ".".join(dir_parts)
95
+
96
+
97
+ def _make_edge(source: str, target: str, edge_type: EdgeType) -> Edge:
98
+ """Construct an Edge with a deterministic id."""
99
+ return Edge(id=f"{source}->{target}", source=source, target=target, type=edge_type)
100
+
101
+
102
+ class TypeScriptExtractor(BaseExtractor):
103
+ """Extracts structural nodes and edges from TypeScript/TSX source files."""
104
+
105
+ def __init__(self, tsx: bool = False, source_roots: list[str] | None = None) -> None:
106
+ """Initialise the tree-sitter TypeScript (or TSX) parser."""
107
+ super().__init__(source_roots=source_roots)
108
+ lang = tsts.language_tsx() if tsx else tsts.language_typescript()
109
+ self._parser = Parser(Language(lang))
110
+
111
+ def parse(self, code: str, file_path: str) -> tuple[list[Node], list[Edge]]:
112
+ """Extract nodes and edges from TypeScript source code."""
113
+ code_bytes = code.encode("utf-8")
114
+ tree = self._parser.parse(code_bytes)
115
+ module_fqn = file_path_to_module_fqn(file_path, self._pick_source_root(file_path))
116
+
117
+ file_node = Node(
118
+ id=module_fqn,
119
+ name=file_path.replace("\\", "/").split("/")[-1],
120
+ type=NodeType.FILE,
121
+ file_path=file_path,
122
+ start_line=1,
123
+ end_line=tree.root_node.end_point[0] + 1,
124
+ namespace=NodeNamespace.INTERNAL,
125
+ )
126
+ nodes: list[Node] = [file_node]
127
+ edges: list[Edge] = []
128
+
129
+ self._walk(
130
+ tree.root_node,
131
+ namespace=module_fqn,
132
+ file_path=file_path,
133
+ file_id=module_fqn,
134
+ active_class_fqn=None,
135
+ nodes=nodes,
136
+ edges=edges,
137
+ )
138
+ return nodes, edges
139
+
140
+ def _walk(
141
+ self,
142
+ node: TSNode,
143
+ namespace: str,
144
+ file_path: str,
145
+ file_id: str,
146
+ active_class_fqn: str | None,
147
+ nodes: list[Node],
148
+ edges: list[Edge],
149
+ ) -> None:
150
+ """Recursively walk the AST, emitting nodes and edges."""
151
+ inner = node
152
+ if node.type == "export_statement":
153
+ for child in node.children:
154
+ if child.type in _EXPORT_UNWRAP_TYPES:
155
+ inner = child
156
+ break
157
+
158
+ if inner.type in ("class_declaration", "abstract_class_declaration"):
159
+ self._handle_class(inner, namespace, file_path, file_id, nodes, edges)
160
+ elif inner.type == "function_declaration":
161
+ self._handle_function(
162
+ inner, namespace, file_path, file_id, active_class_fqn, nodes, edges
163
+ )
164
+ elif inner.type == "lexical_declaration":
165
+ self._handle_lexical(
166
+ inner, namespace, file_path, file_id, active_class_fqn, nodes, edges
167
+ )
168
+ elif inner.type == "import_statement":
169
+ self._handle_import(inner, namespace, file_id, edges)
170
+ else:
171
+ for child in node.children:
172
+ self._walk(child, namespace, file_path, file_id, active_class_fqn, nodes, edges)
173
+
174
+ def _handle_class(
175
+ self,
176
+ node: TSNode,
177
+ namespace: str,
178
+ file_path: str,
179
+ file_id: str,
180
+ nodes: list[Node],
181
+ edges: list[Edge],
182
+ ) -> None:
183
+ """Emit a CLASS node + CONTAINS edge, then walk its body for methods."""
184
+ name = _get_name(node)
185
+ if not name:
186
+ return
187
+ class_fqn = f"{namespace}.{name}"
188
+ nodes.append(_make_node(class_fqn, name, NodeType.CLASS, file_path, node))
189
+ edges.append(_make_edge(file_id, class_fqn, EdgeType.CONTAINS))
190
+ body = node.child_by_field_name("body")
191
+ if body is not None:
192
+ for child in body.children:
193
+ if child.type in ("method_definition", "abstract_method_signature"):
194
+ self._handle_method(child, class_fqn, file_path, nodes, edges)
195
+
196
+ def _handle_method(
197
+ self,
198
+ node: TSNode,
199
+ class_fqn: str,
200
+ file_path: str,
201
+ nodes: list[Node],
202
+ edges: list[Edge],
203
+ ) -> None:
204
+ """Emit a METHOD node + DECLARES edge, then find calls in the body."""
205
+ name = _get_name(node)
206
+ if not name:
207
+ return
208
+ method_fqn = f"{class_fqn}.{name}"
209
+ nodes.append(_make_node(method_fqn, name, NodeType.METHOD, file_path, node))
210
+ edges.append(_make_edge(class_fqn, method_fqn, EdgeType.DECLARES))
211
+ self._find_calls(node, method_fqn, edges)
212
+
213
+ def _handle_function(
214
+ self,
215
+ node: TSNode,
216
+ namespace: str,
217
+ file_path: str,
218
+ file_id: str,
219
+ active_class_fqn: str | None,
220
+ nodes: list[Node],
221
+ edges: list[Edge],
222
+ ) -> None:
223
+ """Emit a FUNCTION node + CONTAINS edge, then find calls in the body."""
224
+ name = _get_name(node)
225
+ if not name:
226
+ return
227
+ func_fqn = f"{namespace}.{name}"
228
+ nodes.append(_make_node(func_fqn, name, NodeType.FUNCTION, file_path, node))
229
+ edges.append(_make_edge(active_class_fqn or file_id, func_fqn, EdgeType.CONTAINS))
230
+ self._find_calls(node, func_fqn, edges)
231
+
232
+ def _handle_lexical(
233
+ self,
234
+ node: TSNode,
235
+ namespace: str,
236
+ file_path: str,
237
+ file_id: str,
238
+ active_class_fqn: str | None,
239
+ nodes: list[Node],
240
+ edges: list[Edge],
241
+ ) -> None:
242
+ """Handle const/let arrow-function declarations."""
243
+ for child in node.children:
244
+ if child.type != "variable_declarator":
245
+ continue
246
+ value = child.child_by_field_name("value")
247
+ if value is None or value.type != "arrow_function":
248
+ continue
249
+ name = _node_text(child.child_by_field_name("name"))
250
+ if not name:
251
+ continue
252
+ func_fqn = f"{namespace}.{name}"
253
+ nodes.append(_make_node(func_fqn, name, NodeType.FUNCTION, file_path, node))
254
+ edges.append(_make_edge(active_class_fqn or file_id, func_fqn, EdgeType.CONTAINS))
255
+ self._find_calls(value, func_fqn, edges)
256
+
257
+ def _handle_import(
258
+ self,
259
+ node: TSNode,
260
+ namespace: str,
261
+ file_id: str,
262
+ edges: list[Edge],
263
+ ) -> None:
264
+ """Emit an IMPORTS edge for an import statement."""
265
+ raw_source = _node_text(node.child_by_field_name("source")).strip("'\"")
266
+ if not raw_source:
267
+ return
268
+ if raw_source.startswith("."):
269
+ target_fqn = _resolve_relative_import(namespace, raw_source)
270
+ else:
271
+ target_fqn = raw_source.replace("/", ".")
272
+ edges.append(
273
+ Edge(
274
+ id=f"{file_id}->import:{target_fqn}",
275
+ source=file_id,
276
+ target=target_fqn,
277
+ type=EdgeType.IMPORTS,
278
+ )
279
+ )
280
+
281
+ def _find_calls(self, node: TSNode, source_id: str, edges: list[Edge]) -> None:
282
+ """Recursively find call_expression nodes and emit CALLS edges."""
283
+ if node.type == "call_expression":
284
+ func_node = node.child_by_field_name("function")
285
+ if func_node is not None:
286
+ call_name = self._get_call_name(func_node)
287
+ if call_name:
288
+ target = f"{_RAW_CALL_PREFIX}{call_name}"
289
+ edges.append(
290
+ Edge(
291
+ id=f"{source_id}->{target}@{node.start_point[0]}:{node.start_point[1]}",
292
+ source=source_id,
293
+ target=target,
294
+ type=EdgeType.CALLS,
295
+ confidence=0.1,
296
+ )
297
+ )
298
+ for child in node.children:
299
+ self._find_calls(child, source_id, edges)
300
+
301
+ def _get_call_name(self, node: TSNode) -> str:
302
+ """Extract a call target name from identifier or member_expression."""
303
+ if node.type == "identifier":
304
+ return _node_text(node)
305
+ if node.type == "member_expression":
306
+ obj_text = _node_text(node.child_by_field_name("object"))
307
+ prop_text = _node_text(node.child_by_field_name("property"))
308
+ if obj_text and prop_text:
309
+ return f"self.{prop_text}" if obj_text == "this" else f"{obj_text}.{prop_text}"
310
+ return ""
@@ -0,0 +1 @@
1
+ """CGIS Guardian — AI-powered code review."""