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,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
|