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,363 @@
|
|
|
1
|
+
"""Hierarchical route composition engine for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Composes declared framework routes across router mounting hierarchies:
|
|
4
|
+
1. FastAPI: include_router(...), nested prefixes, APIRouter definitions & aliases
|
|
5
|
+
2. Flask: register_blueprint(...), url_prefix composition, Blueprint definitions
|
|
6
|
+
3. Django: path(..., include(...)), multi-file urlpatterns composition
|
|
7
|
+
4. Express: app.use(...), router.use(...), nested router mounts
|
|
8
|
+
|
|
9
|
+
Architectural Invariants:
|
|
10
|
+
1. Deterministic and collision-free endpoint IDs.
|
|
11
|
+
2. Route composition produces FRAMEWORK_VERIFIED relationships only when statically proven.
|
|
12
|
+
3. Router mounting is NEVER represented as CALLS.
|
|
13
|
+
4. Exact source evidence is preserved for every mount and composed route.
|
|
14
|
+
5. Idempotent on repeated indexing; handles incremental route changes and file removals cleanly.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import posixpath
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from codegraph.frameworks import (
|
|
23
|
+
RouteDetection,
|
|
24
|
+
RouterDefinition,
|
|
25
|
+
RouterMountDetection,
|
|
26
|
+
join_route_prefixes,
|
|
27
|
+
normalize_route_path,
|
|
28
|
+
)
|
|
29
|
+
from codegraph.indexing.models import ImportRef
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class MountPathStep:
|
|
34
|
+
prefix: str
|
|
35
|
+
evidence: str
|
|
36
|
+
file_path: str
|
|
37
|
+
line: int
|
|
38
|
+
parent_node: tuple[str, str]
|
|
39
|
+
child_node: tuple[str, str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ComposedMountEdge:
|
|
44
|
+
parent_canonical_id: str
|
|
45
|
+
child_canonical_id: str
|
|
46
|
+
file_path: str
|
|
47
|
+
line: int
|
|
48
|
+
evidence: str
|
|
49
|
+
framework: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_child_file_and_router(
|
|
53
|
+
mount: RouterMountDetection,
|
|
54
|
+
imports_by_file: dict[str, list[ImportRef]],
|
|
55
|
+
known_files: set[str],
|
|
56
|
+
) -> tuple[str | None, str]:
|
|
57
|
+
"""Deterministically resolve a child router expression to its (target_file, target_router_name)."""
|
|
58
|
+
child_expr = mount.child_router.strip("'\"")
|
|
59
|
+
source_file = mount.file_path
|
|
60
|
+
framework = mount.framework
|
|
61
|
+
file_imports = imports_by_file.get(source_file, [])
|
|
62
|
+
|
|
63
|
+
# 1. Match against imports in the mounting file
|
|
64
|
+
for imp in file_imports:
|
|
65
|
+
if imp.local_name == child_expr or imp.alias == child_expr:
|
|
66
|
+
imported_mod = imp.imported_module
|
|
67
|
+
imported_name = imp.imported_name or "router"
|
|
68
|
+
|
|
69
|
+
# Express: relative or module path (e.g. './routes/users')
|
|
70
|
+
if framework == "express":
|
|
71
|
+
source_dir = posixpath.dirname(source_file)
|
|
72
|
+
candidate_base = posixpath.normpath(posixpath.join(source_dir, imported_mod))
|
|
73
|
+
candidates = [
|
|
74
|
+
f"{candidate_base}.ts",
|
|
75
|
+
f"{candidate_base}.js",
|
|
76
|
+
f"{candidate_base}/index.ts",
|
|
77
|
+
f"{candidate_base}/index.js",
|
|
78
|
+
candidate_base,
|
|
79
|
+
]
|
|
80
|
+
for c in candidates:
|
|
81
|
+
norm_c = c.replace("\\", "/").lstrip("./")
|
|
82
|
+
if norm_c in known_files:
|
|
83
|
+
return norm_c, "router"
|
|
84
|
+
for kf in known_files:
|
|
85
|
+
if kf == norm_c or kf.endswith(f"/{norm_c}") or norm_c.endswith(f"/{kf}"):
|
|
86
|
+
return kf, "router"
|
|
87
|
+
|
|
88
|
+
# Python: fastapi, flask, django
|
|
89
|
+
else:
|
|
90
|
+
if imported_mod.startswith("."):
|
|
91
|
+
# Relative import
|
|
92
|
+
dots = len(imported_mod) - len(imported_mod.lstrip("."))
|
|
93
|
+
curr_dir = Path(source_file).parent
|
|
94
|
+
for _ in range(dots - 1):
|
|
95
|
+
curr_dir = curr_dir.parent
|
|
96
|
+
rem = imported_mod.lstrip(".")
|
|
97
|
+
rel_sub = rem.replace(".", "/") if rem else ""
|
|
98
|
+
base_cand = curr_dir / rel_sub if rel_sub else curr_dir
|
|
99
|
+
candidates = [
|
|
100
|
+
f"{base_cand}.py",
|
|
101
|
+
f"{base_cand}/__init__.py",
|
|
102
|
+
]
|
|
103
|
+
else:
|
|
104
|
+
# Absolute import
|
|
105
|
+
mod_path = imported_mod.replace(".", "/")
|
|
106
|
+
candidates = [
|
|
107
|
+
f"{mod_path}.py",
|
|
108
|
+
f"src/{mod_path}.py",
|
|
109
|
+
f"{mod_path}/__init__.py",
|
|
110
|
+
f"src/{mod_path}/__init__.py",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for c in candidates:
|
|
114
|
+
norm_c = c.replace("\\", "/").lstrip("./")
|
|
115
|
+
if norm_c in known_files:
|
|
116
|
+
target_r = "urlpatterns" if framework == "django" else (imported_name if imported_name and imported_name != "*" else "router")
|
|
117
|
+
return norm_c, target_r
|
|
118
|
+
for kf in known_files:
|
|
119
|
+
if kf.endswith(norm_c) or norm_c.endswith(kf):
|
|
120
|
+
target_r = "urlpatterns" if framework == "django" else (imported_name if imported_name and imported_name != "*" else "router")
|
|
121
|
+
return kf, target_r
|
|
122
|
+
|
|
123
|
+
# 2. Django string module name e.g. "myapp.urls"
|
|
124
|
+
if framework == "django":
|
|
125
|
+
clean_mod = child_expr.replace(".", "/")
|
|
126
|
+
for c in (f"{clean_mod}.py", f"src/{clean_mod}.py", f"{clean_mod}/__init__.py"):
|
|
127
|
+
norm_c = c.replace("\\", "/").lstrip("./")
|
|
128
|
+
if norm_c in known_files:
|
|
129
|
+
return norm_c, "urlpatterns"
|
|
130
|
+
for kf in known_files:
|
|
131
|
+
if kf.endswith(norm_c):
|
|
132
|
+
return kf, "urlpatterns"
|
|
133
|
+
|
|
134
|
+
# 3. Same-file router reference
|
|
135
|
+
return source_file, child_expr
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def compose_routes(
|
|
139
|
+
raw_routes: list[RouteDetection],
|
|
140
|
+
mounts: list[RouterMountDetection],
|
|
141
|
+
router_defs: list[RouterDefinition],
|
|
142
|
+
imports: list[ImportRef],
|
|
143
|
+
known_files: set[str],
|
|
144
|
+
) -> tuple[list[RouteDetection], list[ComposedMountEdge]]:
|
|
145
|
+
"""Hierarchically compose framework routes across router mounting hierarchies.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
(composed_routes, mount_edges)
|
|
149
|
+
"""
|
|
150
|
+
imports_by_file: dict[str, list[ImportRef]] = {}
|
|
151
|
+
for imp in imports:
|
|
152
|
+
imports_by_file.setdefault(imp.source_file, []).append(imp)
|
|
153
|
+
|
|
154
|
+
# 1. Base prefixes defined on routers (e.g. APIRouter(prefix="/items"), Blueprint(..., url_prefix="/auth"))
|
|
155
|
+
base_prefixes: dict[tuple[str, str], tuple[str, str, int]] = {}
|
|
156
|
+
for r_def in router_defs:
|
|
157
|
+
if r_def.prefix:
|
|
158
|
+
base_prefixes[(r_def.file_path, r_def.router_name)] = (
|
|
159
|
+
r_def.prefix,
|
|
160
|
+
r_def.evidence,
|
|
161
|
+
r_def.line,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 2. Group raw routes by (file_path, router_name)
|
|
165
|
+
# Also index by file_path alone for routers whose local names might match default or single routers
|
|
166
|
+
routes_by_node: dict[tuple[str, str], list[RouteDetection]] = {}
|
|
167
|
+
routes_by_file: dict[str, list[RouteDetection]] = {}
|
|
168
|
+
for rt in raw_routes:
|
|
169
|
+
r_name = rt.router_name or "router"
|
|
170
|
+
routes_by_node.setdefault((rt.file_path, r_name), []).append(rt)
|
|
171
|
+
routes_by_file.setdefault(rt.file_path, []).append(rt)
|
|
172
|
+
|
|
173
|
+
# 3. Resolve mount edges: parent_node -> child_node
|
|
174
|
+
# parent_node: (mount.file_path, mount.parent_router)
|
|
175
|
+
# child_node: (target_file, target_router_name)
|
|
176
|
+
mount_edges: list[tuple[tuple[str, str], tuple[str, str], MountPathStep]] = []
|
|
177
|
+
verified_mount_graph_edges: list[ComposedMountEdge] = []
|
|
178
|
+
|
|
179
|
+
for m in mounts:
|
|
180
|
+
target_file, target_router = _resolve_child_file_and_router(m, imports_by_file, known_files)
|
|
181
|
+
if not target_file:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
parent_node = (m.file_path, m.parent_router)
|
|
185
|
+
child_node = (target_file, target_router)
|
|
186
|
+
|
|
187
|
+
step = MountPathStep(
|
|
188
|
+
prefix=m.prefix,
|
|
189
|
+
evidence=m.evidence,
|
|
190
|
+
file_path=m.file_path,
|
|
191
|
+
line=m.line,
|
|
192
|
+
parent_node=parent_node,
|
|
193
|
+
child_node=child_node,
|
|
194
|
+
)
|
|
195
|
+
mount_edges.append((parent_node, child_node, step))
|
|
196
|
+
|
|
197
|
+
parent_canon = f"{m.file_path}::{m.parent_router}"
|
|
198
|
+
child_canon = f"{target_file}::{target_router}"
|
|
199
|
+
verified_mount_graph_edges.append(
|
|
200
|
+
ComposedMountEdge(
|
|
201
|
+
parent_canonical_id=parent_canon,
|
|
202
|
+
child_canonical_id=child_canon,
|
|
203
|
+
file_path=m.file_path,
|
|
204
|
+
line=m.line,
|
|
205
|
+
evidence=m.evidence,
|
|
206
|
+
framework=m.framework,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Build adjacency maps
|
|
211
|
+
incoming_edges: dict[tuple[str, str], list[MountPathStep]] = {}
|
|
212
|
+
outgoing_edges: dict[tuple[str, str], list[MountPathStep]] = {}
|
|
213
|
+
for parent_node, child_node, step in mount_edges:
|
|
214
|
+
outgoing_edges.setdefault(parent_node, []).append(step)
|
|
215
|
+
incoming_edges.setdefault(child_node, []).append(step)
|
|
216
|
+
|
|
217
|
+
# 4. Find all paths reaching each router node
|
|
218
|
+
# A path is a list of MountPathStep from an unparented root to the node.
|
|
219
|
+
# If a node has incoming mounts, we compute all paths from roots to that node using DFS.
|
|
220
|
+
paths_to_node: dict[tuple[str, str], list[list[MountPathStep]]] = {}
|
|
221
|
+
|
|
222
|
+
def get_paths(node: tuple[str, str], visited: set[tuple[str, str]]) -> list[list[MountPathStep]]:
|
|
223
|
+
if node in visited:
|
|
224
|
+
return []
|
|
225
|
+
in_steps = incoming_edges.get(node, [])
|
|
226
|
+
if not in_steps:
|
|
227
|
+
return [[]]
|
|
228
|
+
|
|
229
|
+
all_paths: list[list[MountPathStep]] = []
|
|
230
|
+
visited.add(node)
|
|
231
|
+
for step in in_steps:
|
|
232
|
+
parent_paths = get_paths(step.parent_node, visited.copy())
|
|
233
|
+
for pp in parent_paths:
|
|
234
|
+
all_paths.append(pp + [step])
|
|
235
|
+
return all_paths
|
|
236
|
+
|
|
237
|
+
for _, child_node, _ in mount_edges:
|
|
238
|
+
if child_node not in paths_to_node:
|
|
239
|
+
paths_to_node[child_node] = get_paths(child_node, set())
|
|
240
|
+
|
|
241
|
+
# 5. Compose routes
|
|
242
|
+
composed_routes: list[RouteDetection] = []
|
|
243
|
+
seen_endpoint_ids: set[str] = set()
|
|
244
|
+
|
|
245
|
+
# Track which nodes had routes successfully composed via mounts
|
|
246
|
+
mounted_nodes: set[tuple[str, str]] = set()
|
|
247
|
+
|
|
248
|
+
for node, paths in paths_to_node.items():
|
|
249
|
+
node_file, node_router = node
|
|
250
|
+
# Find matching routes for this node
|
|
251
|
+
node_routes = routes_by_node.get(node, [])
|
|
252
|
+
if not node_routes:
|
|
253
|
+
# Fallback: if node_file has routes and there's only one router used in that file
|
|
254
|
+
file_rts = routes_by_file.get(node_file, [])
|
|
255
|
+
distinct_routers = {r.router_name for r in file_rts if r.router_name}
|
|
256
|
+
if len(distinct_routers) <= 1:
|
|
257
|
+
node_routes = file_rts
|
|
258
|
+
|
|
259
|
+
if not node_routes:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
mounted_nodes.add(node)
|
|
263
|
+
base_prefix, _, _ = base_prefixes.get(node, ("", "", 0))
|
|
264
|
+
|
|
265
|
+
for path in paths:
|
|
266
|
+
mount_prefix = join_route_prefixes(*[s.prefix for s in path])
|
|
267
|
+
mount_evidence_chain = " -> ".join([f"{s.evidence} ({s.file_path}:{s.line})" for s in path])
|
|
268
|
+
|
|
269
|
+
for rt in node_routes:
|
|
270
|
+
composed_path = join_route_prefixes(mount_prefix, base_prefix, rt.route_path)
|
|
271
|
+
composed_norm = normalize_route_path(composed_path, rt.framework)
|
|
272
|
+
composed_ev = f"{rt.evidence}; mounted via {mount_evidence_chain}"
|
|
273
|
+
if base_prefix:
|
|
274
|
+
composed_ev += f" (base prefix '{base_prefix}')"
|
|
275
|
+
|
|
276
|
+
# Generate endpoint_id and ensure deterministic collision avoidance
|
|
277
|
+
clean_path = rt.file_path.replace("\\", "/").strip().lstrip("./") or "root"
|
|
278
|
+
handler_part = f"#{rt.handler_name}" if rt.handler_name else ""
|
|
279
|
+
fw_part = f":{rt.framework}" if rt.framework else ""
|
|
280
|
+
base_ep_id = f"API_ENDPOINT{fw_part}:{clean_path}:{rt.line}:{rt.http_method} {composed_norm}{handler_part}"
|
|
281
|
+
|
|
282
|
+
final_ep_id = base_ep_id
|
|
283
|
+
counter = 1
|
|
284
|
+
while final_ep_id in seen_endpoint_ids:
|
|
285
|
+
final_ep_id = f"{base_ep_id}#m{counter}"
|
|
286
|
+
counter += 1
|
|
287
|
+
seen_endpoint_ids.add(final_ep_id)
|
|
288
|
+
|
|
289
|
+
composed_routes.append(
|
|
290
|
+
RouteDetection(
|
|
291
|
+
framework=rt.framework,
|
|
292
|
+
http_method=rt.http_method,
|
|
293
|
+
route_path=composed_path,
|
|
294
|
+
normalized_route=composed_norm,
|
|
295
|
+
handler_name=rt.handler_name,
|
|
296
|
+
handler_canonical_id=rt.handler_canonical_id,
|
|
297
|
+
file_path=rt.file_path,
|
|
298
|
+
line=rt.line,
|
|
299
|
+
column=rt.column,
|
|
300
|
+
confidence=rt.confidence,
|
|
301
|
+
resolution_status="RESOLVED",
|
|
302
|
+
evidence=composed_ev,
|
|
303
|
+
module=rt.module,
|
|
304
|
+
endpoint_id_override=final_ep_id,
|
|
305
|
+
router_name=rt.router_name,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# 6. Unmounted / standalone routes (or direct app routes with no parent mounts)
|
|
310
|
+
for (f_path, r_name), node_routes in routes_by_node.items():
|
|
311
|
+
if (f_path, r_name) in mounted_nodes:
|
|
312
|
+
continue
|
|
313
|
+
# Also check if the whole file was handled by a mount
|
|
314
|
+
if any(mn[0] == f_path for mn in mounted_nodes):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
base_prefix, _, _ = base_prefixes.get((f_path, r_name), ("", "", 0))
|
|
318
|
+
for rt in node_routes:
|
|
319
|
+
if base_prefix:
|
|
320
|
+
composed_path = join_route_prefixes(base_prefix, rt.route_path)
|
|
321
|
+
composed_norm = normalize_route_path(composed_path, rt.framework)
|
|
322
|
+
composed_ev = f"{rt.evidence} (base prefix '{base_prefix}')"
|
|
323
|
+
else:
|
|
324
|
+
composed_path = rt.route_path
|
|
325
|
+
composed_norm = rt.normalized_route
|
|
326
|
+
composed_ev = rt.evidence
|
|
327
|
+
|
|
328
|
+
# Use default deterministic endpoint_id
|
|
329
|
+
final_ep_id = rt.endpoint_id
|
|
330
|
+
if base_prefix:
|
|
331
|
+
clean_path = rt.file_path.replace("\\", "/").strip().lstrip("./") or "root"
|
|
332
|
+
handler_part = f"#{rt.handler_name}" if rt.handler_name else ""
|
|
333
|
+
fw_part = f":{rt.framework}" if rt.framework else ""
|
|
334
|
+
final_ep_id = f"API_ENDPOINT{fw_part}:{clean_path}:{rt.line}:{rt.http_method} {composed_norm}{handler_part}"
|
|
335
|
+
|
|
336
|
+
counter = 1
|
|
337
|
+
cand_ep = final_ep_id
|
|
338
|
+
while cand_ep in seen_endpoint_ids:
|
|
339
|
+
cand_ep = f"{final_ep_id}#u{counter}"
|
|
340
|
+
counter += 1
|
|
341
|
+
seen_endpoint_ids.add(cand_ep)
|
|
342
|
+
|
|
343
|
+
composed_routes.append(
|
|
344
|
+
RouteDetection(
|
|
345
|
+
framework=rt.framework,
|
|
346
|
+
http_method=rt.http_method,
|
|
347
|
+
route_path=composed_path,
|
|
348
|
+
normalized_route=composed_norm,
|
|
349
|
+
handler_name=rt.handler_name,
|
|
350
|
+
handler_canonical_id=rt.handler_canonical_id,
|
|
351
|
+
file_path=rt.file_path,
|
|
352
|
+
line=rt.line,
|
|
353
|
+
column=rt.column,
|
|
354
|
+
confidence=rt.confidence,
|
|
355
|
+
resolution_status="RESOLVED",
|
|
356
|
+
evidence=composed_ev,
|
|
357
|
+
module=rt.module,
|
|
358
|
+
endpoint_id_override=cand_ep if cand_ep != rt.endpoint_id else None,
|
|
359
|
+
router_name=rt.router_name,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return composed_routes, verified_mount_graph_edges
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Universal Parameterized Route Topology and Cross-Language Route Matching.
|
|
2
|
+
|
|
3
|
+
Bridges frontend fetch/axios/SWR/React-Query client calls with backend route declarations
|
|
4
|
+
across disparate frameworks (React, Next.js, Express, NestJS, Flask, FastAPI, Django).
|
|
5
|
+
|
|
6
|
+
Supported Syntax Conventions:
|
|
7
|
+
- JavaScript/TypeScript: `${param}`, `:param`, `[param]`, `[...slug]`
|
|
8
|
+
- Python Flask: `<param>`, `<int:param>`, `<path:param>`, `<uuid:param>`
|
|
9
|
+
- Python FastAPI: `{param}`
|
|
10
|
+
- Express/NestJS: `:param`
|
|
11
|
+
- Django: `<int:param>`, `<str:param>`, `<slug:param>`
|
|
12
|
+
- Literal client instances: `/users/42/orders` -> matches `/users/:id/orders`
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_PARAM_SEGMENT_TOKEN = "{*}"
|
|
21
|
+
|
|
22
|
+
# Common API Gateway / Reverse Proxy prefixes used in frontend apps
|
|
23
|
+
_KNOWN_PROXY_PREFIXES = (
|
|
24
|
+
("api", "v1"),
|
|
25
|
+
("api", "v2"),
|
|
26
|
+
("api",),
|
|
27
|
+
("v1",),
|
|
28
|
+
("v2",),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Regex to detect parameter segments across all framework dialects
|
|
32
|
+
_IS_PARAM_SEGMENT = re.compile(
|
|
33
|
+
r"""^(?:
|
|
34
|
+
\$\{[^}]+\} | # JS/TS template literal: ${userId}
|
|
35
|
+
:[a-zA-Z0-9_$]+ | # Express / NestJS: :userId
|
|
36
|
+
\{[a-zA-Z0-9_$]+\} | # FastAPI / Next.js: {userId}
|
|
37
|
+
\[[a-zA-Z0-9_$]+\] | # Next.js: [userId]
|
|
38
|
+
\[\.\.\.[a-zA-Z0-9_$]+\] | # Next.js catch-all: [...slug]
|
|
39
|
+
<[a-zA-Z0-9_:]+> | # Flask / Django: <int:user_id>, <user_id>
|
|
40
|
+
\d+ | # Literal integer in client URL: 42, 101
|
|
41
|
+
[0-9a-fA-F-]{36} # Literal UUID in client URL
|
|
42
|
+
)$""",
|
|
43
|
+
re.VERBOSE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class NormalizedRoute:
|
|
49
|
+
method: str
|
|
50
|
+
raw_path: str
|
|
51
|
+
segments: tuple[str, ...]
|
|
52
|
+
proxy_stripped_segments: tuple[str, ...]
|
|
53
|
+
is_parameterized: bool
|
|
54
|
+
|
|
55
|
+
def as_dict(self) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"method": self.method,
|
|
58
|
+
"raw_path": self.raw_path,
|
|
59
|
+
"segments": list(self.segments),
|
|
60
|
+
"proxy_stripped_segments": list(self.proxy_stripped_segments),
|
|
61
|
+
"is_parameterized": self.is_parameterized,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_route(method: str, path: str) -> NormalizedRoute:
|
|
66
|
+
"""Normalize a raw route into a canonical topological representation.
|
|
67
|
+
|
|
68
|
+
Strips query strings, hashes, leading/trailing whitespace and slashes,
|
|
69
|
+
normalizes all parameter styles into '{*}', and computes proxy-stripped segments.
|
|
70
|
+
"""
|
|
71
|
+
clean_method = (method or "GET").strip().upper()
|
|
72
|
+
# Strip query parameters (?foo=bar) and URL fragments (#anchor)
|
|
73
|
+
clean_path = path.strip().split("?")[0].split("#")[0]
|
|
74
|
+
# Remove leading and trailing slashes
|
|
75
|
+
trimmed = clean_path.strip("/")
|
|
76
|
+
|
|
77
|
+
if not trimmed:
|
|
78
|
+
return NormalizedRoute(
|
|
79
|
+
method=clean_method,
|
|
80
|
+
raw_path=path,
|
|
81
|
+
segments=("/",),
|
|
82
|
+
proxy_stripped_segments=("/",),
|
|
83
|
+
is_parameterized=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
raw_parts = [p for p in trimmed.split("/") if p]
|
|
87
|
+
normalized_parts: list[str] = []
|
|
88
|
+
is_param = False
|
|
89
|
+
|
|
90
|
+
for part in raw_parts:
|
|
91
|
+
if _IS_PARAM_SEGMENT.match(part):
|
|
92
|
+
normalized_parts.append(_PARAM_SEGMENT_TOKEN)
|
|
93
|
+
is_param = True
|
|
94
|
+
elif "${" in part:
|
|
95
|
+
# Embedded template interpolation: e.g. "user-${id}"
|
|
96
|
+
normalized_parts.append(_PARAM_SEGMENT_TOKEN)
|
|
97
|
+
is_param = True
|
|
98
|
+
else:
|
|
99
|
+
normalized_parts.append(part.lower())
|
|
100
|
+
|
|
101
|
+
segments_tuple = tuple(normalized_parts)
|
|
102
|
+
|
|
103
|
+
# Compute proxy-stripped segments
|
|
104
|
+
stripped_parts = list(segments_tuple)
|
|
105
|
+
for prefix in _KNOWN_PROXY_PREFIXES:
|
|
106
|
+
if len(stripped_parts) > len(prefix) and tuple(stripped_parts[: len(prefix)]) == prefix:
|
|
107
|
+
stripped_parts = stripped_parts[len(prefix) :]
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
proxy_stripped_tuple = tuple(stripped_parts) if stripped_parts else ("/",)
|
|
111
|
+
|
|
112
|
+
return NormalizedRoute(
|
|
113
|
+
method=clean_method,
|
|
114
|
+
raw_path=path,
|
|
115
|
+
segments=segments_tuple,
|
|
116
|
+
proxy_stripped_segments=proxy_stripped_tuple,
|
|
117
|
+
is_parameterized=is_param,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def match_route_topology(
|
|
122
|
+
client_route: NormalizedRoute,
|
|
123
|
+
backend_route: NormalizedRoute,
|
|
124
|
+
allow_proxy_prefix: bool = True,
|
|
125
|
+
match_methods: bool = True,
|
|
126
|
+
) -> tuple[bool, str]:
|
|
127
|
+
"""Deterministically match a client-side route against a backend route definition.
|
|
128
|
+
|
|
129
|
+
Returns (matched, match_type):
|
|
130
|
+
- "EXACT": Segments match exactly with parameter wildcards.
|
|
131
|
+
- "PROXY_PREFIX": Client calls with /api or /v1 prefix matching backend root route.
|
|
132
|
+
- "BACKEND_MOUNTED": Backend route has prefix (e.g. mounted blueprint) matching client.
|
|
133
|
+
- "": No match.
|
|
134
|
+
"""
|
|
135
|
+
# 1. Check HTTP method compatibility
|
|
136
|
+
if match_methods and client_route.method and backend_route.method:
|
|
137
|
+
client_m = client_route.method
|
|
138
|
+
backend_m = backend_route.method
|
|
139
|
+
if client_m != "*" and backend_m != "*" and client_m != backend_m:
|
|
140
|
+
return False, ""
|
|
141
|
+
|
|
142
|
+
# 2. Exact topological match
|
|
143
|
+
if client_route.segments == backend_route.segments:
|
|
144
|
+
return True, "EXACT"
|
|
145
|
+
|
|
146
|
+
# 3. Proxy prefix match (e.g., Client: ['api', 'orders'] vs Backend: ['orders'])
|
|
147
|
+
if allow_proxy_prefix:
|
|
148
|
+
if client_route.proxy_stripped_segments == backend_route.segments:
|
|
149
|
+
return True, "PROXY_PREFIX"
|
|
150
|
+
|
|
151
|
+
# Reverse proxy prefix: Backend declared /api/orders, client called /orders (or vice-versa)
|
|
152
|
+
if client_route.segments == backend_route.proxy_stripped_segments:
|
|
153
|
+
return True, "PROXY_PREFIX"
|
|
154
|
+
|
|
155
|
+
if client_route.proxy_stripped_segments == backend_route.proxy_stripped_segments:
|
|
156
|
+
if client_route.proxy_stripped_segments != ("/",):
|
|
157
|
+
return True, "PROXY_PREFIX"
|
|
158
|
+
|
|
159
|
+
# 4. Suffix / sub-path mount match (e.g. Blueprint mounted at /api/shop and client calls /api/shop/checkout)
|
|
160
|
+
# Check if backend route is a suffix of client route or vice versa with identical parameter topology
|
|
161
|
+
c_segs = client_route.proxy_stripped_segments
|
|
162
|
+
b_segs = backend_route.proxy_stripped_segments
|
|
163
|
+
if len(c_segs) == len(b_segs):
|
|
164
|
+
matched = True
|
|
165
|
+
for cs, bs in zip(c_segs, b_segs, strict=True):
|
|
166
|
+
if cs != bs and cs != _PARAM_SEGMENT_TOKEN and bs != _PARAM_SEGMENT_TOKEN:
|
|
167
|
+
matched = False
|
|
168
|
+
break
|
|
169
|
+
if matched:
|
|
170
|
+
return True, "TOPOLOGICAL_PARAM_MATCH"
|
|
171
|
+
|
|
172
|
+
return False, ""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Runtime Observation, Ingestion, Map & Reconciliation package for CodeGraph."""
|
|
2
|
+
from .ingestor import (
|
|
3
|
+
DEFAULT_MAX_INGEST_EVENTS,
|
|
4
|
+
DEFAULT_RETENTION_OBSERVATIONS,
|
|
5
|
+
ingest_runtime_traces,
|
|
6
|
+
parse_json_runtime_events,
|
|
7
|
+
parse_otel_traces,
|
|
8
|
+
parse_sql_query_logs,
|
|
9
|
+
)
|
|
10
|
+
from .models import (
|
|
11
|
+
ReconciliationStatus,
|
|
12
|
+
RuntimeAggregatedEdge,
|
|
13
|
+
RuntimeEvent,
|
|
14
|
+
)
|
|
15
|
+
from .reconciliation import (
|
|
16
|
+
get_runtime_trace,
|
|
17
|
+
reconcile_static_runtime,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DEFAULT_MAX_INGEST_EVENTS",
|
|
22
|
+
"DEFAULT_RETENTION_OBSERVATIONS",
|
|
23
|
+
"ReconciliationStatus",
|
|
24
|
+
"RuntimeAggregatedEdge",
|
|
25
|
+
"RuntimeEvent",
|
|
26
|
+
"ServiceEndpoint",
|
|
27
|
+
"discover_live_listening_ports",
|
|
28
|
+
"discover_static_configured_ports",
|
|
29
|
+
"resolve_port_to_service",
|
|
30
|
+
"SchemaDriftItem",
|
|
31
|
+
"SchemaDriftReport",
|
|
32
|
+
"detect_schema_drift",
|
|
33
|
+
"RuntimeCollectorDaemon",
|
|
34
|
+
"DistributedTraceReport",
|
|
35
|
+
"link_distributed_trace",
|
|
36
|
+
"get_runtime_trace",
|
|
37
|
+
"ingest_runtime_traces",
|
|
38
|
+
"parse_json_runtime_events",
|
|
39
|
+
"parse_otel_traces",
|
|
40
|
+
"parse_sql_query_logs",
|
|
41
|
+
"reconcile_static_runtime",
|
|
42
|
+
]
|
|
43
|
+
from .collector import RuntimeCollectorDaemon
|
|
44
|
+
from .db_watcher import SchemaDriftItem, SchemaDriftReport, detect_schema_drift
|
|
45
|
+
from .port_inspector import (
|
|
46
|
+
ServiceEndpoint,
|
|
47
|
+
discover_live_listening_ports,
|
|
48
|
+
discover_static_configured_ports,
|
|
49
|
+
resolve_port_to_service,
|
|
50
|
+
)
|
|
51
|
+
from .trace_linker import DistributedTraceReport, link_distributed_trace
|