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,445 @@
1
+ """Shared service layer for OSSCodeIQ server.
2
+
3
+ Both REST routes and MCP tools call these methods.
4
+ Every public method returns plain dicts/lists (JSON-serializable).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import threading
10
+ from collections import deque
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from osscodeiq.analyzer import Analyzer, AnalysisResult
15
+ from osscodeiq.cache.store import CacheStore
16
+ from osscodeiq.config import Config
17
+ from osscodeiq.flow.engine import FlowEngine
18
+ from osscodeiq.graph.backends import create_backend
19
+ from osscodeiq.graph.query import GraphQuery
20
+ from osscodeiq.graph.store import GraphStore
21
+ from osscodeiq.models.graph import (
22
+ EdgeKind,
23
+ GraphEdge,
24
+ GraphNode,
25
+ NodeKind,
26
+ SourceLocation,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Serialization helpers
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ def _node_to_dict(node: GraphNode) -> dict:
37
+ """Convert a GraphNode to a JSON-serializable dict."""
38
+ return {
39
+ "id": node.id,
40
+ "kind": node.kind.value,
41
+ "label": node.label,
42
+ "fqn": node.fqn,
43
+ "module": node.module,
44
+ "location": (
45
+ {
46
+ "file_path": node.location.file_path,
47
+ "line_start": node.location.line_start,
48
+ "line_end": node.location.line_end,
49
+ }
50
+ if node.location
51
+ else None
52
+ ),
53
+ "annotations": node.annotations,
54
+ "properties": node.properties,
55
+ }
56
+
57
+
58
+ def _edge_to_dict(edge: GraphEdge) -> dict:
59
+ """Convert a GraphEdge to a JSON-serializable dict."""
60
+ return {
61
+ "source": edge.source,
62
+ "target": edge.target,
63
+ "kind": edge.kind.value,
64
+ "label": edge.label,
65
+ "properties": edge.properties,
66
+ }
67
+
68
+
69
+ def _store_to_dict(store: GraphStore) -> dict:
70
+ """Convert a GraphStore to a JSON-serializable dict with sorted output."""
71
+ return {
72
+ "nodes": [
73
+ _node_to_dict(n)
74
+ for n in sorted(store.all_nodes(), key=lambda n: n.id)
75
+ ],
76
+ "edges": [
77
+ _edge_to_dict(e)
78
+ for e in sorted(store.all_edges(), key=lambda e: (e.source, e.target))
79
+ ],
80
+ }
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Service
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class CodeIQService:
89
+ """Stateful service wrapping the code-intelligence library.
90
+
91
+ Thread-safe: analysis replaces the internal store under a lock;
92
+ read operations are safe against a snapshot reference.
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ path: Path,
98
+ backend: str = "networkx",
99
+ config_path: Path | None = None,
100
+ ) -> None:
101
+ self._path = path.resolve()
102
+ self._backend_name = backend
103
+ self._config = self._load_config(config_path)
104
+ self._store: GraphStore | None = None
105
+ self._lock = threading.Lock()
106
+
107
+ # -- internal helpers ------------------------------------------------
108
+
109
+ @staticmethod
110
+ def _load_config(config_path: Path | None) -> Config:
111
+ if config_path and config_path.exists():
112
+ return Config.load(config_path=config_path)
113
+ return Config()
114
+
115
+ @property
116
+ def store(self) -> GraphStore:
117
+ """Lazy-load and return the graph store."""
118
+ if self._store is None:
119
+ self._store = self._open_store()
120
+ return self._store
121
+
122
+ def _open_store(self) -> GraphStore:
123
+ """Open or create a GraphStore for the configured backend."""
124
+ graph_dir = self._path / ".code-intelligence"
125
+ if self._backend_name == "kuzu":
126
+ db_path = str(graph_dir / "graph.kuzu")
127
+ backend_obj = create_backend("kuzu", path=db_path)
128
+ return GraphStore(backend=backend_obj)
129
+ elif self._backend_name == "sqlite":
130
+ db_path = str(graph_dir / "graph.db")
131
+ backend_obj = create_backend("sqlite", path=db_path)
132
+ return GraphStore(backend=backend_obj)
133
+ else:
134
+ # NetworkX — load from cache
135
+ cache_path = (
136
+ self._path
137
+ / self._config.cache.directory
138
+ / self._config.cache.db_name
139
+ )
140
+ if not cache_path.exists():
141
+ return GraphStore() # empty graph for server
142
+ cache = CacheStore(cache_path)
143
+ return cache.load_full_graph()
144
+
145
+ # -- public API (all return dicts/lists) ----------------------------
146
+
147
+ def get_stats(self) -> dict:
148
+ """Return high-level graph statistics."""
149
+ model = self.store.to_model()
150
+ stats: dict[str, Any] = dict(model.metadata.get("stats", {}))
151
+ stats["backend"] = self._backend_name
152
+ stats["codebase_path"] = str(self._path)
153
+ return stats
154
+
155
+ def list_nodes(
156
+ self,
157
+ kind: str | None = None,
158
+ limit: int = 100,
159
+ offset: int = 0,
160
+ ) -> list[dict]:
161
+ """List nodes, optionally filtered by kind, with pagination."""
162
+ if kind is not None:
163
+ nodes = self.store.nodes_by_kind(NodeKind(kind))
164
+ else:
165
+ nodes = self.store.all_nodes()
166
+ nodes = sorted(nodes, key=lambda n: n.id)
167
+ return [_node_to_dict(n) for n in nodes[offset : offset + limit]]
168
+
169
+ def list_edges(
170
+ self,
171
+ kind: str | None = None,
172
+ limit: int = 100,
173
+ offset: int = 0,
174
+ ) -> list[dict]:
175
+ """List edges, optionally filtered by kind, with pagination."""
176
+ if kind is not None:
177
+ edges = self.store.edges_by_kind(EdgeKind(kind))
178
+ else:
179
+ edges = self.store.all_edges()
180
+ edges = sorted(edges, key=lambda e: (e.source, e.target))
181
+ return [_edge_to_dict(e) for e in edges[offset : offset + limit]]
182
+
183
+ def get_node(self, node_id: str) -> dict | None:
184
+ """Return a single node by id, or None."""
185
+ node = self.store.get_node(node_id)
186
+ if node is None:
187
+ return None
188
+ return _node_to_dict(node)
189
+
190
+ def get_neighbors(
191
+ self,
192
+ node_id: str,
193
+ direction: str = "both",
194
+ edge_kinds: list[str] | None = None,
195
+ ) -> list[dict]:
196
+ """Return neighbor nodes of *node_id*."""
197
+ ek: set[EdgeKind] | None = None
198
+ if edge_kinds:
199
+ ek = {EdgeKind(k) for k in edge_kinds}
200
+ neighbor_ids = self.store.neighbors(node_id, edge_kinds=ek, direction=direction)
201
+ result: list[dict] = []
202
+ for nid in sorted(neighbor_ids):
203
+ node = self.store.get_node(nid)
204
+ if node is not None:
205
+ result.append(_node_to_dict(node))
206
+ return result
207
+
208
+ def get_ego(
209
+ self,
210
+ center: str,
211
+ radius: int = 2,
212
+ edge_kinds: list[str] | None = None,
213
+ ) -> dict:
214
+ """Return the ego subgraph around *center*."""
215
+ ek: set[EdgeKind] | None = None
216
+ if edge_kinds:
217
+ ek = {EdgeKind(k) for k in edge_kinds}
218
+ ego_store = self.store.ego(center, radius=radius, edge_kinds=ek)
219
+ return _store_to_dict(ego_store)
220
+
221
+ def find_cycles(self, limit: int = 100) -> list[list[str]]:
222
+ """Return cycles in the graph (up to *limit*)."""
223
+ return self.store.find_cycles(limit)
224
+
225
+ def shortest_path(self, source: str, target: str) -> list[str] | None:
226
+ """Return shortest path between two nodes, or None."""
227
+ try:
228
+ return self.store.shortest_path(source, target)
229
+ except Exception:
230
+ return None
231
+
232
+ def consumers_of(self, target_id: str) -> dict:
233
+ """Find nodes that consume from *target_id*."""
234
+ result = GraphQuery(self.store).consumers_of(target_id).execute()
235
+ return _store_to_dict(result)
236
+
237
+ def producers_of(self, target_id: str) -> dict:
238
+ """Find nodes that produce to *target_id*."""
239
+ result = GraphQuery(self.store).producers_of(target_id).execute()
240
+ return _store_to_dict(result)
241
+
242
+ def callers_of(self, target_id: str) -> dict:
243
+ """Find nodes that call *target_id*."""
244
+ result = GraphQuery(self.store).callers_of(target_id).execute()
245
+ return _store_to_dict(result)
246
+
247
+ def dependencies_of(self, module_id: str) -> dict:
248
+ """Find modules that *module_id* depends on."""
249
+ result = GraphQuery(self.store).dependencies_of(module_id).execute()
250
+ return _store_to_dict(result)
251
+
252
+ def dependents_of(self, module_id: str) -> dict:
253
+ """Find modules that depend on *module_id*."""
254
+ result = GraphQuery(self.store).dependents_of(module_id).execute()
255
+ return _store_to_dict(result)
256
+
257
+ def generate_flow(
258
+ self, view: str = "overview", fmt: str = "json"
259
+ ) -> dict | str:
260
+ """Generate a flow diagram for the given view and format."""
261
+ engine = FlowEngine(self.store)
262
+ diagram = engine.generate(view)
263
+ if fmt == "json":
264
+ return diagram.to_dict()
265
+ return engine.render(diagram, fmt)
266
+
267
+ def generate_all_flows(self) -> dict:
268
+ """Generate all flow diagrams as JSON dicts."""
269
+ engine = FlowEngine(self.store)
270
+ return {
271
+ name: diagram.to_dict()
272
+ for name, diagram in engine.generate_all().items()
273
+ }
274
+
275
+ def run_analysis(self, incremental: bool = True) -> dict:
276
+ """Run the analysis pipeline and replace the in-memory store."""
277
+ with self._lock:
278
+ analyzer = Analyzer(self._config)
279
+ result: AnalysisResult = analyzer.run(self._path, incremental=incremental)
280
+ self._store = result.graph
281
+ return self.get_stats()
282
+
283
+ def query_cypher(
284
+ self, query: str, params: dict | None = None
285
+ ) -> list[dict]:
286
+ """Execute a Cypher query against the graph backend."""
287
+ if not self.store.supports_cypher:
288
+ raise ValueError(
289
+ f"Backend '{self._backend_name}' does not support Cypher queries. "
290
+ "Use kuzu or another Cypher-capable backend."
291
+ )
292
+ return self.store.query_cypher(query, params)
293
+
294
+ def find_component_by_file(self, file_path: str) -> dict:
295
+ """Find all graph components defined in a source file."""
296
+ matching_nodes: list[GraphNode] = []
297
+ for node in sorted(self.store.all_nodes(), key=lambda n: n.id):
298
+ if (
299
+ node.location
300
+ and node.location.file_path
301
+ and (
302
+ node.location.file_path.endswith(file_path)
303
+ or file_path in node.location.file_path
304
+ )
305
+ ):
306
+ matching_nodes.append(node)
307
+
308
+ components: list[dict] = []
309
+ for node in matching_nodes:
310
+ neighbor_ids = self.store.neighbors(node.id)
311
+ neighbors = []
312
+ for nid in sorted(neighbor_ids):
313
+ nb = self.store.get_node(nid)
314
+ if nb is not None:
315
+ neighbors.append(_node_to_dict(nb))
316
+ components.append({
317
+ "node": _node_to_dict(node),
318
+ "neighbors": neighbors,
319
+ })
320
+
321
+ return {"file": file_path, "components": components}
322
+
323
+ def trace_impact(self, node_id: str, depth: int = 3) -> dict:
324
+ """BFS impact trace from *node_id* following outgoing edges."""
325
+ propagation_kinds = {
326
+ EdgeKind.DEPENDS_ON,
327
+ EdgeKind.IMPORTS,
328
+ EdgeKind.CALLS,
329
+ EdgeKind.QUERIES,
330
+ EdgeKind.CONNECTS_TO,
331
+ }
332
+
333
+ depth = min(depth, 10)
334
+ visited: set[str] = {node_id}
335
+ frontier: set[str] = {node_id}
336
+ impacted_nodes: list[GraphNode] = []
337
+ relevant_edges: list[GraphEdge] = []
338
+
339
+ for _ in range(depth):
340
+ next_frontier: set[str] = set()
341
+ for current_id in sorted(frontier):
342
+ for edge in self.store.all_edges():
343
+ if (
344
+ edge.source == current_id
345
+ and edge.kind in propagation_kinds
346
+ and edge.target not in visited
347
+ ):
348
+ visited.add(edge.target)
349
+ next_frontier.add(edge.target)
350
+ relevant_edges.append(edge)
351
+ target_node = self.store.get_node(edge.target)
352
+ if target_node is not None:
353
+ impacted_nodes.append(target_node)
354
+ frontier = next_frontier
355
+ if not frontier:
356
+ break
357
+
358
+ return {
359
+ "root": node_id,
360
+ "depth": depth,
361
+ "impacted": [
362
+ _node_to_dict(n)
363
+ for n in sorted(impacted_nodes, key=lambda n: n.id)
364
+ ],
365
+ "edges": [
366
+ _edge_to_dict(e)
367
+ for e in sorted(relevant_edges, key=lambda e: (e.source, e.target))
368
+ ],
369
+ }
370
+
371
+ def find_related_endpoints(self, identifier: str) -> list[dict]:
372
+ """Find ENDPOINT nodes reachable (up to 3 hops) from matching nodes."""
373
+ identifier_lower = identifier.lower()
374
+
375
+ # Find seed nodes matching the identifier
376
+ seed_ids: set[str] = set()
377
+ for node in self.store.all_nodes():
378
+ if (
379
+ identifier_lower in node.id.lower()
380
+ or identifier_lower in node.label.lower()
381
+ or (node.fqn and identifier_lower in node.fqn.lower())
382
+ ):
383
+ seed_ids.add(node.id)
384
+
385
+ # BFS up to 3 hops to find ENDPOINT nodes
386
+ visited: set[str] = set(seed_ids)
387
+ frontier: set[str] = set(seed_ids)
388
+ endpoints: dict[str, GraphNode] = {} # deduplicate by id
389
+
390
+ for _ in range(3):
391
+ next_frontier: set[str] = set()
392
+ for nid in sorted(frontier):
393
+ for neighbor_id in self.store.neighbors(nid):
394
+ if neighbor_id not in visited:
395
+ visited.add(neighbor_id)
396
+ next_frontier.add(neighbor_id)
397
+ nb = self.store.get_node(neighbor_id)
398
+ if nb is not None and nb.kind == NodeKind.ENDPOINT:
399
+ endpoints[nb.id] = nb
400
+ frontier = next_frontier
401
+ if not frontier:
402
+ break
403
+
404
+ # Also check seed nodes themselves
405
+ for sid in seed_ids:
406
+ node = self.store.get_node(sid)
407
+ if node is not None and node.kind == NodeKind.ENDPOINT:
408
+ endpoints[node.id] = node
409
+
410
+ return [
411
+ _node_to_dict(n)
412
+ for n in sorted(endpoints.values(), key=lambda n: n.id)
413
+ ]
414
+
415
+ def search_graph(self, query: str, limit: int = 20) -> list[dict]:
416
+ """Case-insensitive substring search across node fields."""
417
+ query_lower = query.lower()
418
+ matches: list[GraphNode] = []
419
+
420
+ for node in self.store.all_nodes():
421
+ if (
422
+ query_lower in node.id.lower()
423
+ or query_lower in node.label.lower()
424
+ or (node.fqn and query_lower in node.fqn.lower())
425
+ or (node.module and query_lower in node.module.lower())
426
+ or any(
427
+ query_lower in str(v).lower()
428
+ for v in node.properties.values()
429
+ )
430
+ ):
431
+ matches.append(node)
432
+
433
+ matches.sort(key=lambda n: n.label)
434
+ return [_node_to_dict(n) for n in matches[:limit]]
435
+
436
+ def read_file(self, file_path: str) -> str:
437
+ """Read a file from the codebase, preventing path traversal."""
438
+ resolved = (self._path / file_path).resolve()
439
+ if not str(resolved).startswith(str(self._path)):
440
+ raise ValueError(
441
+ f"Path '{file_path}' resolves outside the codebase root."
442
+ )
443
+ if not resolved.is_file():
444
+ raise ValueError(f"File not found: {file_path}")
445
+ return resolved.read_text(encoding="utf-8", errors="replace")
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>OSSCodeIQ</title>
7
+ <style>
8
+ :root { --bg:#0a0a0f; --surface:#111118; --border:#25253a; --text:#e4e4ed; --muted:#8888a0; --dim:#55556a; --accent:#6366f1; }
9
+ * { margin:0; padding:0; box-sizing:border-box; }
10
+ body { font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; display:flex; align-items:center; justify-content:center; }
11
+ .card { max-width:480px; width:100%; padding:40px; }
12
+ .logo { width:48px; height:48px; border-radius:12px; background:var(--accent); display:flex; align-items:center; justify-content:center; color:#fff; font-weight:700; font-size:18px; margin-bottom:20px; }
13
+ h1 { font-size:24px; font-weight:700; margin-bottom:4px; }
14
+ .sub { color:var(--muted); font-size:14px; margin-bottom:28px; }
15
+ .stats { display:flex; gap:12px; margin-bottom:28px; flex-wrap:wrap; }
16
+ .stat { padding:10px 16px; border-radius:8px; background:var(--surface); border:1px solid var(--border); flex:1; min-width:100px; }
17
+ .stat-val { font-size:20px; font-weight:700; color:var(--accent); }
18
+ .stat-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.05em; margin-top:2px; }
19
+ .links { display:flex; flex-direction:column; gap:8px; }
20
+ .link { display:flex; align-items:center; justify-content:space-between; padding:10px 14px; border-radius:8px; background:var(--surface); border:1px solid var(--border); color:var(--text); text-decoration:none; font-size:13px; transition:border-color 0.15s; }
21
+ .link:hover { border-color:var(--accent); }
22
+ .link-path { color:var(--muted); font-family:monospace; font-size:12px; }
23
+ .loading { color:var(--dim); font-size:13px; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div class="card">
28
+ <div class="logo">IQ</div>
29
+ <h1>OSSCodeIQ</h1>
30
+ <p class="sub">OSSCodeIQ Server</p>
31
+ <div class="stats" id="stats"><span class="loading">Loading stats...</span></div>
32
+ <div class="links">
33
+ <a class="link" href="/docs"><span>API Documentation</span><span class="link-path">/docs</span></a>
34
+ <a class="link" href="/api/stats"><span>API Stats</span><span class="link-path">/api/stats</span></a>
35
+ <a class="link" href="/api/nodes"><span>Browse Nodes</span><span class="link-path">/api/nodes</span></a>
36
+ <a class="link" href="/api/flow"><span>Flow Diagrams</span><span class="link-path">/api/flow</span></a>
37
+ <a class="link" href="/mcp"><span>MCP Endpoint</span><span class="link-path">/mcp</span></a>
38
+ </div>
39
+ </div>
40
+ <script>
41
+ fetch("/api/stats").then(function(r){return r.json()}).then(function(d){
42
+ var el=document.getElementById("stats");
43
+ while(el.firstChild) el.firstChild.remove();
44
+ var items=[{v:d.total_nodes||0,l:"Nodes"},{v:d.total_edges||0,l:"Edges"},{v:d.backend||"unknown",l:"Backend"}];
45
+ items.forEach(function(s){
46
+ var div=document.createElement("div");div.className="stat";
47
+ var val=document.createElement("div");val.className="stat-val";val.textContent=typeof s.v==="number"?s.v.toLocaleString():s.v;
48
+ var lab=document.createElement("div");lab.className="stat-label";lab.textContent=s.l;
49
+ div.appendChild(val);div.appendChild(lab);el.appendChild(div);
50
+ });
51
+ }).catch(function(){
52
+ document.getElementById("stats").textContent="No analysis data. Run: osscodeiq analyze";
53
+ });
54
+ </script>
55
+ </body>
56
+ </html>
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: osscodeiq
3
+ Version: 0.0.0
4
+ Summary: CLI tool for intelligent code graph discovery and analysis
5
+ Requires-Python: >=3.11
6
+ License-File: LICENSE
7
+ Requires-Dist: typer>=0.9
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: tree-sitter>=0.23
10
+ Requires-Dist: tree-sitter-java>=0.23
11
+ Requires-Dist: tree-sitter-python>=0.23
12
+ Requires-Dist: tree-sitter-typescript>=0.23
13
+ Requires-Dist: tree-sitter-javascript>=0.23
14
+ Requires-Dist: networkx>=3.2
15
+ Requires-Dist: lxml>=5.0
16
+ Requires-Dist: pyyaml>=6.0
17
+ Requires-Dist: sqlparse>=0.5
18
+ Requires-Dist: pydantic>=2.0
19
+ Requires-Dist: pathspec>=0.11
20
+ Requires-Dist: fastapi>=0.115
21
+ Requires-Dist: uvicorn[standard]>=0.34
22
+ Requires-Dist: fastmcp>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
26
+ Provides-Extra: kuzu
27
+ Requires-Dist: kuzu>=0.6; extra == "kuzu"
28
+ Provides-Extra: all-backends
29
+ Requires-Dist: kuzu>=0.6; extra == "all-backends"
30
+ Dynamic: license-file