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,371 @@
1
+ """Repository architecture intelligence with structured node/edge graph projection.
2
+
3
+ Returns a structured view of the repository: entry points, endpoints, controllers,
4
+ services, models, APIs, tests, config, and framework integration edges.
5
+
6
+ All results are source-derived — no fictional architecture is generated.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sqlite3
12
+ from pathlib import Path
13
+
14
+ _ENTRY_PATTERNS = re.compile(
15
+ r"(^|[_/])(main|app|server|index|wsgi|asgi|manage|run)\.(py|js|ts)$",
16
+ re.IGNORECASE,
17
+ )
18
+ _TEST_PATTERNS = re.compile(
19
+ r"(^|[_/])test[_s]?[_/]|test[_s]?\.(py|js|ts)$|spec\.(py|js|ts)$",
20
+ re.IGNORECASE,
21
+ )
22
+ _MODEL_PATTERNS = re.compile(
23
+ r"(^|[_/])(model|schema|entity|orm|db)\.(py|js|ts)$|models?(\.py|\.ts|\.js|/)$",
24
+ re.IGNORECASE,
25
+ )
26
+ _CONFIG_PATTERNS = re.compile(
27
+ r"(^|[_/])(config|settings|conf|env)\.(py|js|ts)$",
28
+ re.IGNORECASE,
29
+ )
30
+ _API_PATTERNS = re.compile(
31
+ r"(^|[_/])(api|routes?|views?|controllers?|handlers?)\.(py|js|ts)$|/api/",
32
+ re.IGNORECASE,
33
+ )
34
+ _REPO_PATTERNS = re.compile(
35
+ r"(^|[_/])(repo|repository|store|dao)\.(py|js|ts)$",
36
+ re.IGNORECASE,
37
+ )
38
+ _SERVICE_PATTERNS = re.compile(
39
+ r"(^|[_/])(service|svc|manager|provider)\.(py|js|ts)$",
40
+ re.IGNORECASE,
41
+ )
42
+ _INTEGRATION_PATTERNS = re.compile(
43
+ r"(database|redis|celery|kafka|rabbitmq|s3|stripe|twilio|sendgrid|oauth|jwt|openai)",
44
+ re.IGNORECASE,
45
+ )
46
+
47
+ _FRAMEWORK_IMPORT_PATTERNS: dict[str, re.Pattern[str]] = {
48
+ "flask": re.compile(r"^flask(?:\.|$)", re.IGNORECASE),
49
+ "fastapi": re.compile(r"^fastapi(?:\.|$)", re.IGNORECASE),
50
+ "django": re.compile(r"^django(?:\.|$)", re.IGNORECASE),
51
+ "express": re.compile(r"^express(?:\/|$)", re.IGNORECASE),
52
+ "nextjs": re.compile(r"^next(?:\/|$)", re.IGNORECASE),
53
+ "tornado": re.compile(r"^tornado(?:\.|$)", re.IGNORECASE),
54
+ "aiohttp": re.compile(r"^aiohttp(?:\.|$)", re.IGNORECASE),
55
+ "starlette": re.compile(r"^starlette(?:\.|$)", re.IGNORECASE),
56
+ }
57
+
58
+ _MANIFEST_NAMES = (
59
+ "pyproject.toml",
60
+ "requirements.txt",
61
+ "package.json",
62
+ "setup.py",
63
+ "setup.cfg",
64
+ "Pipfile",
65
+ )
66
+
67
+
68
+ def detect_frameworks(
69
+ con: sqlite3.Connection,
70
+ repository: Path,
71
+ ) -> list[str]:
72
+ """Detect application frameworks using routes, AST imports, and bounded manifest discovery."""
73
+ detected: set[str] = set()
74
+
75
+ # 1. Concrete routes registered in database
76
+ try:
77
+ fw_rows = con.execute("SELECT DISTINCT framework FROM framework_routes").fetchall()
78
+ for r in fw_rows:
79
+ if r[0]:
80
+ detected.add(str(r[0]).lower())
81
+ except sqlite3.OperationalError:
82
+ pass
83
+
84
+ # 2. AST imports from indexed source files
85
+ try:
86
+ imp_rows = con.execute("SELECT DISTINCT module FROM imports WHERE module != ''").fetchall()
87
+ for r in imp_rows:
88
+ mod = str(r[0]).strip()
89
+ for fw, pat in _FRAMEWORK_IMPORT_PATTERNS.items():
90
+ if pat.search(mod):
91
+ detected.add(fw)
92
+ except sqlite3.OperationalError:
93
+ pass
94
+
95
+ # 3. Bounded recursive manifest discovery (depth <= 3)
96
+ try:
97
+ root = repository.resolve()
98
+ dirs_to_check: list[Path] = [root]
99
+
100
+ def _subdirs(d: Path) -> list[Path]:
101
+ res: list[Path] = []
102
+ try:
103
+ for entry in d.iterdir():
104
+ if entry.is_dir() and not entry.name.startswith(".") and entry.name != "node_modules":
105
+ res.append(entry)
106
+ except OSError:
107
+ pass
108
+ return res
109
+
110
+ level1 = _subdirs(root)
111
+ dirs_to_check.extend(level1)
112
+ for d1 in level1:
113
+ level2 = _subdirs(d1)
114
+ dirs_to_check.extend(level2)
115
+
116
+ for d in dirs_to_check:
117
+ try:
118
+ for item in d.iterdir():
119
+ if not item.is_file():
120
+ continue
121
+ nm = item.name.lower()
122
+ if nm in _MANIFEST_NAMES or (nm.startswith("requirements") and nm.endswith(".txt")):
123
+ text = item.read_text(encoding="utf-8", errors="replace").lower()
124
+ for fw in ("flask", "fastapi", "django", "express", "tornado", "aiohttp", "starlette"):
125
+ if re.search(rf"(?:^|[^\w-]){fw}(?:[^\w-]|$)", text):
126
+ detected.add(fw)
127
+ if "next" in text and ("\"next\"" in text or "'next'" in text or "nextjs" in text):
128
+ detected.add("nextjs")
129
+ except OSError:
130
+ pass
131
+ except Exception:
132
+ pass
133
+
134
+ return sorted(detected)
135
+
136
+
137
+ def get_architecture(
138
+ con: sqlite3.Connection,
139
+ repository: Path,
140
+ max_per_category: int = 20,
141
+ ) -> dict[str, object]:
142
+ """Return a structured, source-derived architecture overview with nodes and edges."""
143
+ files: list[str] = [
144
+ r[0] for r in con.execute("SELECT path FROM files WHERE status='ok' ORDER BY path LIMIT 500")
145
+ ]
146
+
147
+ entry_points = [f for f in files if _ENTRY_PATTERNS.search(f)]
148
+ test_files = [f for f in files if _TEST_PATTERNS.search(f)]
149
+ model_files = [f for f in files if _MODEL_PATTERNS.search(f)]
150
+ config_files = [f for f in files if _CONFIG_PATTERNS.search(f)]
151
+ api_files = [f for f in files if _API_PATTERNS.search(f)]
152
+ repo_files = [f for f in files if _REPO_PATTERNS.search(f)]
153
+ service_files = [f for f in files if _SERVICE_PATTERNS.search(f)]
154
+
155
+ # Integration hints from imports
156
+ integrations: list[str] = []
157
+ try:
158
+ imp_rows = con.execute("SELECT DISTINCT module FROM imports LIMIT 500").fetchall()
159
+ for row in imp_rows:
160
+ m = _INTEGRATION_PATTERNS.search(row[0])
161
+ if m:
162
+ integrations.append(m.group(0).lower())
163
+ except sqlite3.OperationalError:
164
+ pass
165
+
166
+ # Top-level classes
167
+ class_rows = con.execute(
168
+ "SELECT canonical_id, qualified_name, path, kind FROM symbols WHERE kind='class' LIMIT 100"
169
+ ).fetchall()
170
+ classes = [
171
+ {"name": r["qualified_name"], "canonical_id": r["canonical_id"], "file": r["path"]}
172
+ for r in class_rows
173
+ ]
174
+
175
+ # Concrete framework routes
176
+ endpoints: list[dict[str, object]] = []
177
+ nodes: list[dict[str, object]] = []
178
+ edges: list[dict[str, object]] = []
179
+
180
+ try:
181
+ route_rows = con.execute(
182
+ "SELECT endpoint_id, framework, http_method, route_path, normalized_route, "
183
+ "handler_name, handler_canonical_id, file_path, line, evidence, confidence "
184
+ "FROM framework_routes LIMIT 100"
185
+ ).fetchall()
186
+ for rr in route_rows:
187
+ ep = {
188
+ "endpoint_id": rr["endpoint_id"],
189
+ "framework": rr["framework"],
190
+ "http_method": rr["http_method"],
191
+ "route_path": rr["route_path"],
192
+ "normalized_route": rr["normalized_route"],
193
+ "handler_name": rr["handler_name"],
194
+ "handler_canonical_id": rr["handler_canonical_id"],
195
+ "file": rr["file_path"],
196
+ "line": rr["line"],
197
+ "evidence": rr["evidence"],
198
+ "confidence": rr["confidence"],
199
+ }
200
+ endpoints.append(ep)
201
+ nodes.append(
202
+ {
203
+ "id": rr["endpoint_id"],
204
+ "name": f"{rr['http_method']} {rr['normalized_route']}",
205
+ "layer": "endpoint",
206
+ "file": rr["file_path"],
207
+ "line": rr["line"],
208
+ "confidence": rr["confidence"],
209
+ }
210
+ )
211
+ if rr["handler_canonical_id"]:
212
+ edges.append(
213
+ {
214
+ "source": rr["endpoint_id"],
215
+ "target": rr["handler_canonical_id"],
216
+ "relationship": "HANDLED_BY",
217
+ "confidence": rr["confidence"],
218
+ "evidence": rr["evidence"],
219
+ }
220
+ )
221
+ except sqlite3.OperationalError:
222
+ pass
223
+
224
+ # Frameworks
225
+ frameworks = detect_frameworks(con, repository)
226
+
227
+ from codegraph.monorepo import detect_workspace
228
+ workspace_info = detect_workspace(repository, con=con)
229
+
230
+ # Top-level modules (excluding build artifacts, vendors, and bundles, enriched with package boundaries)
231
+ modules: list[str] = []
232
+ try:
233
+ mod_rows = con.execute(
234
+ """
235
+ SELECT DISTINCT s.module
236
+ FROM symbols s
237
+ LEFT JOIN files f ON s.path = f.path
238
+ WHERE s.module != ''
239
+ AND (f.category IS NULL OR f.category NOT IN ('BUILD_ARTIFACT', 'VENDOR', 'BUNDLE', 'MINIFIED'))
240
+ ORDER BY s.module LIMIT 20
241
+ """
242
+ ).fetchall()
243
+ modules = [r[0] for r in mod_rows]
244
+ if workspace_info.packages:
245
+ for p in workspace_info.packages.values():
246
+ if p.root_path and p.root_path not in modules:
247
+ modules.append(p.root_path)
248
+ modules = sorted(modules)[:max_per_category]
249
+ except sqlite3.OperationalError:
250
+ pass
251
+
252
+ # Dependencies summary
253
+ dependencies: list[dict[str, object]] = []
254
+ try:
255
+ dep_rows = con.execute(
256
+ "SELECT module, count(*) AS c FROM imports GROUP BY module ORDER BY c DESC LIMIT 20"
257
+ ).fetchall()
258
+ dependencies = [{"module": r[0], "count": r[1]} for r in dep_rows]
259
+ except sqlite3.OperationalError:
260
+ pass
261
+
262
+ # Artifact directory separation
263
+ import posixpath
264
+ source_dirs: set[str] = set()
265
+ test_dirs: set[str] = set()
266
+ build_dirs: set[str] = set()
267
+ vendor_dirs: set[str] = set()
268
+ generated_dirs: set[str] = set()
269
+ generated_files: list[str] = []
270
+
271
+ try:
272
+ file_rows = con.execute("SELECT path, category FROM files WHERE status='ok'").fetchall()
273
+ for fr in file_rows:
274
+ p_dir = posixpath.dirname(fr["path"]) or "."
275
+ cat = str(fr["category"])
276
+ if cat in ("SOURCE", ""):
277
+ source_dirs.add(p_dir)
278
+ elif cat == "TEST":
279
+ test_dirs.add(p_dir)
280
+ elif cat in ("BUILD_ARTIFACT", "BUNDLE", "MINIFIED"):
281
+ build_dirs.add(p_dir)
282
+ elif cat == "VENDOR":
283
+ vendor_dirs.add(p_dir)
284
+ elif cat == "GENERATED":
285
+ generated_dirs.add(p_dir)
286
+ generated_files.append(fr["path"])
287
+ except sqlite3.OperationalError:
288
+ pass
289
+
290
+ # Test summary with test framework detection
291
+ from codegraph.indexing.test_framework import detect_test_framework
292
+ test_framework, tests_present, test_evidence = detect_test_framework(con, repository)
293
+ test_summary = {
294
+ "framework": test_framework.value,
295
+ "tests_present": tests_present,
296
+ "evidence": test_evidence,
297
+ "test_files_count": len(test_files),
298
+ "test_files": test_files[:max_per_category],
299
+ }
300
+
301
+ # Route summary
302
+ route_summary = {
303
+ "total_routes": len(endpoints),
304
+ "frameworks": frameworks,
305
+ "methods": {
306
+ m: len([e for e in endpoints if e.get("http_method") == m])
307
+ for m in sorted({str(e.get("http_method", "")) for e in endpoints if e.get("http_method")})
308
+ },
309
+ }
310
+
311
+ # Data / model summary
312
+ data_model_summary = {
313
+ "model_files": model_files[:max_per_category],
314
+ "model_classes": [c["name"] for c in classes[:max_per_category]],
315
+ "total_classes": len(classes),
316
+ }
317
+
318
+ # Module count by language
319
+ lang_counts: dict[str, int] = {}
320
+ for r in con.execute("SELECT language, count(*) AS n FROM files GROUP BY language"):
321
+ lang_counts[r["language"]] = r["n"]
322
+
323
+ total_symbols = con.execute("SELECT count(*) FROM symbols").fetchone()[0]
324
+ total_files = len(files)
325
+
326
+ return {
327
+ "source": "deterministic — parser-extracted from repository index",
328
+ "repository": str(repository),
329
+ "summary": {
330
+ "total_files": total_files,
331
+ "total_symbols": total_symbols,
332
+ "total_endpoints": len(endpoints),
333
+ "languages": lang_counts,
334
+ },
335
+ "languages": lang_counts,
336
+ "frameworks": frameworks,
337
+ "workspace": workspace_info.as_dict(),
338
+ "packages": [p.as_dict() for p in workspace_info.packages.values()],
339
+ "package_dependencies": [d.as_dict() for d in workspace_info.dependencies],
340
+ "entry_points": entry_points[:max_per_category],
341
+ "top_level_modules": modules,
342
+ "dependency_summary": dependencies,
343
+ "route_summary": route_summary,
344
+ "test_summary": test_summary,
345
+ "data_model_summary": data_model_summary,
346
+ "external_services": sorted(set(integrations))[:max_per_category],
347
+ "source_directories": sorted(source_dirs)[:max_per_category],
348
+ "test_directories": sorted(test_dirs)[:max_per_category],
349
+ "build_directories": sorted(build_dirs)[:max_per_category],
350
+ "vendor_directories": sorted(vendor_dirs)[:max_per_category],
351
+ "generated_directories": sorted(generated_dirs)[:max_per_category],
352
+ "generated_artifact_summary": {
353
+ "count": len(generated_files),
354
+ "files": generated_files,
355
+ },
356
+ "endpoints": endpoints[:max_per_category],
357
+ "services": service_files[:max_per_category],
358
+ "repositories": repo_files[:max_per_category],
359
+ "models": model_files[:max_per_category],
360
+ "apis": api_files[:max_per_category],
361
+ "tests": test_files[:max_per_category],
362
+ "config": config_files[:max_per_category],
363
+ "classes": classes[:max_per_category],
364
+ "external_integrations": sorted(set(integrations))[:max_per_category],
365
+ "architecture_nodes": nodes[:max_per_category * 2],
366
+ "architecture_edges": edges[:max_per_category * 2],
367
+ "note": (
368
+ "Architecture is inferred from source declarations, routes, and import patterns. "
369
+ "Framework endpoints are verified from AST route registrations."
370
+ ),
371
+ }
@@ -0,0 +1,50 @@
1
+ """Asynchronous message queue and cross-service distributed task intelligence.
2
+
3
+ Provides static detection, queue linking, and OpenTelemetry distributed tracing
4
+ reconciliation for Kafka, Celery, AWS SQS, RabbitMQ, Redis Pub/Sub, and Streams.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from codegraph.async_queue.models import (
9
+ AsyncConsumerRecord,
10
+ AsyncMessagingSystem,
11
+ AsyncProducerRecord,
12
+ AsyncQueueEntity,
13
+ AsyncQueueEvidenceClass,
14
+ AsyncQueueLink,
15
+ )
16
+ from codegraph.async_queue.otel_models import (
17
+ OTelEvidenceStatus,
18
+ OTelSpanRecord,
19
+ TraceReconciliationResult,
20
+ )
21
+ from codegraph.async_queue.otel_parser import parse_otel_spans
22
+ from codegraph.async_queue.reconciler import OTelTraceReconciler, ensure_otel_tables
23
+ from codegraph.async_queue.scanner import AsyncQueueScanner, ensure_async_tables
24
+ from codegraph.async_queue.traversal import (
25
+ find_queue_consumers,
26
+ find_queue_producers,
27
+ list_async_queues,
28
+ trace_async_flow,
29
+ )
30
+
31
+ __all__ = [
32
+ "AsyncConsumerRecord",
33
+ "AsyncMessagingSystem",
34
+ "AsyncProducerRecord",
35
+ "AsyncQueueEntity",
36
+ "AsyncQueueEvidenceClass",
37
+ "AsyncQueueLink",
38
+ "AsyncQueueScanner",
39
+ "OTelEvidenceStatus",
40
+ "OTelSpanRecord",
41
+ "OTelTraceReconciler",
42
+ "TraceReconciliationResult",
43
+ "ensure_async_tables",
44
+ "ensure_otel_tables",
45
+ "find_queue_consumers",
46
+ "find_queue_producers",
47
+ "list_async_queues",
48
+ "parse_otel_spans",
49
+ "trace_async_flow",
50
+ ]
@@ -0,0 +1,48 @@
1
+ """Detectors for asynchronous message brokers and task dispatchers."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+
6
+ from codegraph.async_queue.detectors.celery import detect_celery
7
+ from codegraph.async_queue.detectors.kafka import detect_kafka
8
+ from codegraph.async_queue.detectors.rabbitmq import detect_rabbitmq
9
+ from codegraph.async_queue.detectors.redis import detect_redis
10
+ from codegraph.async_queue.detectors.sqs import detect_sqs
11
+ from codegraph.async_queue.models import AsyncConsumerRecord, AsyncProducerRecord
12
+
13
+ ALL_DETECTORS = [
14
+ detect_celery,
15
+ detect_kafka,
16
+ detect_sqs,
17
+ detect_rabbitmq,
18
+ detect_redis,
19
+ ]
20
+
21
+
22
+ def run_all_detectors(
23
+ tree: ast.AST,
24
+ content: str,
25
+ file_path: str,
26
+ module_name: str,
27
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
28
+ """Run all framework detectors on parsed Python AST."""
29
+ all_producers: list[AsyncProducerRecord] = []
30
+ all_consumers: list[AsyncConsumerRecord] = []
31
+
32
+ for detector in ALL_DETECTORS:
33
+ prods, cons = detector(tree, content, file_path, module_name)
34
+ all_producers.extend(prods)
35
+ all_consumers.extend(cons)
36
+
37
+ return all_producers, all_consumers
38
+
39
+
40
+ __all__ = [
41
+ "ALL_DETECTORS",
42
+ "detect_celery",
43
+ "detect_kafka",
44
+ "detect_rabbitmq",
45
+ "detect_redis",
46
+ "detect_sqs",
47
+ "run_all_detectors",
48
+ ]
@@ -0,0 +1,160 @@
1
+ """Celery asynchronous task and canvas detector.
2
+
3
+ Detects:
4
+ - Workers / Consumers: @app.task, @celery.task, @shared_task, @celery_app.task
5
+ - Publishers / Producers: task.delay(), task.apply_async(), app.send_task()
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from codegraph.async_queue.models import (
13
+ AsyncConsumerRecord,
14
+ AsyncMessagingSystem,
15
+ AsyncProducerRecord,
16
+ )
17
+
18
+ _CELERY_TASK_DECORATORS = {
19
+ "task",
20
+ "shared_task",
21
+ }
22
+
23
+
24
+ def detect_celery(
25
+ tree: ast.AST,
26
+ content: str,
27
+ file_path: str,
28
+ module_name: str,
29
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
30
+ producers: list[AsyncProducerRecord] = []
31
+ consumers: list[AsyncConsumerRecord] = []
32
+ lines = content.splitlines()
33
+
34
+ def get_line_snippet(lineno: int) -> str:
35
+ if 1 <= lineno <= len(lines):
36
+ return lines[lineno - 1].strip()
37
+ return ""
38
+
39
+ # Map of function names that are Celery tasks in this file
40
+ local_tasks: dict[str, str] = {} # fn_name -> task_name
41
+
42
+ # 1. Detect consumers (task definitions)
43
+ for node in ast.walk(tree):
44
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
45
+ for dec in node.decorator_list:
46
+ dec_func = dec.func if isinstance(dec, ast.Call) else dec
47
+ dec_id = getattr(dec_func, "id", "")
48
+ dec_attr = getattr(dec_func, "attr", "")
49
+
50
+ if dec_id in _CELERY_TASK_DECORATORS or dec_attr in _CELERY_TASK_DECORATORS:
51
+ # Found a Celery task definition
52
+ task_name = f"{module_name}.{node.name}"
53
+ metadata: dict[str, Any] = {}
54
+
55
+ if isinstance(dec, ast.Call):
56
+ for kw in dec.keywords:
57
+ if kw.arg == "name" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
58
+ task_name = kw.value.value
59
+ elif kw.arg == "queue" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
60
+ metadata["queue"] = kw.value.value
61
+ elif kw.arg == "max_retries" and isinstance(kw.value, ast.Constant):
62
+ metadata["max_retries"] = kw.value.value
63
+ elif kw.arg == "autoretry_for":
64
+ metadata["autoretry"] = True
65
+ elif kw.arg == "retry_backoff":
66
+ metadata["retry_backoff"] = True
67
+
68
+ local_tasks[node.name] = task_name
69
+ canonical_id = f"{module_name}.{node.name}"
70
+ queue_id = f"celery:{task_name}"
71
+
72
+ consumers.append(
73
+ AsyncConsumerRecord(
74
+ system=AsyncMessagingSystem.CELERY,
75
+ destination=task_name,
76
+ queue_id=queue_id,
77
+ handler_canonical_id=canonical_id,
78
+ file_path=file_path,
79
+ line=node.lineno,
80
+ handler_snippet=get_line_snippet(node.lineno),
81
+ confidence="HIGH",
82
+ metadata=metadata,
83
+ )
84
+ )
85
+
86
+ # 2. Detect producers (task invocations: .delay, .apply_async, app.send_task)
87
+ class CallVisitor(ast.NodeVisitor):
88
+ def __init__(self) -> None:
89
+ self.current_function: str = f"{module_name}.<module>"
90
+
91
+ def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
92
+ prev = self.current_function
93
+ self.current_function = f"{module_name}.{fn.name}"
94
+ self.generic_visit(fn)
95
+ self.current_function = prev
96
+
97
+ def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
98
+ prev = self.current_function
99
+ self.current_function = f"{module_name}.{fn.name}"
100
+ self.generic_visit(fn)
101
+ self.current_function = prev
102
+
103
+ def visit_Call(self, node: ast.Call) -> None:
104
+ if isinstance(node.func, ast.Attribute):
105
+ method_name = node.func.attr
106
+ # Case A: task.delay(...) or task.apply_async(...)
107
+ if method_name in ("delay", "apply_async"):
108
+ caller_val = node.func.value
109
+ task_target_name = ""
110
+ if isinstance(caller_val, ast.Name):
111
+ task_target_name = caller_val.id
112
+ elif isinstance(caller_val, ast.Attribute):
113
+ task_target_name = caller_val.attr
114
+
115
+ if task_target_name:
116
+ resolved_dest = local_tasks.get(task_target_name, task_target_name)
117
+ meta: dict[str, Any] = {"method": method_name}
118
+ if method_name == "apply_async":
119
+ for kw in node.keywords:
120
+ if kw.arg in ("countdown", "eta", "queue", "retry"):
121
+ meta[kw.arg] = True
122
+
123
+ producers.append(
124
+ AsyncProducerRecord(
125
+ system=AsyncMessagingSystem.CELERY,
126
+ destination=resolved_dest,
127
+ queue_id=f"celery:{resolved_dest}",
128
+ caller_canonical_id=self.current_function,
129
+ file_path=file_path,
130
+ line=node.lineno,
131
+ call_snippet=get_line_snippet(node.lineno),
132
+ confidence="HIGH",
133
+ metadata=meta,
134
+ )
135
+ )
136
+
137
+ # Case B: app.send_task("task_name", ...)
138
+ elif method_name == "send_task" and node.args:
139
+ first_arg = node.args[0]
140
+ if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
141
+ dest = first_arg.value
142
+ producers.append(
143
+ AsyncProducerRecord(
144
+ system=AsyncMessagingSystem.CELERY,
145
+ destination=dest,
146
+ queue_id=f"celery:{dest}",
147
+ caller_canonical_id=self.current_function,
148
+ file_path=file_path,
149
+ line=node.lineno,
150
+ call_snippet=get_line_snippet(node.lineno),
151
+ confidence="HIGH",
152
+ metadata={"method": "send_task"},
153
+ )
154
+ )
155
+
156
+ self.generic_visit(node)
157
+
158
+ CallVisitor().visit(tree)
159
+
160
+ return producers, consumers