codegraph-engine 3.0.0__2-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 +868 -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,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