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.
Files changed (93) hide show
  1. code_graph_builder/__init__.py +82 -0
  2. code_graph_builder/builder.py +366 -0
  3. code_graph_builder/cgb_cli.py +32 -0
  4. code_graph_builder/cli.py +564 -0
  5. code_graph_builder/commands_cli.py +1288 -0
  6. code_graph_builder/config.py +340 -0
  7. code_graph_builder/constants.py +708 -0
  8. code_graph_builder/embeddings/__init__.py +40 -0
  9. code_graph_builder/embeddings/qwen3_embedder.py +573 -0
  10. code_graph_builder/embeddings/vector_store.py +584 -0
  11. code_graph_builder/examples/__init__.py +0 -0
  12. code_graph_builder/examples/example_configuration.py +276 -0
  13. code_graph_builder/examples/example_kuzu_usage.py +109 -0
  14. code_graph_builder/examples/example_semantic_search_full.py +347 -0
  15. code_graph_builder/examples/generate_wiki.py +915 -0
  16. code_graph_builder/examples/graph_export_example.py +100 -0
  17. code_graph_builder/examples/rag_example.py +206 -0
  18. code_graph_builder/examples/test_cli_demo.py +129 -0
  19. code_graph_builder/examples/test_embedding_api.py +153 -0
  20. code_graph_builder/examples/test_kuzu_local.py +190 -0
  21. code_graph_builder/examples/test_rag_redis.py +390 -0
  22. code_graph_builder/graph_updater.py +605 -0
  23. code_graph_builder/guidance/__init__.py +1 -0
  24. code_graph_builder/guidance/agent.py +123 -0
  25. code_graph_builder/guidance/prompts.py +74 -0
  26. code_graph_builder/guidance/toolset.py +264 -0
  27. code_graph_builder/language_spec.py +536 -0
  28. code_graph_builder/mcp/__init__.py +21 -0
  29. code_graph_builder/mcp/api_doc_generator.py +764 -0
  30. code_graph_builder/mcp/file_editor.py +207 -0
  31. code_graph_builder/mcp/pipeline.py +777 -0
  32. code_graph_builder/mcp/server.py +161 -0
  33. code_graph_builder/mcp/tools.py +1800 -0
  34. code_graph_builder/models.py +115 -0
  35. code_graph_builder/parser_loader.py +344 -0
  36. code_graph_builder/parsers/__init__.py +7 -0
  37. code_graph_builder/parsers/call_processor.py +306 -0
  38. code_graph_builder/parsers/call_resolver.py +139 -0
  39. code_graph_builder/parsers/definition_processor.py +796 -0
  40. code_graph_builder/parsers/factory.py +119 -0
  41. code_graph_builder/parsers/import_processor.py +293 -0
  42. code_graph_builder/parsers/structure_processor.py +145 -0
  43. code_graph_builder/parsers/type_inference.py +143 -0
  44. code_graph_builder/parsers/utils.py +134 -0
  45. code_graph_builder/rag/__init__.py +68 -0
  46. code_graph_builder/rag/camel_agent.py +429 -0
  47. code_graph_builder/rag/client.py +298 -0
  48. code_graph_builder/rag/config.py +239 -0
  49. code_graph_builder/rag/cypher_generator.py +67 -0
  50. code_graph_builder/rag/llm_backend.py +210 -0
  51. code_graph_builder/rag/markdown_generator.py +352 -0
  52. code_graph_builder/rag/prompt_templates.py +440 -0
  53. code_graph_builder/rag/rag_engine.py +640 -0
  54. code_graph_builder/rag/review_report.md +172 -0
  55. code_graph_builder/rag/tests/__init__.py +3 -0
  56. code_graph_builder/rag/tests/test_camel_agent.py +313 -0
  57. code_graph_builder/rag/tests/test_client.py +221 -0
  58. code_graph_builder/rag/tests/test_config.py +177 -0
  59. code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
  60. code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
  61. code_graph_builder/services/__init__.py +39 -0
  62. code_graph_builder/services/graph_service.py +465 -0
  63. code_graph_builder/services/kuzu_service.py +665 -0
  64. code_graph_builder/services/memory_service.py +171 -0
  65. code_graph_builder/settings.py +75 -0
  66. code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
  67. code_graph_builder/tests/__init__.py +1 -0
  68. code_graph_builder/tests/run_acceptance_check.py +378 -0
  69. code_graph_builder/tests/test_api_find.py +231 -0
  70. code_graph_builder/tests/test_api_find_integration.py +226 -0
  71. code_graph_builder/tests/test_basic.py +78 -0
  72. code_graph_builder/tests/test_c_api_extraction.py +388 -0
  73. code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
  74. code_graph_builder/tests/test_embedder.py +411 -0
  75. code_graph_builder/tests/test_integration_semantic.py +434 -0
  76. code_graph_builder/tests/test_mcp_protocol.py +298 -0
  77. code_graph_builder/tests/test_mcp_user_flow.py +190 -0
  78. code_graph_builder/tests/test_rag.py +404 -0
  79. code_graph_builder/tests/test_settings.py +135 -0
  80. code_graph_builder/tests/test_step1_graph_build.py +264 -0
  81. code_graph_builder/tests/test_step2_api_docs.py +323 -0
  82. code_graph_builder/tests/test_step3_embedding.py +278 -0
  83. code_graph_builder/tests/test_vector_store.py +552 -0
  84. code_graph_builder/tools/__init__.py +40 -0
  85. code_graph_builder/tools/graph_query.py +495 -0
  86. code_graph_builder/tools/semantic_search.py +387 -0
  87. code_graph_builder/types.py +333 -0
  88. code_graph_builder/utils/__init__.py +0 -0
  89. code_graph_builder/utils/path_utils.py +30 -0
  90. code_graph_builder-0.2.0.dist-info/METADATA +321 -0
  91. code_graph_builder-0.2.0.dist-info/RECORD +93 -0
  92. code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
  93. 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)}