code-graph-builder 0.2.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.
- code_graph_builder/__init__.py +82 -0
- code_graph_builder/builder.py +366 -0
- code_graph_builder/cgb_cli.py +32 -0
- code_graph_builder/cli.py +564 -0
- code_graph_builder/commands_cli.py +1288 -0
- code_graph_builder/config.py +340 -0
- code_graph_builder/constants.py +708 -0
- code_graph_builder/embeddings/__init__.py +40 -0
- code_graph_builder/embeddings/qwen3_embedder.py +573 -0
- code_graph_builder/embeddings/vector_store.py +584 -0
- code_graph_builder/examples/__init__.py +0 -0
- code_graph_builder/examples/example_configuration.py +276 -0
- code_graph_builder/examples/example_kuzu_usage.py +109 -0
- code_graph_builder/examples/example_semantic_search_full.py +347 -0
- code_graph_builder/examples/generate_wiki.py +915 -0
- code_graph_builder/examples/graph_export_example.py +100 -0
- code_graph_builder/examples/rag_example.py +206 -0
- code_graph_builder/examples/test_cli_demo.py +129 -0
- code_graph_builder/examples/test_embedding_api.py +153 -0
- code_graph_builder/examples/test_kuzu_local.py +190 -0
- code_graph_builder/examples/test_rag_redis.py +390 -0
- code_graph_builder/graph_updater.py +605 -0
- code_graph_builder/guidance/__init__.py +1 -0
- code_graph_builder/guidance/agent.py +123 -0
- code_graph_builder/guidance/prompts.py +74 -0
- code_graph_builder/guidance/toolset.py +264 -0
- code_graph_builder/language_spec.py +536 -0
- code_graph_builder/mcp/__init__.py +21 -0
- code_graph_builder/mcp/api_doc_generator.py +764 -0
- code_graph_builder/mcp/file_editor.py +207 -0
- code_graph_builder/mcp/pipeline.py +777 -0
- code_graph_builder/mcp/server.py +161 -0
- code_graph_builder/mcp/tools.py +1800 -0
- code_graph_builder/models.py +115 -0
- code_graph_builder/parser_loader.py +344 -0
- code_graph_builder/parsers/__init__.py +7 -0
- code_graph_builder/parsers/call_processor.py +306 -0
- code_graph_builder/parsers/call_resolver.py +139 -0
- code_graph_builder/parsers/definition_processor.py +796 -0
- code_graph_builder/parsers/factory.py +119 -0
- code_graph_builder/parsers/import_processor.py +293 -0
- code_graph_builder/parsers/structure_processor.py +145 -0
- code_graph_builder/parsers/type_inference.py +143 -0
- code_graph_builder/parsers/utils.py +134 -0
- code_graph_builder/rag/__init__.py +68 -0
- code_graph_builder/rag/camel_agent.py +429 -0
- code_graph_builder/rag/client.py +298 -0
- code_graph_builder/rag/config.py +239 -0
- code_graph_builder/rag/cypher_generator.py +67 -0
- code_graph_builder/rag/llm_backend.py +210 -0
- code_graph_builder/rag/markdown_generator.py +352 -0
- code_graph_builder/rag/prompt_templates.py +440 -0
- code_graph_builder/rag/rag_engine.py +640 -0
- code_graph_builder/rag/review_report.md +172 -0
- code_graph_builder/rag/tests/__init__.py +3 -0
- code_graph_builder/rag/tests/test_camel_agent.py +313 -0
- code_graph_builder/rag/tests/test_client.py +221 -0
- code_graph_builder/rag/tests/test_config.py +177 -0
- code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
- code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
- code_graph_builder/services/__init__.py +39 -0
- code_graph_builder/services/graph_service.py +465 -0
- code_graph_builder/services/kuzu_service.py +665 -0
- code_graph_builder/services/memory_service.py +171 -0
- code_graph_builder/settings.py +75 -0
- code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
- code_graph_builder/tests/__init__.py +1 -0
- code_graph_builder/tests/run_acceptance_check.py +378 -0
- code_graph_builder/tests/test_api_find.py +231 -0
- code_graph_builder/tests/test_api_find_integration.py +226 -0
- code_graph_builder/tests/test_basic.py +78 -0
- code_graph_builder/tests/test_c_api_extraction.py +388 -0
- code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
- code_graph_builder/tests/test_embedder.py +411 -0
- code_graph_builder/tests/test_integration_semantic.py +434 -0
- code_graph_builder/tests/test_mcp_protocol.py +298 -0
- code_graph_builder/tests/test_mcp_user_flow.py +190 -0
- code_graph_builder/tests/test_rag.py +404 -0
- code_graph_builder/tests/test_settings.py +135 -0
- code_graph_builder/tests/test_step1_graph_build.py +264 -0
- code_graph_builder/tests/test_step2_api_docs.py +323 -0
- code_graph_builder/tests/test_step3_embedding.py +278 -0
- code_graph_builder/tests/test_vector_store.py +552 -0
- code_graph_builder/tools/__init__.py +40 -0
- code_graph_builder/tools/graph_query.py +495 -0
- code_graph_builder/tools/semantic_search.py +387 -0
- code_graph_builder/types.py +333 -0
- code_graph_builder/utils/__init__.py +0 -0
- code_graph_builder/utils/path_utils.py +30 -0
- code_graph_builder-0.2.0.dist-info/METADATA +321 -0
- code_graph_builder-0.2.0.dist-info/RECORD +93 -0
- code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
- code_graph_builder-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import diff_match_patch
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from tree_sitter import Node, Parser
|
|
10
|
+
|
|
11
|
+
from .. import constants as cs
|
|
12
|
+
from ..language_spec import get_language_for_extension, get_language_spec
|
|
13
|
+
from ..parser_loader import load_parsers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileEditor:
|
|
17
|
+
def __init__(self, repo_root: Path) -> None:
|
|
18
|
+
self._repo_root = repo_root.resolve()
|
|
19
|
+
self._dmp = diff_match_patch.diff_match_patch()
|
|
20
|
+
self._parsers, _ = load_parsers()
|
|
21
|
+
logger.info(f"FileEditor initialised for: {self._repo_root}")
|
|
22
|
+
|
|
23
|
+
def _get_real_extension(self, file_path: Path) -> str:
|
|
24
|
+
ext = file_path.suffix
|
|
25
|
+
if ext == ".tmp":
|
|
26
|
+
stem = file_path.stem
|
|
27
|
+
if "." in stem:
|
|
28
|
+
return "." + stem.split(".")[-1]
|
|
29
|
+
return ext
|
|
30
|
+
|
|
31
|
+
def _get_parser(self, file_path: Path) -> Parser | None:
|
|
32
|
+
ext = self._get_real_extension(file_path)
|
|
33
|
+
lang = get_language_for_extension(ext)
|
|
34
|
+
return self._parsers.get(lang) if lang else None
|
|
35
|
+
|
|
36
|
+
def _extract_declarator_name(self, node: Node) -> str | None:
|
|
37
|
+
if node.type == "identifier" and node.text:
|
|
38
|
+
return node.text.decode(cs.ENCODING_UTF8)
|
|
39
|
+
child = node.child_by_field_name("declarator")
|
|
40
|
+
if child:
|
|
41
|
+
return self._extract_declarator_name(child)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def _extract_function_name(self, node: Node) -> str | None:
|
|
45
|
+
name_node = node.child_by_field_name("name")
|
|
46
|
+
if name_node and name_node.text:
|
|
47
|
+
return name_node.text.decode(cs.ENCODING_UTF8)
|
|
48
|
+
declarator = node.child_by_field_name("declarator")
|
|
49
|
+
if declarator:
|
|
50
|
+
return self._extract_declarator_name(declarator)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def locate_function(
|
|
54
|
+
self,
|
|
55
|
+
file_path: Path,
|
|
56
|
+
function_name: str,
|
|
57
|
+
line_number: int | None = None,
|
|
58
|
+
) -> dict[str, Any] | None:
|
|
59
|
+
parser = self._get_parser(file_path)
|
|
60
|
+
if not parser:
|
|
61
|
+
logger.warning(f"No parser for: {file_path}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
content = file_path.read_bytes()
|
|
66
|
+
except OSError as exc:
|
|
67
|
+
logger.warning(f"Cannot read {file_path}: {exc}")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
tree = parser.parse(content)
|
|
71
|
+
ext = self._get_real_extension(file_path)
|
|
72
|
+
lang_config = get_language_spec(ext)
|
|
73
|
+
if not lang_config:
|
|
74
|
+
logger.warning(f"No language config for extension: {ext}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
matching: list[dict[str, Any]] = []
|
|
78
|
+
|
|
79
|
+
def traverse(node: Node, parent_class: str | None = None) -> None:
|
|
80
|
+
if node.type in lang_config.function_node_types:
|
|
81
|
+
func_name = self._extract_function_name(node)
|
|
82
|
+
if func_name:
|
|
83
|
+
qualified = (
|
|
84
|
+
f"{parent_class}.{func_name}" if parent_class else func_name
|
|
85
|
+
)
|
|
86
|
+
if function_name in (func_name, qualified):
|
|
87
|
+
matching.append(
|
|
88
|
+
{
|
|
89
|
+
"node": node,
|
|
90
|
+
"simple_name": func_name,
|
|
91
|
+
"qualified_name": qualified,
|
|
92
|
+
"parent_class": parent_class,
|
|
93
|
+
"line_number": node.start_point[0] + 1,
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
current_class = parent_class
|
|
99
|
+
if node.type in lang_config.class_node_types:
|
|
100
|
+
name_node = node.child_by_field_name("name")
|
|
101
|
+
if name_node and name_node.text:
|
|
102
|
+
current_class = name_node.text.decode(cs.ENCODING_UTF8)
|
|
103
|
+
|
|
104
|
+
for child in node.children:
|
|
105
|
+
traverse(child, current_class)
|
|
106
|
+
|
|
107
|
+
traverse(tree.root_node)
|
|
108
|
+
|
|
109
|
+
if not matching:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
match_count = len(matching)
|
|
113
|
+
|
|
114
|
+
def _build_result(m: dict[str, Any]) -> dict[str, Any] | None:
|
|
115
|
+
node: Node = m["node"]
|
|
116
|
+
if node.text is None:
|
|
117
|
+
return None
|
|
118
|
+
return {
|
|
119
|
+
"qualified_name": m["qualified_name"],
|
|
120
|
+
"source_code": node.text.decode(cs.ENCODING_UTF8),
|
|
121
|
+
"start_line": m["line_number"],
|
|
122
|
+
"end_line": node.end_point[0] + 1,
|
|
123
|
+
"file_path": str(file_path),
|
|
124
|
+
"match_count": match_count,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if match_count == 1:
|
|
128
|
+
return _build_result(matching[0])
|
|
129
|
+
|
|
130
|
+
if line_number is not None:
|
|
131
|
+
for m in matching:
|
|
132
|
+
if m["line_number"] == line_number:
|
|
133
|
+
return _build_result(m)
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"'{function_name}' not found at line {line_number} in {file_path}"
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
if cs.SEPARATOR_DOT in function_name:
|
|
140
|
+
for m in matching:
|
|
141
|
+
if m["qualified_name"] == function_name:
|
|
142
|
+
return _build_result(m)
|
|
143
|
+
logger.warning(
|
|
144
|
+
f"'{function_name}' not found by qualified name in {file_path}"
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
details = ", ".join(
|
|
149
|
+
f"'{m['qualified_name']}' at line {m['line_number']}" for m in matching
|
|
150
|
+
)
|
|
151
|
+
logger.warning(
|
|
152
|
+
f"Ambiguous: '{function_name}' has {match_count} matches in {file_path}: "
|
|
153
|
+
f"{details}. Returning first."
|
|
154
|
+
)
|
|
155
|
+
return _build_result(matching[0])
|
|
156
|
+
|
|
157
|
+
def get_diff(
|
|
158
|
+
self, original_code: str, new_code: str, label: str = "function"
|
|
159
|
+
) -> str:
|
|
160
|
+
diff = difflib.unified_diff(
|
|
161
|
+
original_code.splitlines(keepends=True),
|
|
162
|
+
new_code.splitlines(keepends=True),
|
|
163
|
+
fromfile=f"original/{label}",
|
|
164
|
+
tofile=f"new/{label}",
|
|
165
|
+
)
|
|
166
|
+
return "".join(diff)
|
|
167
|
+
|
|
168
|
+
def replace_code_block(
|
|
169
|
+
self,
|
|
170
|
+
file_path: Path,
|
|
171
|
+
target_block: str,
|
|
172
|
+
replacement_block: str,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
try:
|
|
175
|
+
if not file_path.is_file():
|
|
176
|
+
return {"success": False, "error": f"File not found: {file_path}"}
|
|
177
|
+
|
|
178
|
+
original = file_path.read_text(encoding=cs.ENCODING_UTF8)
|
|
179
|
+
|
|
180
|
+
if target_block not in original:
|
|
181
|
+
return {"success": False, "error": "Target block not found in file."}
|
|
182
|
+
|
|
183
|
+
multiple = original.count(target_block) > 1
|
|
184
|
+
modified = original.replace(target_block, replacement_block, 1)
|
|
185
|
+
|
|
186
|
+
if original == modified:
|
|
187
|
+
return {
|
|
188
|
+
"success": False,
|
|
189
|
+
"error": "No changes: replacement is identical to target.",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
patches = self._dmp.patch_make(original, modified)
|
|
193
|
+
patched, results = self._dmp.patch_apply(patches, original)
|
|
194
|
+
|
|
195
|
+
if not all(results):
|
|
196
|
+
return {
|
|
197
|
+
"success": False,
|
|
198
|
+
"error": "patch_apply failed to apply all patches.",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
file_path.write_text(patched, encoding=cs.ENCODING_UTF8)
|
|
202
|
+
logger.success(f"Surgical replace succeeded: {file_path}")
|
|
203
|
+
|
|
204
|
+
return {"success": True, "multiple_occurrences": multiple, "error": None}
|
|
205
|
+
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
return {"success": False, "error": str(exc)}
|