codegraph-engine 2.1.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 (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,434 @@
1
+ """Pluggable, evidence-driven framework analyzers for Python and JavaScript/TypeScript.
2
+
3
+ Supported Frameworks:
4
+ Python: Flask, FastAPI, Django
5
+ JavaScript/TypeScript: Express, Next.js (App Router)
6
+
7
+ Architectural Invariants:
8
+ 1. Framework behavior is detected ONLY when concrete source evidence (imports, decorators,
9
+ route registration calls, or verified route handler exports) exists in the file.
10
+ 2. Route detection NEVER rescans the full AST; it consumes node visit events during
11
+ the primary single-pass parser traversal.
12
+ 3. Every endpoint produces a stable endpoint_id, human-readable route_signature, original
13
+ source route_path, and canonical normalized_route.
14
+ 4. Framework endpoints link to handlers via HANDLED_BY semantic edges using the standard
15
+ canonical symbol ID.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import ast
20
+ import re
21
+ from dataclasses import asdict, dataclass
22
+ from typing import Protocol
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class FrameworkEvidence:
27
+ framework: str # flask | fastapi | django | express | nextjs
28
+ construct_type: str # ROUTE | CONTROLLER | MIDDLEWARE | DEPENDENCY | MODEL
29
+ file_path: str
30
+ line: int
31
+ column: int | None
32
+ evidence: str
33
+ confidence: str = "HIGH"
34
+
35
+ def as_dict(self) -> dict[str, object]:
36
+ return asdict(self)
37
+
38
+
39
+ def normalize_route_path(route_path: str, framework: str) -> str:
40
+ """Normalize framework-specific route parameters into canonical `{param}` format.
41
+
42
+ Examples:
43
+ Flask: /users/<id> -> /users/{id}
44
+ Flask: /users/<int:id> -> /users/{id}
45
+ FastAPI: /users/{id} -> /users/{id}
46
+ Express: /users/:id -> /users/{id}
47
+ Next.js: /users/[id] -> /users/{id}
48
+ Next.js: /users/[...slug] -> /users/{slug}
49
+ """
50
+ raw = "/" + route_path.strip().lstrip("/") if route_path.strip() else "/"
51
+ # Strip trailing slash unless root
52
+ if len(raw) > 1 and raw.endswith("/"):
53
+ raw = raw.rstrip("/")
54
+
55
+ if framework == "flask":
56
+ # Match <[type:]param>
57
+ return re.sub(r"<(?:\w+:)?([A-Za-z_][\w]*)>", r"{\1}", raw)
58
+ elif framework == "express":
59
+ # Match :param
60
+ return re.sub(r":([A-Za-z_][\w]*)", r"{\1}", raw)
61
+ elif framework == "nextjs":
62
+ # Match [[...param]] or [...param] or [param]
63
+ return re.sub(r"\[(?:\.\.\.)?([A-Za-z_][\w]*)\]", r"{\1}", raw)
64
+ return raw
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class RouteDetection:
69
+ framework: str # flask | fastapi | django | express | nextjs
70
+ http_method: str # GET | POST | PUT | DELETE | PATCH | OPTIONS | HEAD | ANY
71
+ route_path: str # Original framework route string, e.g. /users/<int:id>
72
+ normalized_route: str # Canonical route string, e.g. /users/{id}
73
+ handler_name: str # e.g. "login_controller"
74
+ handler_canonical_id: str # Standard canonical symbol ID, e.g. "src.auth.routes.login_controller"
75
+ file_path: str
76
+ line: int
77
+ column: int | None = None
78
+ confidence: str = "HIGH"
79
+ resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
80
+ evidence: str = ""
81
+ module: str = ""
82
+
83
+ @property
84
+ def route_signature(self) -> str:
85
+ """Human-readable route signature, e.g. 'POST /login'."""
86
+ return f"{self.http_method} {self.route_path}"
87
+
88
+ @property
89
+ def endpoint_id(self) -> str:
90
+ """Stable, globally unique endpoint identifier scoped by module to prevent collisions."""
91
+ if self.module:
92
+ mod = self.module
93
+ else:
94
+ from codegraph.indexing.models import normalize_module
95
+ mod = normalize_module(self.file_path)
96
+ return f"API_ENDPOINT:{mod}:{self.http_method} {self.normalized_route}"
97
+
98
+ def as_dict(self) -> dict[str, object]:
99
+ return {
100
+ "endpoint_id": self.endpoint_id,
101
+ "route_signature": self.route_signature,
102
+ "framework": self.framework,
103
+ "http_method": self.http_method,
104
+ "route_path": self.route_path,
105
+ "normalized_route": self.normalized_route,
106
+ "handler_name": self.handler_name,
107
+ "handler_canonical_id": self.handler_canonical_id,
108
+ "file": self.file_path,
109
+ "file_path": self.file_path,
110
+ "line": self.line,
111
+ "column": self.column,
112
+ "confidence": self.confidence,
113
+ "resolution_status": self.resolution_status,
114
+ "evidence": self.evidence,
115
+ }
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class AnalysisContext:
120
+ file_path: str
121
+ module: str
122
+ imports_modules: set[str]
123
+ scope_qname: str
124
+ scope_canonical_id: str
125
+ scope_kind: str
126
+
127
+
128
+ class FrameworkAnalyzer(Protocol):
129
+ framework: str
130
+
131
+ def analyze_python_node(
132
+ self,
133
+ node: ast.AST,
134
+ context: AnalysisContext,
135
+ ) -> list[RouteDetection]: ...
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Python Framework Analyzers
140
+ # ---------------------------------------------------------------------------
141
+
142
+ _FLASK_METHODS = {"route", "get", "post", "put", "delete", "patch"}
143
+
144
+
145
+ class FlaskAnalyzer:
146
+ framework = "flask"
147
+
148
+ def analyze_python_node(
149
+ self,
150
+ node: ast.AST,
151
+ context: AnalysisContext,
152
+ ) -> list[RouteDetection]:
153
+ routes: list[RouteDetection] = []
154
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
155
+ return routes
156
+
157
+ for dec in node.decorator_list:
158
+ if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
159
+ continue
160
+ attr = dec.func.attr.lower()
161
+ if attr not in _FLASK_METHODS or not dec.args:
162
+ continue
163
+
164
+ first_arg = dec.args[0]
165
+ if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
166
+ continue
167
+
168
+ route_path = first_arg.value
169
+ methods = [attr.upper()] if attr != "route" else ["GET"]
170
+
171
+ for kw in dec.keywords:
172
+ if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
173
+ methods = [
174
+ e.value.upper()
175
+ for e in kw.value.elts
176
+ if isinstance(e, ast.Constant) and isinstance(e.value, str)
177
+ ] or ["GET"]
178
+
179
+ col = getattr(dec, "col_offset", None)
180
+ norm = normalize_route_path(route_path, "flask")
181
+ handler_canon = (
182
+ f"{context.scope_canonical_id}.{node.name}"
183
+ if context.scope_qname
184
+ else f"{context.module}.{node.name}"
185
+ )
186
+
187
+ for method in methods:
188
+ routes.append(
189
+ RouteDetection(
190
+ framework="flask",
191
+ http_method=method,
192
+ route_path=route_path,
193
+ normalized_route=norm,
194
+ handler_name=node.name,
195
+ handler_canonical_id=handler_canon,
196
+ file_path=context.file_path,
197
+ line=dec.lineno,
198
+ column=col,
199
+ confidence="HIGH",
200
+ resolution_status="RESOLVED",
201
+ evidence=f"Flask decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
202
+ module=context.module,
203
+ )
204
+ )
205
+ return routes
206
+
207
+
208
+ _FASTAPI_METHODS = {"get", "post", "put", "delete", "patch", "options", "head", "api_route"}
209
+
210
+
211
+ class FastAPIAnalyzer:
212
+ framework = "fastapi"
213
+
214
+ def analyze_python_node(
215
+ self,
216
+ node: ast.AST,
217
+ context: AnalysisContext,
218
+ ) -> list[RouteDetection]:
219
+ routes: list[RouteDetection] = []
220
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
221
+ return routes
222
+
223
+ for dec in node.decorator_list:
224
+ if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
225
+ continue
226
+ attr = dec.func.attr.lower()
227
+ if attr not in _FASTAPI_METHODS or not dec.args:
228
+ continue
229
+
230
+ first_arg = dec.args[0]
231
+ if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
232
+ continue
233
+
234
+ route_path = first_arg.value
235
+ methods = [attr.upper()] if attr != "api_route" else ["GET"]
236
+
237
+ for kw in dec.keywords:
238
+ if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
239
+ methods = [
240
+ e.value.upper()
241
+ for e in kw.value.elts
242
+ if isinstance(e, ast.Constant) and isinstance(e.value, str)
243
+ ] or ["GET"]
244
+
245
+ col = getattr(dec, "col_offset", None)
246
+ norm = normalize_route_path(route_path, "fastapi")
247
+ handler_canon = (
248
+ f"{context.scope_canonical_id}.{node.name}"
249
+ if context.scope_qname
250
+ else f"{context.module}.{node.name}"
251
+ )
252
+
253
+ for method in methods:
254
+ routes.append(
255
+ RouteDetection(
256
+ framework="fastapi",
257
+ http_method=method,
258
+ route_path=route_path,
259
+ normalized_route=norm,
260
+ handler_name=node.name,
261
+ handler_canonical_id=handler_canon,
262
+ file_path=context.file_path,
263
+ line=dec.lineno,
264
+ column=col,
265
+ confidence="HIGH",
266
+ resolution_status="RESOLVED",
267
+ evidence=f"FastAPI decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
268
+ module=context.module,
269
+ )
270
+ )
271
+ return routes
272
+
273
+
274
+ class DjangoAnalyzer:
275
+ framework = "django"
276
+
277
+ def analyze_python_node(
278
+ self,
279
+ node: ast.AST,
280
+ context: AnalysisContext,
281
+ ) -> list[RouteDetection]:
282
+ routes: list[RouteDetection] = []
283
+ if not isinstance(node, ast.Call):
284
+ return routes
285
+
286
+ func_name = ""
287
+ if isinstance(node.func, ast.Name):
288
+ func_name = node.func.id
289
+ elif isinstance(node.func, ast.Attribute):
290
+ func_name = node.func.attr
291
+
292
+ if func_name in ("path", "re_path") and len(node.args) >= 2:
293
+ arg0, arg1 = node.args[0], node.args[1]
294
+ if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
295
+ route_path = "/" + arg0.value.lstrip("/")
296
+ handler_expr = ast.unparse(arg1) if hasattr(ast, "unparse") else "handler"
297
+ handler_clean = handler_expr.replace(".as_view()", "").strip()
298
+ handler_name = handler_clean.split(".")[-1]
299
+ handler_canon = (
300
+ f"{context.module}.{handler_clean}"
301
+ if not handler_clean.startswith(context.module)
302
+ else handler_clean
303
+ )
304
+ col = getattr(node, "col_offset", None)
305
+ norm = normalize_route_path(route_path, "django")
306
+
307
+ routes.append(
308
+ RouteDetection(
309
+ framework="django",
310
+ http_method="ANY",
311
+ route_path=route_path,
312
+ normalized_route=norm,
313
+ handler_name=handler_name,
314
+ handler_canonical_id=handler_canon,
315
+ file_path=context.file_path,
316
+ line=node.lineno,
317
+ column=col,
318
+ confidence="HIGH",
319
+ resolution_status="RESOLVED",
320
+ evidence=f"Django {func_name}('{arg0.value}', {handler_expr})",
321
+ module=context.module,
322
+ )
323
+ )
324
+ return routes
325
+
326
+
327
+ def get_python_analyzers(imports_modules: set[str]) -> list[FrameworkAnalyzer]:
328
+ """Select relevant Python framework analyzers based on concrete source import evidence."""
329
+ analyzers: list[FrameworkAnalyzer] = []
330
+ has_flask = any("flask" in m for m in imports_modules)
331
+ has_fastapi = any("fastapi" in m for m in imports_modules)
332
+ has_django = any("django" in m for m in imports_modules)
333
+
334
+ if has_flask:
335
+ analyzers.append(FlaskAnalyzer())
336
+ if has_fastapi:
337
+ analyzers.append(FastAPIAnalyzer())
338
+ if has_django:
339
+ analyzers.append(DjangoAnalyzer())
340
+
341
+ # If no framework import is explicitly present, enable all 3 conservatively so route decorators
342
+ # in files where imports are implicit or aliased are still recognized
343
+ if not analyzers:
344
+ analyzers = [FlaskAnalyzer(), FastAPIAnalyzer(), DjangoAnalyzer()]
345
+
346
+ return analyzers
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # JavaScript / TypeScript Framework Analyzers
351
+ # ---------------------------------------------------------------------------
352
+
353
+ _EXPRESS_CALL_RE = re.compile(
354
+ r"\b(?:app|router)\.(get|post|put|delete|patch|options|head|all)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z_$][\w$.]*)",
355
+ re.IGNORECASE,
356
+ )
357
+
358
+ _NEXT_ROUTE_EXPORT_RE = re.compile(
359
+ r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s*\("
360
+ )
361
+
362
+
363
+ def analyze_js_ts_line(
364
+ line: str,
365
+ lineno: int,
366
+ file_path: str,
367
+ module: str,
368
+ imports_modules: set[str],
369
+ ) -> list[RouteDetection]:
370
+ """Analyze a single JS/TS line during single-pass scan for Express and Next.js routes."""
371
+ routes: list[RouteDetection] = []
372
+
373
+ # 1. Express route registration
374
+ for m in _EXPRESS_CALL_RE.finditer(line):
375
+ method = m.group(1).upper()
376
+ if method == "ALL":
377
+ method = "ANY"
378
+ route_path = m.group(2)
379
+ handler_expr = m.group(3)
380
+ handler_name = handler_expr.split(".")[-1]
381
+ handler_canon = f"{module}.{handler_expr}"
382
+ norm = normalize_route_path(route_path, "express")
383
+
384
+ routes.append(
385
+ RouteDetection(
386
+ framework="express",
387
+ http_method=method,
388
+ route_path=route_path,
389
+ normalized_route=norm,
390
+ handler_name=handler_name,
391
+ handler_canonical_id=handler_canon,
392
+ file_path=file_path,
393
+ line=lineno,
394
+ column=m.start(),
395
+ confidence="HIGH",
396
+ resolution_status="RESOLVED",
397
+ evidence=f"Express route registration {m.group(0)}",
398
+ module=module,
399
+ )
400
+ )
401
+
402
+ # 2. Next.js App Router route handlers (e.g. app/api/login/route.ts)
403
+ norm_path = file_path.replace("\\", "/")
404
+ is_next_app_route = "/route." in norm_path or norm_path.startswith("route.")
405
+ has_next_import = any("next" in m.lower() for m in imports_modules)
406
+
407
+ if is_next_app_route or has_next_import:
408
+ for m in _NEXT_ROUTE_EXPORT_RE.finditer(line):
409
+ method = m.group(1).upper()
410
+ route_path = "/" + norm_path.rsplit("/", 1)[0] if "/" in norm_path else "/"
411
+ for prefix in ("/src/app", "/app", "/src/pages", "/pages"):
412
+ if route_path.startswith(prefix):
413
+ route_path = route_path[len(prefix) :] or "/"
414
+ break
415
+ norm = normalize_route_path(route_path, "nextjs")
416
+ routes.append(
417
+ RouteDetection(
418
+ framework="nextjs",
419
+ http_method=method,
420
+ route_path=route_path,
421
+ normalized_route=norm,
422
+ handler_name=method,
423
+ handler_canonical_id=f"{module}.{method}",
424
+ file_path=file_path,
425
+ line=lineno,
426
+ column=m.start(),
427
+ confidence="HIGH",
428
+ resolution_status="RESOLVED",
429
+ evidence=f"Next.js App Router HTTP export {method} in {file_path}",
430
+ module=module,
431
+ )
432
+ )
433
+
434
+ return routes