osscodeiq 0.0.0__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 (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,174 @@
1
+ """MCP server tools for OSSCodeIQ."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ from fastmcp import FastMCP
7
+
8
+ mcp = FastMCP(
9
+ "OSSCodeIQ",
10
+ instructions="Code intelligence graph query tools for exploring a codebase's architecture. "
11
+ "Use these tools to query nodes, edges, find components, trace impact, and generate flow diagrams.",
12
+ )
13
+
14
+ _service = None # Set during app startup
15
+
16
+
17
+ def set_service(svc) -> None:
18
+ global _service
19
+ _service = svc
20
+
21
+
22
+ def _svc():
23
+ if _service is None:
24
+ raise RuntimeError("Service not initialized")
25
+ return _service
26
+
27
+
28
+ def get_mcp_app():
29
+ """Return the MCP ASGI app for mounting into FastAPI."""
30
+ return mcp.http_app(path="/", transport="streamable-http")
31
+
32
+
33
+ # ── Core tools ───────────────────────────────────────────────────────────────
34
+
35
+
36
+ @mcp.tool()
37
+ def get_stats() -> str:
38
+ """Get project graph statistics — node counts, edge counts, backend info."""
39
+ return json.dumps(_svc().get_stats(), indent=2)
40
+
41
+
42
+ @mcp.tool()
43
+ def query_nodes(kind: str | None = None, limit: int = 50) -> str:
44
+ """Query nodes in the code graph. Filter by kind (endpoint, entity, guard, class, method, component, module, etc.)."""
45
+ return json.dumps(_svc().list_nodes(kind=kind, limit=limit, offset=0), indent=2)
46
+
47
+
48
+ @mcp.tool()
49
+ def query_edges(kind: str | None = None, limit: int = 50) -> str:
50
+ """Query edges in the code graph. Filter by kind (calls, imports, depends_on, queries, protects, etc.)."""
51
+ return json.dumps(_svc().list_edges(kind=kind, limit=limit, offset=0), indent=2)
52
+
53
+
54
+ @mcp.tool()
55
+ def get_node_neighbors(node_id: str, direction: str = "both") -> str:
56
+ """Get all nodes connected to a given node. Direction: both, in, out."""
57
+ return json.dumps(
58
+ _svc().get_neighbors(node_id, direction=direction, edge_kinds=None), indent=2
59
+ )
60
+
61
+
62
+ @mcp.tool()
63
+ def get_ego_graph(center: str, radius: int = 2) -> str:
64
+ """Get the subgraph within N hops of a center node. Returns all nodes and edges in the neighborhood."""
65
+ return json.dumps(
66
+ _svc().get_ego(center, radius=radius, edge_kinds=None), indent=2
67
+ )
68
+
69
+
70
+ @mcp.tool()
71
+ def find_cycles(limit: int = 100) -> str:
72
+ """Find circular dependency cycles in the graph."""
73
+ return json.dumps(_svc().find_cycles(limit=limit), indent=2)
74
+
75
+
76
+ @mcp.tool()
77
+ def find_shortest_path(source: str, target: str) -> str:
78
+ """Find the shortest path between two nodes."""
79
+ result = _svc().shortest_path(source, target)
80
+ if result is None:
81
+ return json.dumps({"error": f"No path found between {source} and {target}"}, indent=2)
82
+ return json.dumps(result, indent=2)
83
+
84
+
85
+ @mcp.tool()
86
+ def find_consumers(target_id: str) -> str:
87
+ """Find nodes that consume from a target (CONSUMES/LISTENS edges)."""
88
+ return json.dumps(_svc().consumers_of(target_id), indent=2)
89
+
90
+
91
+ @mcp.tool()
92
+ def find_producers(target_id: str) -> str:
93
+ """Find nodes that produce to a target (PRODUCES/PUBLISHES edges)."""
94
+ return json.dumps(_svc().producers_of(target_id), indent=2)
95
+
96
+
97
+ @mcp.tool()
98
+ def find_callers(target_id: str) -> str:
99
+ """Find nodes that call a target (CALLS edges)."""
100
+ return json.dumps(_svc().callers_of(target_id), indent=2)
101
+
102
+
103
+ @mcp.tool()
104
+ def find_dependencies(module_id: str) -> str:
105
+ """Find modules that a given module depends on."""
106
+ return json.dumps(_svc().dependencies_of(module_id), indent=2)
107
+
108
+
109
+ @mcp.tool()
110
+ def find_dependents(module_id: str) -> str:
111
+ """Find modules that depend on a given module."""
112
+ return json.dumps(_svc().dependents_of(module_id), indent=2)
113
+
114
+
115
+ @mcp.tool()
116
+ def generate_flow(view: str = "overview", format: str = "json") -> str:
117
+ """Generate an architecture flow diagram. Views: overview, ci, deploy, runtime, auth. Formats: json, mermaid."""
118
+ return json.dumps(_svc().generate_flow(view, format=format), indent=2)
119
+
120
+
121
+ @mcp.tool()
122
+ def analyze_codebase(incremental: bool = True) -> str:
123
+ """Trigger codebase analysis. Scans files, runs detectors, builds the code graph."""
124
+ try:
125
+ result = _svc().run_analysis(incremental)
126
+ return json.dumps(result, indent=2)
127
+ except Exception as exc:
128
+ return json.dumps({"error": str(exc)}, indent=2)
129
+
130
+
131
+ @mcp.tool()
132
+ def run_cypher(query: str) -> str:
133
+ """Execute a raw Cypher query (requires KuzuDB backend)."""
134
+ try:
135
+ result = _svc().query_cypher(query, None)
136
+ return json.dumps(result, indent=2)
137
+ except ValueError as exc:
138
+ return json.dumps({"error": str(exc)}, indent=2)
139
+
140
+
141
+ # ── Agentic triage tools ────────────────────────────────────────────────────
142
+
143
+
144
+ @mcp.tool()
145
+ def find_component_by_file(file_path: str) -> str:
146
+ """Given a file path (e.g. from a stacktrace), find the component/module it belongs to, its layer, and all connected nodes. Use this to map stack traces to architecture."""
147
+ return json.dumps(_svc().find_component_by_file(file_path), indent=2)
148
+
149
+
150
+ @mcp.tool()
151
+ def trace_impact(node_id: str, depth: int = 3) -> str:
152
+ """Trace downstream impact of a node — what depends on it, what breaks if it fails. Returns all transitively affected nodes."""
153
+ return json.dumps(_svc().trace_impact(node_id, depth=depth), indent=2)
154
+
155
+
156
+ @mcp.tool()
157
+ def find_related_endpoints(identifier: str) -> str:
158
+ """Given a file, class, or entity name, find all API endpoints that interact with it. Useful for mapping business operations to code."""
159
+ return json.dumps(_svc().find_related_endpoints(identifier), indent=2)
160
+
161
+
162
+ @mcp.tool()
163
+ def search_graph(query: str, limit: int = 20) -> str:
164
+ """Free-text search across node labels, IDs, and properties. Find components by name or keyword."""
165
+ return json.dumps(_svc().search_graph(query, limit=limit), indent=2)
166
+
167
+
168
+ @mcp.tool()
169
+ def read_file(file_path: str) -> str:
170
+ """Read a source file's content for deep analysis. Path is relative to the codebase root."""
171
+ try:
172
+ return _svc().read_file(file_path)
173
+ except ValueError as exc:
174
+ return f"Error: {exc}"
@@ -0,0 +1,16 @@
1
+ """Authentication middleware stub for OSSCodeIQ server."""
2
+ from __future__ import annotations
3
+
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+ from starlette.requests import Request
6
+ from starlette.responses import Response
7
+
8
+
9
+ class AuthMiddleware(BaseHTTPMiddleware):
10
+ """No-op auth middleware. Replace dispatch logic to add authentication."""
11
+
12
+ async def dispatch(self, request: Request, call_next):
13
+ # Future: validate request.headers.get("Authorization")
14
+ # request.state.user = validated_user
15
+ response = await call_next(request)
16
+ return response
@@ -0,0 +1,184 @@
1
+ """REST API routes for OSSCodeIQ server."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, HTTPException, Query
7
+ from fastapi.responses import PlainTextResponse
8
+ from pydantic import BaseModel
9
+
10
+ from osscodeiq.server.service import CodeIQService
11
+
12
+
13
+ class AnalyzeRequest(BaseModel):
14
+ incremental: bool = True
15
+
16
+
17
+ class CypherRequest(BaseModel):
18
+ query: str
19
+ params: dict | None = None
20
+
21
+
22
+ def create_router(service: CodeIQService) -> APIRouter:
23
+ router = APIRouter(prefix="/api", tags=["OSSCodeIQ API"])
24
+
25
+ # ── Stats ────────────────────────────────────────────────────────────
26
+
27
+ @router.get("/stats")
28
+ async def stats():
29
+ return service.get_stats()
30
+
31
+ # ── Nodes & Edges ────────────────────────────────────────────────────
32
+
33
+ @router.get("/nodes")
34
+ async def list_nodes(
35
+ kind: str | None = None,
36
+ limit: Annotated[int, Query(ge=1)] = 100,
37
+ offset: Annotated[int, Query(ge=0)] = 0,
38
+ ):
39
+ return service.list_nodes(kind=kind, limit=limit, offset=offset)
40
+
41
+ @router.get(
42
+ "/nodes/{node_id:path}",
43
+ responses={404: {"description": "Node not found"}},
44
+ )
45
+ async def get_node(node_id: str):
46
+ result = service.get_node(node_id)
47
+ if result is None:
48
+ raise HTTPException(status_code=404, detail=f"Node not found: {node_id}")
49
+ return result
50
+
51
+ @router.get("/edges")
52
+ async def list_edges(
53
+ kind: str | None = None,
54
+ limit: Annotated[int, Query(ge=1)] = 100,
55
+ offset: Annotated[int, Query(ge=0)] = 0,
56
+ ):
57
+ return service.list_edges(kind=kind, limit=limit, offset=offset)
58
+
59
+ # ── Neighbors & Ego ──────────────────────────────────────────────────
60
+
61
+ @router.get("/nodes/{node_id:path}/neighbors")
62
+ async def get_neighbors(
63
+ node_id: str,
64
+ direction: str = "both",
65
+ edge_kinds: str | None = None,
66
+ ):
67
+ kinds = edge_kinds.split(",") if edge_kinds else None
68
+ return service.get_neighbors(node_id, direction=direction, edge_kinds=kinds)
69
+
70
+ @router.get("/ego/{center:path}")
71
+ async def get_ego(
72
+ center: str,
73
+ radius: Annotated[int, Query(ge=1)] = 2,
74
+ edge_kinds: str | None = None,
75
+ ):
76
+ kinds = edge_kinds.split(",") if edge_kinds else None
77
+ return service.get_ego(center, radius=radius, edge_kinds=kinds)
78
+
79
+ # ── Query endpoints ──────────────────────────────────────────────────
80
+
81
+ @router.get("/query/cycles")
82
+ async def find_cycles(limit: Annotated[int, Query(ge=1)] = 100):
83
+ return service.find_cycles(limit=limit)
84
+
85
+ @router.get(
86
+ "/query/shortest-path",
87
+ responses={404: {"description": "No path found between source and target"}},
88
+ )
89
+ async def shortest_path(
90
+ source: Annotated[str, Query()],
91
+ target: Annotated[str, Query()],
92
+ ):
93
+ result = service.shortest_path(source, target)
94
+ if result is None:
95
+ raise HTTPException(
96
+ status_code=404,
97
+ detail=f"No path found between {source} and {target}",
98
+ )
99
+ return result
100
+
101
+ @router.get("/query/consumers/{target_id:path}")
102
+ async def consumers_of(target_id: str):
103
+ return service.consumers_of(target_id)
104
+
105
+ @router.get("/query/producers/{target_id:path}")
106
+ async def producers_of(target_id: str):
107
+ return service.producers_of(target_id)
108
+
109
+ @router.get("/query/callers/{target_id:path}")
110
+ async def callers_of(target_id: str):
111
+ return service.callers_of(target_id)
112
+
113
+ @router.get("/query/dependencies/{module_id:path}")
114
+ async def dependencies_of(module_id: str):
115
+ return service.dependencies_of(module_id)
116
+
117
+ @router.get("/query/dependents/{module_id:path}")
118
+ async def dependents_of(module_id: str):
119
+ return service.dependents_of(module_id)
120
+
121
+ # ── Flow ─────────────────────────────────────────────────────────────
122
+
123
+ @router.get("/flow/{view}")
124
+ async def generate_flow(view: str, fmt: str = "json"):
125
+ return service.generate_flow(view, fmt=fmt)
126
+
127
+ @router.get("/flow")
128
+ async def generate_all_flows():
129
+ return service.generate_all_flows()
130
+
131
+ # ── Analysis ─────────────────────────────────────────────────────────
132
+
133
+ @router.post("/analyze")
134
+ async def analyze(body: AnalyzeRequest):
135
+ return service.run_analysis(body.incremental)
136
+
137
+ # ── Cypher ───────────────────────────────────────────────────────────
138
+
139
+ @router.post(
140
+ "/cypher",
141
+ responses={400: {"description": "Invalid Cypher query or backend unavailable"}},
142
+ )
143
+ async def cypher(body: CypherRequest):
144
+ try:
145
+ return service.query_cypher(body.query, body.params)
146
+ except ValueError as exc:
147
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
148
+
149
+ # ── Triage ───────────────────────────────────────────────────────────
150
+
151
+ @router.get("/triage/component")
152
+ async def find_component(file_path: Annotated[str, Query()]):
153
+ return service.find_component_by_file(file_path)
154
+
155
+ @router.get("/triage/impact/{node_id:path}")
156
+ async def trace_impact(node_id: str, depth: Annotated[int, Query(ge=1)] = 3):
157
+ return service.trace_impact(node_id, depth=depth)
158
+
159
+ @router.get("/triage/endpoints")
160
+ async def find_related_endpoints(identifier: Annotated[str, Query()]):
161
+ return service.find_related_endpoints(identifier)
162
+
163
+ # ── Search ───────────────────────────────────────────────────────────
164
+
165
+ @router.get("/search")
166
+ async def search_graph(
167
+ q: Annotated[str, Query()],
168
+ limit: Annotated[int, Query(ge=1)] = 20,
169
+ ):
170
+ return service.search_graph(q, limit=limit)
171
+
172
+ # ── File ─────────────────────────────────────────────────────────────
173
+
174
+ @router.get("/file")
175
+ async def read_file(path: Annotated[str, Query()]):
176
+ try:
177
+ content = service.read_file(path)
178
+ except ValueError as exc:
179
+ detail = str(exc)
180
+ status = 404 if "not found" in detail.lower() else 400
181
+ raise HTTPException(status_code=status, detail=detail) from exc
182
+ return PlainTextResponse(content)
183
+
184
+ return router