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.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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