codegraph-engine 3.0.0__1-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 +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Semantic Decorator & Task/Command Framework Intelligence for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Recognizes explicit, deterministic decorator-driven registrations:
|
|
4
|
+
- Celery / task systems: @celery.task, @app.task, @shared_task -> TASK_HANDLER
|
|
5
|
+
- Click / CLI commands: @click.command, @click.group, @command -> COMMAND_HANDLER
|
|
6
|
+
- Django signals / event systems: @receiver, @event, @listener -> EVENT_LISTENER
|
|
7
|
+
- General registry decorators: @registry.register -> REGISTERS
|
|
8
|
+
|
|
9
|
+
Invariants:
|
|
10
|
+
- TASK_HANDLER != CALLS
|
|
11
|
+
- COMMAND_HANDLER != CALLS
|
|
12
|
+
- EVENT_LISTENER != CALLS
|
|
13
|
+
- REGISTERS != CALLS
|
|
14
|
+
- Unknown decorators remain ordinary AST metadata only.
|
|
15
|
+
- Dynamic arguments produce UNKNOWN/POSSIBLE findings without fake canonical IDs.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from codegraph.epistemic import RelationshipEvidenceClass
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from codegraph.binding_resolver import LocalBindingResolver
|
|
27
|
+
from codegraph.indexing.models import ImportRef
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SemanticDecoratorSpec:
|
|
32
|
+
framework: str
|
|
33
|
+
semantic_role: str # TASK_HANDLER | COMMAND_HANDLER | EVENT_LISTENER | REGISTERS
|
|
34
|
+
patterns: tuple[str, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Canonical registry of recognized semantic framework decorators
|
|
38
|
+
_RECOGNIZED_DECORATOR_SPECS: tuple[SemanticDecoratorSpec, ...] = (
|
|
39
|
+
SemanticDecoratorSpec(
|
|
40
|
+
framework="celery",
|
|
41
|
+
semantic_role="TASK_HANDLER",
|
|
42
|
+
patterns=("celery.task", "celery.shared_task", "shared_task", "app.task"),
|
|
43
|
+
),
|
|
44
|
+
SemanticDecoratorSpec(
|
|
45
|
+
framework="django",
|
|
46
|
+
semantic_role="EVENT_LISTENER",
|
|
47
|
+
patterns=("django.dispatch.receiver", "receiver"),
|
|
48
|
+
),
|
|
49
|
+
SemanticDecoratorSpec(
|
|
50
|
+
framework="click",
|
|
51
|
+
semantic_role="COMMAND_HANDLER",
|
|
52
|
+
patterns=("click.command", "click.group", "command", "group"),
|
|
53
|
+
),
|
|
54
|
+
SemanticDecoratorSpec(
|
|
55
|
+
framework="typer",
|
|
56
|
+
semantic_role="COMMAND_HANDLER",
|
|
57
|
+
patterns=("app.command",),
|
|
58
|
+
),
|
|
59
|
+
SemanticDecoratorSpec(
|
|
60
|
+
framework="di",
|
|
61
|
+
semantic_role="INJECTS",
|
|
62
|
+
patterns=("inject", "inject.inject", "di.inject"),
|
|
63
|
+
),
|
|
64
|
+
SemanticDecoratorSpec(
|
|
65
|
+
framework="di",
|
|
66
|
+
semantic_role="PROVIDES",
|
|
67
|
+
patterns=("provide", "di.provide", "provider", "singleton", "di.singleton"),
|
|
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
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class SemanticDecoratorDetection:
|
|
99
|
+
decorator_expr: str
|
|
100
|
+
target_name: str
|
|
101
|
+
target_canonical_id: str
|
|
102
|
+
file_path: str
|
|
103
|
+
line: int
|
|
104
|
+
semantic_role: str # TASK_HANDLER | COMMAND_HANDLER | EVENT_LISTENER | REGISTERS
|
|
105
|
+
framework: str
|
|
106
|
+
metadata: dict[str, object] = field(default_factory=dict)
|
|
107
|
+
is_conditional: bool = False
|
|
108
|
+
is_dynamic_arg: bool = False
|
|
109
|
+
reason: str | None = None
|
|
110
|
+
evidence_class: RelationshipEvidenceClass = RelationshipEvidenceClass.FRAMEWORK_VERIFIED
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def analyze_symbol_decorators(
|
|
114
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
|
|
115
|
+
file_path: str,
|
|
116
|
+
target_canonical_id: str,
|
|
117
|
+
imports: list[ImportRef],
|
|
118
|
+
binding_resolver: LocalBindingResolver | None = None,
|
|
119
|
+
conditional_depth: int = 0,
|
|
120
|
+
) -> list[SemanticDecoratorDetection]:
|
|
121
|
+
"""Analyze decorators on a function or class AST node and extract recognized semantic decorators."""
|
|
122
|
+
detections: list[SemanticDecoratorDetection] = []
|
|
123
|
+
is_cond = conditional_depth > 0
|
|
124
|
+
|
|
125
|
+
# Build local import map for quick grounded lookup
|
|
126
|
+
import_map: dict[str, tuple[str, str | None]] = {}
|
|
127
|
+
for imp in imports:
|
|
128
|
+
loc = imp.local_name or imp.alias or imp.name or ""
|
|
129
|
+
mod = imp.imported_module or imp.module
|
|
130
|
+
orig = imp.imported_name or imp.name
|
|
131
|
+
if loc:
|
|
132
|
+
import_map[loc] = (mod, orig)
|
|
133
|
+
|
|
134
|
+
for d in node.decorator_list:
|
|
135
|
+
args_nodes: list[ast.expr] = []
|
|
136
|
+
kw_nodes: list[ast.keyword] = []
|
|
137
|
+
if isinstance(d, ast.Call):
|
|
138
|
+
func_node = d.func
|
|
139
|
+
args_nodes = d.args
|
|
140
|
+
kw_nodes = d.keywords
|
|
141
|
+
else:
|
|
142
|
+
func_node = d
|
|
143
|
+
|
|
144
|
+
raw_expr = ast.unparse(func_node) if hasattr(ast, "unparse") else ""
|
|
145
|
+
if not raw_expr:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# 1. Ground the decorator identifier
|
|
149
|
+
grounded_name: str | None = None
|
|
150
|
+
matched_spec: SemanticDecoratorSpec | None = None
|
|
151
|
+
evidence_class = RelationshipEvidenceClass.FRAMEWORK_VERIFIED
|
|
152
|
+
|
|
153
|
+
# Check direct qualified pattern
|
|
154
|
+
for spec in _RECOGNIZED_DECORATOR_SPECS:
|
|
155
|
+
if raw_expr in spec.patterns:
|
|
156
|
+
# If qualified (e.g. celery.task, click.command, app.task)
|
|
157
|
+
if "." in raw_expr:
|
|
158
|
+
base = raw_expr.split(".")[0]
|
|
159
|
+
# Verify base is imported or standard framework name
|
|
160
|
+
if base in import_map or base in ("app", "celery", "click", "django", "typer", "di", "inject", "ray", "langchain", "llama_index", "prefect", "zenml"):
|
|
161
|
+
grounded_name = raw_expr
|
|
162
|
+
matched_spec = spec
|
|
163
|
+
break
|
|
164
|
+
else:
|
|
165
|
+
# Bare name (e.g. shared_task, receiver, command, inject, provide, singleton, tool)
|
|
166
|
+
# Verify it was imported from the framework or known pattern
|
|
167
|
+
if raw_expr in import_map:
|
|
168
|
+
imp_mod, imp_orig = import_map[raw_expr]
|
|
169
|
+
orig_or_raw = imp_orig or raw_expr
|
|
170
|
+
if spec.framework in imp_mod.lower() or orig_or_raw in spec.patterns:
|
|
171
|
+
grounded_name = f"{imp_mod}.{orig_or_raw}"
|
|
172
|
+
matched_spec = spec
|
|
173
|
+
break
|
|
174
|
+
elif raw_expr in ("inject", "provide", "singleton", "receiver", "tool", "function_tool", "remote", "step", "flow"):
|
|
175
|
+
grounded_name = raw_expr
|
|
176
|
+
matched_spec = spec
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
# Check local alias via LocalBindingResolver (e.g. task = celery.task; @task)
|
|
180
|
+
if not matched_spec and binding_resolver and "." not in raw_expr:
|
|
181
|
+
alias_target, alias_ev, _rsn = binding_resolver.resolve(
|
|
182
|
+
file_path=file_path,
|
|
183
|
+
name=raw_expr,
|
|
184
|
+
use_line=node.lineno,
|
|
185
|
+
)
|
|
186
|
+
if alias_target and alias_ev == RelationshipEvidenceClass.DATAFLOW_VERIFIED:
|
|
187
|
+
for spec in _RECOGNIZED_DECORATOR_SPECS:
|
|
188
|
+
for pat in spec.patterns:
|
|
189
|
+
if pat in alias_target or alias_target.endswith(pat):
|
|
190
|
+
grounded_name = alias_target
|
|
191
|
+
matched_spec = spec
|
|
192
|
+
evidence_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
193
|
+
break
|
|
194
|
+
if matched_spec:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# Check general registry decorator: @registry.register or @command_registry.register
|
|
198
|
+
if not matched_spec and "." in raw_expr:
|
|
199
|
+
base_part, attr_part = raw_expr.rsplit(".", 1)
|
|
200
|
+
if attr_part == "register":
|
|
201
|
+
base_lower = base_part.lower()
|
|
202
|
+
if (
|
|
203
|
+
base_lower == "registry"
|
|
204
|
+
or base_lower.endswith("_registry")
|
|
205
|
+
or base_lower.endswith("registry")
|
|
206
|
+
):
|
|
207
|
+
grounded_name = raw_expr
|
|
208
|
+
matched_spec = SemanticDecoratorSpec(
|
|
209
|
+
framework="registry",
|
|
210
|
+
semantic_role="REGISTERS",
|
|
211
|
+
patterns=(raw_expr,),
|
|
212
|
+
)
|
|
213
|
+
evidence_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
214
|
+
|
|
215
|
+
# If not recognized, ignore: leave as ordinary AST metadata only
|
|
216
|
+
if not matched_spec or not grounded_name:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# 2. Extract static arguments and detect dynamic arguments
|
|
220
|
+
metadata: dict[str, object] = {}
|
|
221
|
+
is_dynamic_arg = False
|
|
222
|
+
dyn_reason: str | None = None
|
|
223
|
+
|
|
224
|
+
if isinstance(func_node, ast.Call):
|
|
225
|
+
is_dynamic_arg = True
|
|
226
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
227
|
+
|
|
228
|
+
if matched_spec.semantic_role == "EVENT_LISTENER":
|
|
229
|
+
# Extract events from args (e.g. @receiver(UserCreated) or @receiver([E1, E2]))
|
|
230
|
+
event_names: list[str] = []
|
|
231
|
+
for arg in args_nodes:
|
|
232
|
+
if isinstance(arg, ast.Name):
|
|
233
|
+
event_names.append(arg.id)
|
|
234
|
+
elif isinstance(arg, ast.Attribute) and hasattr(ast, "unparse"):
|
|
235
|
+
base_expr = ast.unparse(arg.value) if hasattr(ast, "unparse") else ""
|
|
236
|
+
if base_expr.lower() in ("config", "settings", "setting", "env", "environ", "os"):
|
|
237
|
+
is_dynamic_arg = True
|
|
238
|
+
dyn_reason = "dynamic_event_name"
|
|
239
|
+
else:
|
|
240
|
+
event_names.append(ast.unparse(arg))
|
|
241
|
+
elif isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
242
|
+
event_names.append(arg.value)
|
|
243
|
+
elif isinstance(arg, ast.Call):
|
|
244
|
+
is_dynamic_arg = True
|
|
245
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
246
|
+
elif isinstance(arg, (ast.List, ast.Tuple)):
|
|
247
|
+
for elt in arg.elts:
|
|
248
|
+
if isinstance(elt, ast.Name):
|
|
249
|
+
event_names.append(elt.id)
|
|
250
|
+
elif isinstance(elt, ast.Attribute) and hasattr(ast, "unparse"):
|
|
251
|
+
base_expr = ast.unparse(elt.value) if hasattr(ast, "unparse") else ""
|
|
252
|
+
if base_expr.lower() in ("config", "settings", "setting", "env", "environ", "os"):
|
|
253
|
+
is_dynamic_arg = True
|
|
254
|
+
dyn_reason = "dynamic_event_name"
|
|
255
|
+
else:
|
|
256
|
+
event_names.append(ast.unparse(elt))
|
|
257
|
+
elif isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
258
|
+
event_names.append(elt.value)
|
|
259
|
+
elif isinstance(elt, ast.Call):
|
|
260
|
+
is_dynamic_arg = True
|
|
261
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
262
|
+
else:
|
|
263
|
+
is_dynamic_arg = True
|
|
264
|
+
dyn_reason = "dynamic_event_name"
|
|
265
|
+
else:
|
|
266
|
+
is_dynamic_arg = True
|
|
267
|
+
dyn_reason = "dynamic_event_name"
|
|
268
|
+
if event_names:
|
|
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
|
|
276
|
+
|
|
277
|
+
elif matched_spec.semantic_role == "COMMAND_HANDLER":
|
|
278
|
+
# Extract command name
|
|
279
|
+
cmd_name: str | None = None
|
|
280
|
+
if args_nodes:
|
|
281
|
+
if isinstance(args_nodes[0], ast.Constant) and isinstance(args_nodes[0].value, str):
|
|
282
|
+
cmd_name = args_nodes[0].value
|
|
283
|
+
elif isinstance(args_nodes[0], ast.Call):
|
|
284
|
+
is_dynamic_arg = True
|
|
285
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
286
|
+
else:
|
|
287
|
+
is_dynamic_arg = True
|
|
288
|
+
dyn_reason = "dynamic_command_name"
|
|
289
|
+
for kw in kw_nodes:
|
|
290
|
+
if kw.arg == "name":
|
|
291
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
292
|
+
cmd_name = kw.value.value
|
|
293
|
+
elif isinstance(kw.value, ast.Call):
|
|
294
|
+
is_dynamic_arg = True
|
|
295
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
296
|
+
else:
|
|
297
|
+
is_dynamic_arg = True
|
|
298
|
+
dyn_reason = "dynamic_command_name"
|
|
299
|
+
if cmd_name:
|
|
300
|
+
metadata["command"] = cmd_name
|
|
301
|
+
|
|
302
|
+
elif matched_spec.semantic_role == "TASK_HANDLER":
|
|
303
|
+
# Extract task name
|
|
304
|
+
task_name: str | None = None
|
|
305
|
+
for kw in kw_nodes:
|
|
306
|
+
if kw.arg == "name":
|
|
307
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
308
|
+
task_name = kw.value.value
|
|
309
|
+
elif isinstance(kw.value, ast.Call):
|
|
310
|
+
is_dynamic_arg = True
|
|
311
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
312
|
+
else:
|
|
313
|
+
is_dynamic_arg = True
|
|
314
|
+
dyn_reason = "dynamic_task_name"
|
|
315
|
+
if task_name:
|
|
316
|
+
metadata["task_name"] = task_name
|
|
317
|
+
|
|
318
|
+
elif matched_spec.semantic_role == "REGISTERS":
|
|
319
|
+
# Extract registry key if provided: @registry.register("key")
|
|
320
|
+
if args_nodes:
|
|
321
|
+
if isinstance(args_nodes[0], ast.Constant) and isinstance(args_nodes[0].value, str):
|
|
322
|
+
metadata["registry_key"] = args_nodes[0].value
|
|
323
|
+
elif isinstance(args_nodes[0], ast.Call):
|
|
324
|
+
is_dynamic_arg = True
|
|
325
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
326
|
+
else:
|
|
327
|
+
is_dynamic_arg = True
|
|
328
|
+
dyn_reason = "dynamic_registry_key"
|
|
329
|
+
|
|
330
|
+
elif matched_spec.semantic_role == "PROVIDES":
|
|
331
|
+
iface_name: str | None = None
|
|
332
|
+
if args_nodes:
|
|
333
|
+
if isinstance(args_nodes[0], (ast.Name, ast.Attribute)) and hasattr(ast, "unparse"):
|
|
334
|
+
iface_name = ast.unparse(args_nodes[0])
|
|
335
|
+
elif isinstance(args_nodes[0], ast.Call):
|
|
336
|
+
is_dynamic_arg = True
|
|
337
|
+
dyn_reason = "unsupported_decorator_factory"
|
|
338
|
+
else:
|
|
339
|
+
is_dynamic_arg = True
|
|
340
|
+
dyn_reason = "dynamic_provider"
|
|
341
|
+
elif isinstance(node, ast.ClassDef) and node.bases and hasattr(ast, "unparse"):
|
|
342
|
+
base0 = node.bases[0]
|
|
343
|
+
if isinstance(base0, (ast.Name, ast.Attribute)):
|
|
344
|
+
iface_name = ast.unparse(base0)
|
|
345
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.returns and hasattr(ast, "unparse"):
|
|
346
|
+
if isinstance(node.returns, (ast.Name, ast.Attribute)):
|
|
347
|
+
iface_name = ast.unparse(node.returns)
|
|
348
|
+
if not iface_name and not is_dynamic_arg:
|
|
349
|
+
iface_name = node.name
|
|
350
|
+
if iface_name:
|
|
351
|
+
metadata["interface"] = iface_name
|
|
352
|
+
|
|
353
|
+
metadata["evidence_class"] = evidence_class.value
|
|
354
|
+
|
|
355
|
+
detections.append(
|
|
356
|
+
SemanticDecoratorDetection(
|
|
357
|
+
decorator_expr=raw_expr,
|
|
358
|
+
target_name=node.name,
|
|
359
|
+
target_canonical_id=target_canonical_id,
|
|
360
|
+
file_path=file_path,
|
|
361
|
+
line=node.lineno,
|
|
362
|
+
semantic_role=matched_spec.semantic_role,
|
|
363
|
+
framework=matched_spec.framework,
|
|
364
|
+
metadata=metadata,
|
|
365
|
+
is_conditional=is_cond,
|
|
366
|
+
is_dynamic_arg=is_dynamic_arg,
|
|
367
|
+
reason=dyn_reason,
|
|
368
|
+
evidence_class=evidence_class,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return detections
|