codegraph-engine 2.2.1__py3-none-any.whl → 3.0.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 (82) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +564 -6
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/branch_comparison.py +197 -0
  20. codegraph/change_impact.py +547 -0
  21. codegraph/cli.py +992 -3
  22. codegraph/cli_output.py +17 -2
  23. codegraph/context.py +38 -11
  24. codegraph/context_freshness.py +351 -0
  25. codegraph/database/__init__.py +14 -0
  26. codegraph/database/extractor.py +712 -17
  27. codegraph/database/interrogation.py +388 -2
  28. codegraph/database/schema_drift.py +862 -0
  29. codegraph/epistemic.py +7 -0
  30. codegraph/evidence_contract.py +21 -0
  31. codegraph/frameworks.py +171 -6
  32. codegraph/frameworks_nestjs.py +502 -0
  33. codegraph/frameworks_nextjs.py +337 -0
  34. codegraph/frameworks_react.py +457 -0
  35. codegraph/freshness.py +2 -0
  36. codegraph/frontend_html.py +109 -0
  37. codegraph/git.py +2 -0
  38. codegraph/git_state.py +545 -0
  39. codegraph/indexing/__init__.py +7 -1
  40. codegraph/indexing/indexer.py +291 -38
  41. codegraph/indexing/models.py +6 -0
  42. codegraph/indexing/parser.py +239 -0
  43. codegraph/indexing/pool.py +136 -0
  44. codegraph/indexing/scanner.py +6 -0
  45. codegraph/indexing/templates.py +418 -0
  46. codegraph/interrogation.py +128 -20
  47. codegraph/mcp/server.py +258 -27
  48. codegraph/models.py +7 -0
  49. codegraph/monorepo.py +3 -0
  50. codegraph/optimizer.py +42 -5
  51. codegraph/process_lifecycle.py +31 -13
  52. codegraph/refactor/__init__.py +36 -0
  53. codegraph/refactor/models.py +114 -0
  54. codegraph/refactor/renamer.py +844 -0
  55. codegraph/refactor/transactions.py +403 -0
  56. codegraph/resolver.py +1202 -159
  57. codegraph/retrieval_policy.py +6 -0
  58. codegraph/route_topology.py +172 -0
  59. codegraph/runtime/__init__.py +19 -0
  60. codegraph/runtime/collector.py +155 -0
  61. codegraph/runtime/db_watcher.py +191 -0
  62. codegraph/runtime/ingestor.py +30 -10
  63. codegraph/runtime/port_inspector.py +304 -0
  64. codegraph/runtime/runner.py +155 -6
  65. codegraph/runtime/trace_linker.py +175 -0
  66. codegraph/search/hybrid.py +26 -0
  67. codegraph/semantic_conflicts.py +270 -0
  68. codegraph/semantic_decorators.py +34 -3
  69. codegraph/structural_diff.py +542 -0
  70. codegraph/styles.py +144 -0
  71. codegraph/symbol_history.py +277 -0
  72. codegraph/tsconfig.py +236 -0
  73. codegraph/ui/__init__.py +6 -0
  74. codegraph/ui/server.py +715 -0
  75. codegraph/watcher.py +323 -0
  76. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
  77. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  78. codegraph_engine-2.2.1.dist-info/RECORD +0 -87
  79. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  80. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  81. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  82. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
