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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|