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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +564 -6
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +992 -3
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +545 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +258 -27
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.2.1.dist-info/RECORD +0 -87
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
codegraph/search/hybrid.py
CHANGED
|
@@ -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
|
+
)
|
codegraph/semantic_decorators.py
CHANGED
|
@@ -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
|