@@ -623,6 +623,11 @@ def search_code(
623
623
 
624
624
  config_exts = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"}
625
625
  filtered: list[dict[str, object]] = []
626
+ seen_file_lines: set[tuple[str, int]] = set()
627
+ files_with_exact_matches: set[str] = set()
628
+
629
+ query_terms = _safe_fts5_terms(clean_query)
630
+ query_terms_lower = [t.lower() for t in query_terms]
626
631
 
627
632
  for item in raw_hits:
628
633
  rel_path = item.path or item.file
@@ -659,6 +664,27 @@ def search_code(
659
664
  d["match_type"] = "case_insensitive"
660
665
  else:
661
666
  d["match_type"] = "token"
667
+
668
+ hit_line = item.line
669
+ file_line_key = (rel_path, hit_line)
670
+ if file_line_key in seen_file_lines:
671
+ continue
672
+
673
+ # If multi-word query and this hit is only a token match:
674
+ # verify that all query terms appear in the snippet or matched text
675
+ if len(query_terms_lower) >= 2 and d["match_type"] == "token":
676
+ haystack = (str(d.get("snippet", "")) + " " + str(d.get("matched_text", ""))).lower()
677
+ if not all(t in haystack for t in query_terms_lower):
678
+ continue
679
+
680
+ # If a file already had an exact/case-insensitive match, discard subsequent loose token matches
681
+ if rel_path in files_with_exact_matches and d["match_type"] == "token":
682
+ continue
683
+
684
+ if d["match_type"] in ("exact", "case_insensitive"):
685
+ files_with_exact_matches.add(rel_path)
686
+
687
+ seen_file_lines.add(file_line_key)
662
688
  filtered.append(d)
663
689
  if len(filtered) >= effective_limit:
664
690
  break
@@ -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
+ )
@@ -66,6 +66,31 @@ _RECOGNIZED_DECORATOR_SPECS: tuple[SemanticDecoratorSpec, ...] = (
66
66
  semantic_role="PROVIDES",
67
67
  patterns=("provide", "di.provide", "provider", "singleton", "di.singleton"),
68
68
  ),
69
+ SemanticDecoratorSpec(
70
+ framework="langchain",
71
+ semantic_role="TOOL_HANDLER",
72
+ patterns=("tool", "langchain.tools.tool", "langchain_core.tools.tool", "agents.tool"),
73
+ ),
74
+ SemanticDecoratorSpec(
75
+ framework="llama_index",
76
+ semantic_role="TOOL_HANDLER",
77
+ patterns=("function_tool", "llama_index.core.tools.function_tool"),
78
+ ),
79
+ SemanticDecoratorSpec(
80
+ framework="ray",
81
+ semantic_role="TASK_HANDLER",
82
+ patterns=("ray.remote", "remote"),
83
+ ),
84
+ SemanticDecoratorSpec(
85
+ framework="prefect",
86
+ semantic_role="TASK_HANDLER",
87
+ patterns=("prefect.task", "prefect.flow", "task", "flow"),
88
+ ),
89
+ SemanticDecoratorSpec(
90
+ framework="zenml",
91
+ semantic_role="TASK_HANDLER",
92
+ patterns=("step", "pipeline"),
93
+ ),
69
94
  )
70
95
 
71
96
 
@@ -132,12 +157,12 @@ def analyze_symbol_decorators(
132
157
  if "." in raw_expr:
133
158
  base = raw_expr.split(".")[0]
134
159
  # Verify base is imported or standard framework name
135
- if base in import_map or base in ("app", "celery", "click", "django", "typer", "di", "inject"):
160
+ if base in import_map or base in ("app", "celery", "click", "django", "typer", "di", "inject", "ray", "langchain", "llama_index", "prefect", "zenml"):
136
161
  grounded_name = raw_expr
137
162
  matched_spec = spec
138
163
  break
139
164
  else:
140
- # Bare name (e.g. shared_task, receiver, command, inject, provide, singleton)
165
+ # Bare name (e.g. shared_task, receiver, command, inject, provide, singleton, tool)
141
166
  # Verify it was imported from the framework or known pattern
142
167
  if raw_expr in import_map:
143
168
  imp_mod, imp_orig = import_map[raw_expr]
@@ -146,7 +171,7 @@ def analyze_symbol_decorators(
146
171
  grounded_name = f"{imp_mod}.{orig_or_raw}"
147
172
  matched_spec = spec
148
173
  break
149
- elif raw_expr in ("inject", "provide", "singleton"):
174
+ elif raw_expr in ("inject", "provide", "singleton", "receiver", "tool", "function_tool", "remote", "step", "flow"):
150
175
  grounded_name = raw_expr
151
176
  matched_spec = spec
152
177
  break
@@ -242,6 +267,12 @@ def analyze_symbol_decorators(
242
267
  dyn_reason = "dynamic_event_name"
243
268
  if event_names:
244
269
  metadata["events"] = event_names
270
+ for kw in kw_nodes:
271
+ if kw.arg == "sender":
272
+ if isinstance(kw.value, (ast.Name, ast.Attribute)) and hasattr(ast, "unparse"):
273
+ metadata["sender"] = ast.unparse(kw.value)
274
+ elif isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
275
+ metadata["sender"] = kw.value.value
245
276
 
246
277
  elif matched_spec.semantic_role == "COMMAND_HANDLER":
247
278
  # Extract command name