codegraph-engine 2.2.0__py3-none-any.whl → 2.3.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.
- codegraph/__init__.py +1 -1
- codegraph/agent_capabilities.py +245 -4
- codegraph/branch_comparison.py +197 -0
- codegraph/change_impact.py +337 -0
- codegraph/cli.py +238 -0
- codegraph/context_freshness.py +349 -0
- codegraph/git_state.py +543 -0
- codegraph/indexing/parser.py +55 -21
- codegraph/mcp/server.py +69 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/structural_diff.py +540 -0
- codegraph/symbol_history.py +275 -0
- codegraph_engine-2.3.0.dist-info/METADATA +733 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/RECORD +18 -11
- codegraph_engine-2.2.0.dist-info/METADATA +0 -642
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.2.0.dist-info → codegraph_engine-2.3.0.dist-info}/top_level.txt +0 -0
codegraph/mcp/server.py
CHANGED
|
@@ -956,6 +956,65 @@ def create_server(
|
|
|
956
956
|
with indexer.session() as con:
|
|
957
957
|
return runtime_reconcile_static_runtime(con, indexer.repository, symbol=symbol, route=route, table=table)
|
|
958
958
|
|
|
959
|
+
# -----------------------------------------------------------------------
|
|
960
|
+
# Git Intelligence Tools (5 Unified Capabilities)
|
|
961
|
+
# -----------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
def get_git_state() -> dict[str, Any]:
|
|
964
|
+
"""Track current branch, HEAD, indexed HEAD, working-tree modifications, staged files, and explicit freshness states (CLEAN, DIRTY, STALE, REINDEXING, ERROR). Use when asking if the index is up to date or what uncommitted changes exist. Does not perform a full repository scan when an incremental update is safe."""
|
|
965
|
+
from codegraph.git_state import get_git_state as impl_get_git_state
|
|
966
|
+
with indexer.session() as con:
|
|
967
|
+
return impl_get_git_state(indexer.repository, con=con).as_dict()
|
|
968
|
+
|
|
969
|
+
def compare_git(base: str = "HEAD~1", head: str = "HEAD", branch_comparison: bool = False) -> dict[str, Any]:
|
|
970
|
+
"""Compare two Git revisions, commits, or branches structurally. Detects added/deleted/modified/renamed files, AST symbol changes, relationship diffs, routes, and affected tests. Use when comparing branches or commits structurally. Does not execute repository code."""
|
|
971
|
+
if branch_comparison:
|
|
972
|
+
from codegraph.branch_comparison import compare_branch_architecture
|
|
973
|
+
with indexer.session() as con:
|
|
974
|
+
return compare_branch_architecture(indexer.repository, base_branch=base, head_branch=head, con=con).as_dict()
|
|
975
|
+
else:
|
|
976
|
+
from codegraph.structural_diff import compare_revisions
|
|
977
|
+
with indexer.session() as con:
|
|
978
|
+
return compare_revisions(indexer.repository, base=base, head=head, con=con).as_dict()
|
|
979
|
+
|
|
980
|
+
def get_change_impact(base: str = "HEAD~1", head: str = "HEAD", max_depth: int = 2, max_results: int = 50) -> dict[str, Any]:
|
|
981
|
+
"""Compute deep downstream change impact across callers, callees, framework routes, covering tests, mutating database queries, and monorepo packages. Use when analyzing blast radius and ripple effects of git commits. Does not assume unobserved runtime paths are impossible."""
|
|
982
|
+
from codegraph.change_impact import get_deep_change_impact
|
|
983
|
+
with indexer.session() as con:
|
|
984
|
+
return get_deep_change_impact(indexer.repository, con, base=base, head=head, max_depth=max_depth, max_results=max_results).as_dict()
|
|
985
|
+
|
|
986
|
+
def check_context_freshness(task: str = "", context_packet: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
987
|
+
"""Validate whether a previously compiled context packet or task context is still VALID, PARTIALLY_STALE, or STALE. Use when checking if cached context is valid after edits. Does not invalidate context when only unrelated files change."""
|
|
988
|
+
from codegraph.context_freshness import (
|
|
989
|
+
check_context_freshness as impl_check_context_freshness,
|
|
990
|
+
)
|
|
991
|
+
with indexer.session() as con:
|
|
992
|
+
target_packet = context_packet
|
|
993
|
+
if target_packet is None and task:
|
|
994
|
+
from codegraph.context import get_context as ctx_compile
|
|
995
|
+
target_packet = ctx_compile(con, indexer.repository, task, mode="FAST").model_dump()
|
|
996
|
+
if target_packet is None:
|
|
997
|
+
return {
|
|
998
|
+
"status": "UNKNOWN",
|
|
999
|
+
"reason": "Neither context_packet nor task prompt was provided.",
|
|
1000
|
+
"recommended_action": "RECOMPILE_CONTEXT",
|
|
1001
|
+
}
|
|
1002
|
+
return impl_check_context_freshness(target_packet, indexer.repository, con=con).as_dict()
|
|
1003
|
+
|
|
1004
|
+
def trace_symbol_history(symbol: str, path: str = "", max_commits: int = 15) -> dict[str, Any]:
|
|
1005
|
+
"""Trace deterministic symbol evolution across Git history: introduced, modified, moved across files, renamed, or deleted. Use when discovering when a symbol was added, moved, or renamed. Does not guess ambiguous renames without evidence."""
|
|
1006
|
+
from codegraph.symbol_history import trace_symbol_history as impl_trace_symbol_history
|
|
1007
|
+
with indexer.session() as con:
|
|
1008
|
+
return impl_trace_symbol_history(indexer.repository, symbol=symbol, path=path or None, con=con, max_commits=max_commits).as_dict()
|
|
1009
|
+
|
|
1010
|
+
def detect_semantic_conflicts(base_branch: str = "main", head_branch: str = "HEAD") -> dict[str, Any]:
|
|
1011
|
+
"""Detect semantic and contract conflicts between Git branches (calling deleted symbols, broken call signatures, or missing route handlers). Use when verifying whether branches can be safely merged without semantic breakage. Does not report false conflicts when branches are compatible."""
|
|
1012
|
+
from codegraph.semantic_conflicts import (
|
|
1013
|
+
detect_semantic_conflicts as impl_detect_semantic_conflicts,
|
|
1014
|
+
)
|
|
1015
|
+
with indexer.session() as con:
|
|
1016
|
+
return impl_detect_semantic_conflicts(indexer.repository, base_branch=base_branch, head_branch=head_branch, con=con).as_dict()
|
|
1017
|
+
|
|
959
1018
|
core_tools: dict[str, Any] = {
|
|
960
1019
|
"resolve_symbol": resolve_symbol,
|
|
961
1020
|
"search_symbols": search_symbols,
|
|
@@ -972,9 +1031,19 @@ def create_server(
|
|
|
972
1031
|
"get_git_impact": get_git_impact,
|
|
973
1032
|
}
|
|
974
1033
|
|
|
1034
|
+
git_intelligence_tools: dict[str, Any] = {
|
|
1035
|
+
"get_git_state": get_git_state,
|
|
1036
|
+
"compare_git": compare_git,
|
|
1037
|
+
"get_change_impact": get_change_impact,
|
|
1038
|
+
"check_context_freshness": check_context_freshness,
|
|
1039
|
+
"trace_symbol_history": trace_symbol_history,
|
|
1040
|
+
"detect_semantic_conflicts": detect_semantic_conflicts,
|
|
1041
|
+
}
|
|
1042
|
+
|
|
975
1043
|
# Map of all available tools
|
|
976
1044
|
all_tools: dict[str, Any] = {
|
|
977
1045
|
**core_tools,
|
|
1046
|
+
**git_intelligence_tools,
|
|
978
1047
|
"search_code": search_code,
|
|
979
1048
|
"read_file": read_file,
|
|
980
1049
|
"find_symbol": find_symbol,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Semantic and Contract Merge Conflict Engine.
|
|
2
|
+
|
|
3
|
+
Analyzes two branches or revisions to identify semantic/contractual merge conflicts
|
|
4
|
+
that standard Git text-based merge algorithms cannot detect:
|
|
5
|
+
- A symbol's signature/parameters changed in one branch, but the other branch calls the old signature.
|
|
6
|
+
- A symbol was deleted/renamed in one branch, but the other branch adds new references/calls to it.
|
|
7
|
+
- A route handler or DB model changed in one branch, breaking endpoints/queries in the other.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from codegraph.indexing.parser import parse
|
|
18
|
+
from codegraph.indexing.scanner import LANGUAGES
|
|
19
|
+
from codegraph.structural_diff import (
|
|
20
|
+
_get_file_content_at_ref,
|
|
21
|
+
_run_git,
|
|
22
|
+
compare_revisions,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SemanticConflictType(StrEnum):
|
|
27
|
+
CALL_SIGNATURE_MISMATCH = "CALL_SIGNATURE_MISMATCH"
|
|
28
|
+
DELETED_SYMBOL_REFERENCED = "DELETED_SYMBOL_REFERENCED"
|
|
29
|
+
ROUTE_HANDLER_MISMATCH = "ROUTE_HANDLER_MISMATCH"
|
|
30
|
+
UNRESOLVED_IMPORT = "UNRESOLVED_IMPORT"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SemanticConflictSeverity(StrEnum):
|
|
34
|
+
ERROR = "ERROR" # Definite runtime failure or missing symbol
|
|
35
|
+
WARNING = "WARNING" # Plausible contract drift or signature change
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SemanticConflict:
|
|
40
|
+
conflict_type: SemanticConflictType
|
|
41
|
+
symbol: str
|
|
42
|
+
base_file: str
|
|
43
|
+
head_file: str
|
|
44
|
+
severity: SemanticConflictSeverity
|
|
45
|
+
description: str
|
|
46
|
+
base_evidence: str
|
|
47
|
+
head_evidence: str
|
|
48
|
+
line: int | None = None
|
|
49
|
+
|
|
50
|
+
def as_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"conflict_type": self.conflict_type.value,
|
|
53
|
+
"symbol": self.symbol,
|
|
54
|
+
"base_file": self.base_file,
|
|
55
|
+
"head_file": self.head_file,
|
|
56
|
+
"severity": self.severity.value,
|
|
57
|
+
"description": self.description,
|
|
58
|
+
"base_evidence": self.base_evidence,
|
|
59
|
+
"head_evidence": self.head_evidence,
|
|
60
|
+
"line": self.line,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class SemanticConflictReport:
|
|
66
|
+
base_branch: str
|
|
67
|
+
head_branch: str
|
|
68
|
+
has_conflicts: bool
|
|
69
|
+
total_conflicts: int
|
|
70
|
+
conflicts: list[SemanticConflict] = field(default_factory=list)
|
|
71
|
+
summary: str = ""
|
|
72
|
+
|
|
73
|
+
def as_dict(self) -> dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"base_branch": self.base_branch,
|
|
76
|
+
"head_branch": self.head_branch,
|
|
77
|
+
"has_conflicts": self.has_conflicts,
|
|
78
|
+
"total_conflicts": self.total_conflicts,
|
|
79
|
+
"conflicts": [c.as_dict() for c in self.conflicts],
|
|
80
|
+
"summary": self.summary,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def detect_semantic_conflicts(
|
|
85
|
+
repository: Path,
|
|
86
|
+
base_branch: str = "main",
|
|
87
|
+
head_branch: str = "HEAD",
|
|
88
|
+
con: sqlite3.Connection | None = None,
|
|
89
|
+
) -> SemanticConflictReport:
|
|
90
|
+
"""Detect semantic and contract conflicts between two branches using 3-way merge-base analysis."""
|
|
91
|
+
# Fast path: identical branches have zero conflicts
|
|
92
|
+
if base_branch == head_branch:
|
|
93
|
+
return SemanticConflictReport(
|
|
94
|
+
base_branch=base_branch,
|
|
95
|
+
head_branch=head_branch,
|
|
96
|
+
has_conflicts=False,
|
|
97
|
+
total_conflicts=0,
|
|
98
|
+
summary=f"Branches {base_branch} and {head_branch} are identical; 0 semantic conflicts.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# 1. Resolve merge base
|
|
102
|
+
merge_base_raw = _run_git(repository, "merge-base", base_branch, head_branch)
|
|
103
|
+
merge_base = merge_base_raw.strip() if merge_base_raw else base_branch
|
|
104
|
+
|
|
105
|
+
# 2. What changed on base vs merge-base, and what changed on head vs merge-base
|
|
106
|
+
base_diff = compare_revisions(repository, base=merge_base, head=base_branch, con=con)
|
|
107
|
+
head_diff = compare_revisions(repository, base=merge_base, head=head_branch, con=con)
|
|
108
|
+
|
|
109
|
+
conflicts: list[SemanticConflict] = []
|
|
110
|
+
seen_conflict_keys: set[str] = set()
|
|
111
|
+
|
|
112
|
+
# Case A: Symbols removed or modified on base, but invoked on head
|
|
113
|
+
base_removed = {s.name: s for s in base_diff.removed_symbols}
|
|
114
|
+
base_modified = {s.name: s for s in base_diff.changed_symbols}
|
|
115
|
+
|
|
116
|
+
for m_path in (head_diff.modified_files + head_diff.added_files):
|
|
117
|
+
ext = Path(m_path).suffix.lower()
|
|
118
|
+
lang = LANGUAGES.get(ext)
|
|
119
|
+
if not lang:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
head_content = _get_file_content_at_ref(repository, head_branch, m_path)
|
|
123
|
+
if not head_content:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
parsed = parse(head_content, lang, m_path)
|
|
128
|
+
except Exception:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
for call in parsed.calls:
|
|
132
|
+
target = call.callee
|
|
133
|
+
if not target:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if target in base_removed:
|
|
137
|
+
rem_s = base_removed[target]
|
|
138
|
+
c_key = f"A_DEL:{target}:{m_path}:{call.line}"
|
|
139
|
+
if c_key not in seen_conflict_keys:
|
|
140
|
+
seen_conflict_keys.add(c_key)
|
|
141
|
+
conflicts.append(
|
|
142
|
+
SemanticConflict(
|
|
143
|
+
conflict_type=SemanticConflictType.DELETED_SYMBOL_REFERENCED,
|
|
144
|
+
symbol=target,
|
|
145
|
+
base_file=rem_s.path,
|
|
146
|
+
head_file=m_path,
|
|
147
|
+
severity=SemanticConflictSeverity.ERROR,
|
|
148
|
+
description=f"Branch '{head_branch}' invokes '{target}' at {m_path}:{call.line}, but symbol was removed in '{base_branch}' ({rem_s.path}).",
|
|
149
|
+
base_evidence=f"Symbol '{target}' removed in '{base_branch}'",
|
|
150
|
+
head_evidence=f"Call site {m_path}:{call.line} -> {target}",
|
|
151
|
+
line=call.line,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
elif target in base_modified:
|
|
155
|
+
mod_s = base_modified[target]
|
|
156
|
+
c_key = f"A_MOD:{target}:{m_path}:{call.line}"
|
|
157
|
+
if c_key not in seen_conflict_keys:
|
|
158
|
+
seen_conflict_keys.add(c_key)
|
|
159
|
+
conflicts.append(
|
|
160
|
+
SemanticConflict(
|
|
161
|
+
conflict_type=SemanticConflictType.CALL_SIGNATURE_MISMATCH,
|
|
162
|
+
symbol=target,
|
|
163
|
+
base_file=mod_s.path,
|
|
164
|
+
head_file=m_path,
|
|
165
|
+
severity=SemanticConflictSeverity.WARNING,
|
|
166
|
+
description=f"Branch '{head_branch}' invokes '{target}' at {m_path}:{call.line}, whose body or signature was modified in '{base_branch}' ({mod_s.path}).",
|
|
167
|
+
base_evidence=f"Symbol '{target}' modified in '{base_branch}'",
|
|
168
|
+
head_evidence=f"Call site at {m_path}:{call.line}",
|
|
169
|
+
line=call.line,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for imp in parsed.imports:
|
|
174
|
+
target_imp = imp.imported_name or imp.name
|
|
175
|
+
if target_imp and target_imp in base_removed:
|
|
176
|
+
rem_s = base_removed[target_imp]
|
|
177
|
+
c_key = f"A_IMP:{target_imp}:{m_path}:{imp.line}"
|
|
178
|
+
if c_key not in seen_conflict_keys:
|
|
179
|
+
seen_conflict_keys.add(c_key)
|
|
180
|
+
conflicts.append(
|
|
181
|
+
SemanticConflict(
|
|
182
|
+
conflict_type=SemanticConflictType.UNRESOLVED_IMPORT,
|
|
183
|
+
symbol=target_imp,
|
|
184
|
+
base_file=rem_s.path,
|
|
185
|
+
head_file=m_path,
|
|
186
|
+
severity=SemanticConflictSeverity.ERROR,
|
|
187
|
+
description=f"Branch '{head_branch}' imports '{target_imp}' at {m_path}:{imp.line}, which was removed in '{base_branch}' ({rem_s.path}).",
|
|
188
|
+
base_evidence=f"Symbol '{target_imp}' removed in '{base_branch}'",
|
|
189
|
+
head_evidence=f"Import statement at {m_path}:{imp.line}",
|
|
190
|
+
line=imp.line,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for r in parsed.routes:
|
|
195
|
+
handler = r.handler_name
|
|
196
|
+
if handler and handler in base_removed:
|
|
197
|
+
rem_s = base_removed[handler]
|
|
198
|
+
c_key = f"A_ROUTE:{handler}:{m_path}:{r.line}"
|
|
199
|
+
if c_key not in seen_conflict_keys:
|
|
200
|
+
seen_conflict_keys.add(c_key)
|
|
201
|
+
conflicts.append(
|
|
202
|
+
SemanticConflict(
|
|
203
|
+
conflict_type=SemanticConflictType.ROUTE_HANDLER_MISMATCH,
|
|
204
|
+
symbol=handler,
|
|
205
|
+
base_file=rem_s.path,
|
|
206
|
+
head_file=m_path,
|
|
207
|
+
severity=SemanticConflictSeverity.ERROR,
|
|
208
|
+
description=f"Branch '{head_branch}' registers route '{r.http_method} {r.route_path}' pointing to handler '{handler}', which was deleted in '{base_branch}'.",
|
|
209
|
+
base_evidence=f"Handler '{handler}' deleted in '{base_branch}'",
|
|
210
|
+
head_evidence=f"Route '{r.http_method} {r.route_path}' at {m_path}:{r.line}",
|
|
211
|
+
line=r.line,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Case B: Symbols removed or modified on head, but invoked on base
|
|
216
|
+
head_removed = {s.name: s for s in head_diff.removed_symbols}
|
|
217
|
+
for b_path in (base_diff.modified_files + base_diff.added_files):
|
|
218
|
+
ext = Path(b_path).suffix.lower()
|
|
219
|
+
lang = LANGUAGES.get(ext)
|
|
220
|
+
if not lang:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
base_content = _get_file_content_at_ref(repository, base_branch, b_path)
|
|
224
|
+
if not base_content:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
parsed = parse(base_content, lang, b_path)
|
|
229
|
+
except Exception:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
for call in parsed.calls:
|
|
233
|
+
target = call.callee
|
|
234
|
+
if not target:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if target in head_removed:
|
|
238
|
+
rem_s = head_removed[target]
|
|
239
|
+
c_key = f"B_DEL:{target}:{b_path}:{call.line}"
|
|
240
|
+
if c_key not in seen_conflict_keys:
|
|
241
|
+
seen_conflict_keys.add(c_key)
|
|
242
|
+
conflicts.append(
|
|
243
|
+
SemanticConflict(
|
|
244
|
+
conflict_type=SemanticConflictType.DELETED_SYMBOL_REFERENCED,
|
|
245
|
+
symbol=target,
|
|
246
|
+
base_file=b_path,
|
|
247
|
+
head_file=rem_s.path,
|
|
248
|
+
severity=SemanticConflictSeverity.ERROR,
|
|
249
|
+
description=f"Branch '{base_branch}' invokes '{target}' at {b_path}:{call.line}, but symbol was removed in '{head_branch}' ({rem_s.path}).",
|
|
250
|
+
base_evidence=f"Call site at {b_path}:{call.line}",
|
|
251
|
+
head_evidence=f"Symbol '{target}' removed in '{head_branch}'",
|
|
252
|
+
line=call.line,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
has_conflicts = len(conflicts) > 0
|
|
257
|
+
summary = (
|
|
258
|
+
f"Detected {len(conflicts)} semantic conflict(s) between {base_branch} and {head_branch}."
|
|
259
|
+
if has_conflicts
|
|
260
|
+
else f"Clean semantic comparison between {base_branch} and {head_branch} (0 semantic conflicts detected)."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return SemanticConflictReport(
|
|
264
|
+
base_branch=base_branch,
|
|
265
|
+
head_branch=head_branch,
|
|
266
|
+
has_conflicts=has_conflicts,
|
|
267
|
+
total_conflicts=len(conflicts),
|
|
268
|
+
conflicts=conflicts,
|
|
269
|
+
summary=summary,
|
|
270
|
+
)
|