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
osscodeiq/graph/store.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Graph store facade delegating to a pluggable backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from osscodeiq.graph.backend import CypherBackend, GraphBackend
|
|
11
|
+
from osscodeiq.models.graph import (
|
|
12
|
+
CodeGraph,
|
|
13
|
+
EdgeKind,
|
|
14
|
+
GraphEdge,
|
|
15
|
+
GraphNode,
|
|
16
|
+
NodeKind,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GraphStore:
|
|
23
|
+
"""Public API for graph operations. Delegates to a pluggable backend."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, backend: GraphBackend | None = None) -> None:
|
|
26
|
+
if backend is None:
|
|
27
|
+
from osscodeiq.graph.backends.networkx import NetworkXBackend
|
|
28
|
+
backend = NetworkXBackend()
|
|
29
|
+
self._backend: GraphBackend = backend
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def graph(self) -> Any:
|
|
33
|
+
"""Deprecated. Direct backend access; only works with NetworkXBackend."""
|
|
34
|
+
warnings.warn(
|
|
35
|
+
"GraphStore.graph is deprecated. Use public API methods instead.",
|
|
36
|
+
DeprecationWarning,
|
|
37
|
+
stacklevel=2,
|
|
38
|
+
)
|
|
39
|
+
if hasattr(self._backend, "_g"):
|
|
40
|
+
return self._backend._g
|
|
41
|
+
raise AttributeError("Backend does not expose raw graph object")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def node_count(self) -> int:
|
|
45
|
+
return self._backend.node_count
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def edge_count(self) -> int:
|
|
49
|
+
return self._backend.edge_count
|
|
50
|
+
|
|
51
|
+
def add_node(self, node: GraphNode) -> None:
|
|
52
|
+
self._backend.add_node(node)
|
|
53
|
+
|
|
54
|
+
def add_edge(self, edge: GraphEdge) -> None:
|
|
55
|
+
self._backend.add_edge(edge)
|
|
56
|
+
|
|
57
|
+
def get_node(self, node_id: str) -> GraphNode | None:
|
|
58
|
+
return self._backend.get_node(node_id)
|
|
59
|
+
|
|
60
|
+
def get_edges_between(self, source: str, target: str) -> list[GraphEdge]:
|
|
61
|
+
return self._backend.get_edges_between(source, target)
|
|
62
|
+
|
|
63
|
+
def all_nodes(self) -> list[GraphNode]:
|
|
64
|
+
return self._backend.all_nodes()
|
|
65
|
+
|
|
66
|
+
def all_edges(self) -> list[GraphEdge]:
|
|
67
|
+
return self._backend.all_edges()
|
|
68
|
+
|
|
69
|
+
def nodes_by_kind(self, kind: NodeKind) -> list[GraphNode]:
|
|
70
|
+
return self._backend.nodes_by_kind(kind)
|
|
71
|
+
|
|
72
|
+
def edges_by_kind(self, kind: EdgeKind) -> list[GraphEdge]:
|
|
73
|
+
return self._backend.edges_by_kind(kind)
|
|
74
|
+
|
|
75
|
+
def neighbors(
|
|
76
|
+
self,
|
|
77
|
+
node_id: str,
|
|
78
|
+
edge_kinds: set[EdgeKind] | None = None,
|
|
79
|
+
direction: str = "both",
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
return self._backend.neighbors(node_id, edge_kinds, direction)
|
|
82
|
+
|
|
83
|
+
def subgraph(self, node_ids: set[str]) -> GraphStore:
|
|
84
|
+
return GraphStore(backend=self._backend.subgraph(node_ids))
|
|
85
|
+
|
|
86
|
+
def ego(
|
|
87
|
+
self,
|
|
88
|
+
center: str,
|
|
89
|
+
radius: int = 2,
|
|
90
|
+
edge_kinds: set[EdgeKind] | None = None,
|
|
91
|
+
) -> GraphStore:
|
|
92
|
+
if not self._backend.has_node(center):
|
|
93
|
+
return GraphStore(backend=type(self._backend)() if callable(type(self._backend)) else None)
|
|
94
|
+
|
|
95
|
+
radius = min(radius, 10)
|
|
96
|
+
visited: set[str] = {center}
|
|
97
|
+
frontier: set[str] = {center}
|
|
98
|
+
|
|
99
|
+
for _ in range(radius):
|
|
100
|
+
next_frontier: set[str] = set()
|
|
101
|
+
for node_id in frontier:
|
|
102
|
+
next_frontier.update(
|
|
103
|
+
n for n in self.neighbors(node_id, edge_kinds, "both")
|
|
104
|
+
if n not in visited
|
|
105
|
+
)
|
|
106
|
+
visited.update(next_frontier)
|
|
107
|
+
frontier = next_frontier
|
|
108
|
+
|
|
109
|
+
return self.subgraph(visited)
|
|
110
|
+
|
|
111
|
+
def filter(
|
|
112
|
+
self,
|
|
113
|
+
node_filter: Callable[[GraphNode], bool] | None = None,
|
|
114
|
+
edge_filter: Callable[[GraphEdge], bool] | None = None,
|
|
115
|
+
) -> GraphStore:
|
|
116
|
+
new_store = GraphStore() # Always uses NetworkX for filtered results
|
|
117
|
+
|
|
118
|
+
for node in self.all_nodes():
|
|
119
|
+
if node_filter is None or node_filter(node):
|
|
120
|
+
new_store.add_node(node)
|
|
121
|
+
|
|
122
|
+
for edge in self.all_edges():
|
|
123
|
+
if new_store._backend.has_node(edge.source) and new_store._backend.has_node(edge.target):
|
|
124
|
+
if edge_filter is None or edge_filter(edge):
|
|
125
|
+
new_store.add_edge(edge)
|
|
126
|
+
|
|
127
|
+
return new_store
|
|
128
|
+
|
|
129
|
+
def find_cycles(self, limit: int = 100) -> list[list[str]]:
|
|
130
|
+
return self._backend.find_cycles(limit)
|
|
131
|
+
|
|
132
|
+
def shortest_path(self, source: str, target: str) -> list[str] | None:
|
|
133
|
+
return self._backend.shortest_path(source, target)
|
|
134
|
+
|
|
135
|
+
def update_node_properties(self, node_id: str, properties: dict[str, Any]) -> None:
|
|
136
|
+
self._backend.update_node_properties(node_id, properties)
|
|
137
|
+
|
|
138
|
+
def to_model(self) -> CodeGraph:
|
|
139
|
+
nodes = self.all_nodes()
|
|
140
|
+
edges = self.all_edges()
|
|
141
|
+
node_counts: dict[str, int] = {}
|
|
142
|
+
for n in nodes:
|
|
143
|
+
k = n.kind.value
|
|
144
|
+
node_counts[k] = node_counts.get(k, 0) + 1
|
|
145
|
+
edge_counts: dict[str, int] = {}
|
|
146
|
+
for e in edges:
|
|
147
|
+
k = e.kind.value
|
|
148
|
+
edge_counts[k] = edge_counts.get(k, 0) + 1
|
|
149
|
+
|
|
150
|
+
return CodeGraph(
|
|
151
|
+
nodes=nodes,
|
|
152
|
+
edges=edges,
|
|
153
|
+
metadata={
|
|
154
|
+
"stats": {
|
|
155
|
+
"total_nodes": len(nodes),
|
|
156
|
+
"total_edges": len(edges),
|
|
157
|
+
"node_counts_by_kind": node_counts,
|
|
158
|
+
"edge_counts_by_kind": edge_counts,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def from_model(self, code_graph: CodeGraph) -> None:
|
|
164
|
+
self._backend.clear()
|
|
165
|
+
for node in code_graph.nodes:
|
|
166
|
+
self._backend.add_node(node)
|
|
167
|
+
for edge in code_graph.edges:
|
|
168
|
+
self._backend.add_edge(edge)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def supports_cypher(self) -> bool:
|
|
172
|
+
return isinstance(self._backend, CypherBackend)
|
|
173
|
+
|
|
174
|
+
def query_cypher(self, cypher: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
|
175
|
+
if not isinstance(self._backend, CypherBackend):
|
|
176
|
+
raise NotImplementedError(
|
|
177
|
+
f"Backend {type(self._backend).__name__} does not support Cypher. "
|
|
178
|
+
"Use kuzu, neo4j, or age backend."
|
|
179
|
+
)
|
|
180
|
+
return self._backend.query_cypher(cypher, params)
|
|
181
|
+
|
|
182
|
+
def close(self) -> None:
|
|
183
|
+
self._backend.close()
|
osscodeiq/graph/views.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Multi-level view transformations for the OSSCodeIQ graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from osscodeiq.config import DomainMapping
|
|
8
|
+
from osscodeiq.graph.store import GraphStore
|
|
9
|
+
from osscodeiq.models.graph import (
|
|
10
|
+
EdgeKind,
|
|
11
|
+
GraphEdge,
|
|
12
|
+
GraphNode,
|
|
13
|
+
NodeKind,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ArchitectView:
|
|
18
|
+
"""Collapses detail nodes into module-level nodes.
|
|
19
|
+
|
|
20
|
+
Method-level calls, imports, and injections are rolled up into
|
|
21
|
+
module-level ``depends_on`` edges. Messaging edges (produces,
|
|
22
|
+
consumes, publishes, listens) preserve their identity so that
|
|
23
|
+
data-flow remains visible at the architecture level.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
EDGE_ROLLUP: dict[EdgeKind, EdgeKind] = {
|
|
27
|
+
EdgeKind.CALLS: EdgeKind.DEPENDS_ON,
|
|
28
|
+
EdgeKind.IMPORTS: EdgeKind.DEPENDS_ON,
|
|
29
|
+
EdgeKind.INJECTS: EdgeKind.DEPENDS_ON,
|
|
30
|
+
EdgeKind.EXTENDS: EdgeKind.DEPENDS_ON,
|
|
31
|
+
EdgeKind.IMPLEMENTS: EdgeKind.DEPENDS_ON,
|
|
32
|
+
EdgeKind.PRODUCES: EdgeKind.PRODUCES,
|
|
33
|
+
EdgeKind.CONSUMES: EdgeKind.CONSUMES,
|
|
34
|
+
EdgeKind.PUBLISHES: EdgeKind.PUBLISHES,
|
|
35
|
+
EdgeKind.LISTENS: EdgeKind.LISTENS,
|
|
36
|
+
EdgeKind.INVOKES_RMI: EdgeKind.INVOKES_RMI,
|
|
37
|
+
EdgeKind.EXPORTS_RMI: EdgeKind.EXPORTS_RMI,
|
|
38
|
+
EdgeKind.DEPENDS_ON: EdgeKind.DEPENDS_ON,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def _resolve_module(self, node: GraphNode) -> str | None:
|
|
42
|
+
"""Return the module id that owns *node*.
|
|
43
|
+
|
|
44
|
+
MODULE nodes own themselves; every other node uses its
|
|
45
|
+
``module`` property.
|
|
46
|
+
"""
|
|
47
|
+
if node.kind == NodeKind.MODULE:
|
|
48
|
+
return node.id
|
|
49
|
+
return node.module
|
|
50
|
+
|
|
51
|
+
def roll_up(self, store: GraphStore) -> GraphStore:
|
|
52
|
+
"""Collapse all non-module nodes into their owning module.
|
|
53
|
+
|
|
54
|
+
Returns a new :class:`GraphStore` containing only MODULE nodes
|
|
55
|
+
with rolled-up edges and summary properties.
|
|
56
|
+
"""
|
|
57
|
+
new_store = GraphStore()
|
|
58
|
+
|
|
59
|
+
# --- 1. Collect module nodes and build summary counters --------
|
|
60
|
+
module_nodes: dict[str, GraphNode] = {}
|
|
61
|
+
summary: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
|
62
|
+
|
|
63
|
+
for node in store.all_nodes():
|
|
64
|
+
mod_id = self._resolve_module(node)
|
|
65
|
+
if mod_id is None:
|
|
66
|
+
continue
|
|
67
|
+
# Ensure we have a MODULE node entry
|
|
68
|
+
if mod_id not in module_nodes:
|
|
69
|
+
existing = store.get_node(mod_id)
|
|
70
|
+
if existing is not None and existing.kind == NodeKind.MODULE:
|
|
71
|
+
module_nodes[mod_id] = existing
|
|
72
|
+
else:
|
|
73
|
+
# Synthesise a module node when one is not present
|
|
74
|
+
module_nodes[mod_id] = GraphNode(
|
|
75
|
+
id=mod_id,
|
|
76
|
+
kind=NodeKind.MODULE,
|
|
77
|
+
label=mod_id,
|
|
78
|
+
)
|
|
79
|
+
# Count child node kinds for summary
|
|
80
|
+
summary[mod_id][node.kind.value] += 1
|
|
81
|
+
|
|
82
|
+
# Add module nodes with summary properties
|
|
83
|
+
for mod_id, mod_node in module_nodes.items():
|
|
84
|
+
counts = dict(summary.get(mod_id, {}))
|
|
85
|
+
props = dict(mod_node.properties)
|
|
86
|
+
props["endpoint_count"] = counts.get(NodeKind.ENDPOINT.value, 0)
|
|
87
|
+
props["entity_count"] = counts.get(NodeKind.ENTITY.value, 0)
|
|
88
|
+
props["class_count"] = counts.get(NodeKind.CLASS.value, 0)
|
|
89
|
+
props["method_count"] = counts.get(NodeKind.METHOD.value, 0)
|
|
90
|
+
enriched = mod_node.model_copy(update={"properties": props})
|
|
91
|
+
new_store.add_node(enriched)
|
|
92
|
+
|
|
93
|
+
# --- 2. Roll up edges -----------------------------------------
|
|
94
|
+
# Pre-build node_id -> module_id mapping via public API
|
|
95
|
+
module_map: dict[str, str | None] = {}
|
|
96
|
+
for node in store.all_nodes():
|
|
97
|
+
module_map[node.id] = node.module
|
|
98
|
+
|
|
99
|
+
# (source_module, target_module, rolled_kind) -> weight
|
|
100
|
+
edge_weights: dict[tuple[str, str, EdgeKind], int] = defaultdict(int)
|
|
101
|
+
|
|
102
|
+
for edge in store.all_edges():
|
|
103
|
+
rolled_kind = self.EDGE_ROLLUP.get(edge.kind)
|
|
104
|
+
if rolled_kind is None:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
src_mod = module_map.get(edge.source)
|
|
108
|
+
tgt_mod = module_map.get(edge.target)
|
|
109
|
+
if src_mod is None or tgt_mod is None:
|
|
110
|
+
continue
|
|
111
|
+
# Skip self-loops at module level
|
|
112
|
+
if src_mod == tgt_mod:
|
|
113
|
+
continue
|
|
114
|
+
# Ensure both modules exist in new store
|
|
115
|
+
if src_mod not in module_nodes or tgt_mod not in module_nodes:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
edge_weights[(src_mod, tgt_mod, rolled_kind)] += 1
|
|
119
|
+
|
|
120
|
+
# --- 3. Create merged edges -----------------------------------
|
|
121
|
+
for (src, tgt, kind), weight in edge_weights.items():
|
|
122
|
+
props: dict[str, object] = {"weight": weight}
|
|
123
|
+
new_store.add_edge(
|
|
124
|
+
GraphEdge(
|
|
125
|
+
source=src,
|
|
126
|
+
target=tgt,
|
|
127
|
+
kind=kind,
|
|
128
|
+
label=f"{kind.value} (x{weight})" if weight > 1 else kind.value,
|
|
129
|
+
properties=props,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return new_store
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DomainView:
|
|
137
|
+
"""Collapses modules into business domain groups.
|
|
138
|
+
|
|
139
|
+
Uses :class:`DomainMapping` definitions from the project
|
|
140
|
+
configuration to merge module-level nodes into domain-level
|
|
141
|
+
aggregates.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, domain_mappings: list[DomainMapping]) -> None:
|
|
145
|
+
self._mappings = domain_mappings
|
|
146
|
+
# Pre-build module -> domain lookup
|
|
147
|
+
self._module_to_domain: dict[str, str] = {}
|
|
148
|
+
for mapping in domain_mappings:
|
|
149
|
+
for module in mapping.modules:
|
|
150
|
+
self._module_to_domain[module] = mapping.name
|
|
151
|
+
|
|
152
|
+
def _resolve_domain(self, module_id: str) -> str | None:
|
|
153
|
+
"""Return the domain name for a module, or ``None`` if unmapped."""
|
|
154
|
+
# Exact match first
|
|
155
|
+
if module_id in self._module_to_domain:
|
|
156
|
+
return self._module_to_domain[module_id]
|
|
157
|
+
# Prefix match (e.g. ``com.example.orders`` matches ``com.example.orders.service``)
|
|
158
|
+
for mod_prefix, domain in self._module_to_domain.items():
|
|
159
|
+
if module_id.startswith(mod_prefix + ".") or module_id.startswith(mod_prefix + "/"):
|
|
160
|
+
return domain
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def roll_up(self, store: GraphStore) -> GraphStore:
|
|
164
|
+
"""Collapse module-level nodes into domain-level aggregates.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
store:
|
|
169
|
+
A :class:`GraphStore` — ideally already at module level
|
|
170
|
+
(i.e. output of :meth:`ArchitectView.roll_up`).
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
GraphStore
|
|
175
|
+
A new store with one node per domain and rolled-up edges.
|
|
176
|
+
"""
|
|
177
|
+
new_store = GraphStore()
|
|
178
|
+
|
|
179
|
+
# --- 1. Build domain nodes ------------------------------------
|
|
180
|
+
domain_modules: dict[str, list[str]] = defaultdict(list)
|
|
181
|
+
|
|
182
|
+
for node in store.all_nodes():
|
|
183
|
+
domain = self._resolve_domain(node.id)
|
|
184
|
+
if domain is None:
|
|
185
|
+
# Keep unmapped nodes as-is
|
|
186
|
+
new_store.add_node(node)
|
|
187
|
+
continue
|
|
188
|
+
domain_modules[domain].append(node.id)
|
|
189
|
+
|
|
190
|
+
for domain_name, mod_ids in domain_modules.items():
|
|
191
|
+
props: dict[str, object] = {
|
|
192
|
+
"module_count": len(mod_ids),
|
|
193
|
+
"modules": mod_ids,
|
|
194
|
+
}
|
|
195
|
+
new_store.add_node(
|
|
196
|
+
GraphNode(
|
|
197
|
+
id=f"domain:{domain_name}",
|
|
198
|
+
kind=NodeKind.MODULE,
|
|
199
|
+
label=domain_name,
|
|
200
|
+
properties=props,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# --- 2. Roll up edges -----------------------------------------
|
|
205
|
+
edge_weights: dict[tuple[str, str, EdgeKind], int] = defaultdict(int)
|
|
206
|
+
|
|
207
|
+
for edge in store.all_edges():
|
|
208
|
+
src_domain = self._resolve_domain(edge.source)
|
|
209
|
+
tgt_domain = self._resolve_domain(edge.target)
|
|
210
|
+
|
|
211
|
+
src_id = f"domain:{src_domain}" if src_domain else edge.source
|
|
212
|
+
tgt_id = f"domain:{tgt_domain}" if tgt_domain else edge.target
|
|
213
|
+
|
|
214
|
+
if src_id == tgt_id:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
edge_weights[(src_id, tgt_id, edge.kind)] += 1
|
|
218
|
+
|
|
219
|
+
for (src, tgt, kind), weight in edge_weights.items():
|
|
220
|
+
props = {"weight": weight}
|
|
221
|
+
new_store.add_edge(
|
|
222
|
+
GraphEdge(
|
|
223
|
+
source=src,
|
|
224
|
+
target=tgt,
|
|
225
|
+
kind=kind,
|
|
226
|
+
label=f"{kind.value} (x{weight})" if weight > 1 else kind.value,
|
|
227
|
+
properties=props,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return new_store
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Core graph data models for OSSCodeIQ."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeKind(str, Enum):
|
|
12
|
+
"""Types of nodes in the OSSCodeIQ graph."""
|
|
13
|
+
|
|
14
|
+
MODULE = "module"
|
|
15
|
+
PACKAGE = "package"
|
|
16
|
+
CLASS = "class"
|
|
17
|
+
METHOD = "method"
|
|
18
|
+
ENDPOINT = "endpoint"
|
|
19
|
+
ENTITY = "entity"
|
|
20
|
+
REPOSITORY = "repository"
|
|
21
|
+
QUERY = "query"
|
|
22
|
+
MIGRATION = "migration"
|
|
23
|
+
TOPIC = "topic"
|
|
24
|
+
QUEUE = "queue"
|
|
25
|
+
EVENT = "event"
|
|
26
|
+
RMI_INTERFACE = "rmi_interface"
|
|
27
|
+
CONFIG_FILE = "config_file"
|
|
28
|
+
CONFIG_KEY = "config_key"
|
|
29
|
+
WEBSOCKET_ENDPOINT = "websocket_endpoint"
|
|
30
|
+
INTERFACE = "interface"
|
|
31
|
+
ABSTRACT_CLASS = "abstract_class"
|
|
32
|
+
ENUM = "enum"
|
|
33
|
+
ANNOTATION_TYPE = "annotation_type"
|
|
34
|
+
PROTOCOL_MESSAGE = "protocol_message"
|
|
35
|
+
CONFIG_DEFINITION = "config_definition"
|
|
36
|
+
DATABASE_CONNECTION = "database_connection"
|
|
37
|
+
AZURE_RESOURCE = "azure_resource"
|
|
38
|
+
AZURE_FUNCTION = "azure_function"
|
|
39
|
+
MESSAGE_QUEUE = "message_queue"
|
|
40
|
+
INFRA_RESOURCE = "infra_resource"
|
|
41
|
+
COMPONENT = "component"
|
|
42
|
+
GUARD = "guard"
|
|
43
|
+
MIDDLEWARE = "middleware"
|
|
44
|
+
HOOK = "hook"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EdgeKind(str, Enum):
|
|
48
|
+
"""Types of edges (relationships) in the OSSCodeIQ graph."""
|
|
49
|
+
|
|
50
|
+
DEPENDS_ON = "depends_on"
|
|
51
|
+
IMPORTS = "imports"
|
|
52
|
+
EXTENDS = "extends"
|
|
53
|
+
IMPLEMENTS = "implements"
|
|
54
|
+
CALLS = "calls"
|
|
55
|
+
INJECTS = "injects"
|
|
56
|
+
EXPOSES = "exposes"
|
|
57
|
+
QUERIES = "queries"
|
|
58
|
+
MAPS_TO = "maps_to"
|
|
59
|
+
PRODUCES = "produces"
|
|
60
|
+
CONSUMES = "consumes"
|
|
61
|
+
PUBLISHES = "publishes"
|
|
62
|
+
LISTENS = "listens"
|
|
63
|
+
INVOKES_RMI = "invokes_rmi"
|
|
64
|
+
EXPORTS_RMI = "exports_rmi"
|
|
65
|
+
READS_CONFIG = "reads_config"
|
|
66
|
+
MIGRATES = "migrates"
|
|
67
|
+
CONTAINS = "contains"
|
|
68
|
+
DEFINES = "defines"
|
|
69
|
+
OVERRIDES = "overrides"
|
|
70
|
+
CONNECTS_TO = "connects_to"
|
|
71
|
+
TRIGGERS = "triggers"
|
|
72
|
+
PROVISIONS = "provisions"
|
|
73
|
+
SENDS_TO = "sends_to"
|
|
74
|
+
RECEIVES_FROM = "receives_from"
|
|
75
|
+
PROTECTS = "protects"
|
|
76
|
+
RENDERS = "renders"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SourceLocation(BaseModel):
|
|
80
|
+
"""Source code location reference."""
|
|
81
|
+
|
|
82
|
+
file_path: str
|
|
83
|
+
line_start: int | None = None
|
|
84
|
+
line_end: int | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GraphNode(BaseModel):
|
|
88
|
+
"""A node in the OSSCodeIQ graph."""
|
|
89
|
+
|
|
90
|
+
id: str
|
|
91
|
+
kind: NodeKind
|
|
92
|
+
label: str
|
|
93
|
+
fqn: str | None = None
|
|
94
|
+
module: str | None = None
|
|
95
|
+
location: SourceLocation | None = None
|
|
96
|
+
annotations: list[str] = Field(default_factory=list)
|
|
97
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GraphEdge(BaseModel):
|
|
101
|
+
"""An edge (relationship) in the OSSCodeIQ graph."""
|
|
102
|
+
|
|
103
|
+
source: str
|
|
104
|
+
target: str
|
|
105
|
+
kind: EdgeKind
|
|
106
|
+
label: str | None = None
|
|
107
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CodeGraph(BaseModel):
|
|
111
|
+
"""Top-level serializable graph container."""
|
|
112
|
+
|
|
113
|
+
version: str = "1.0.0"
|
|
114
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
115
|
+
nodes: list[GraphNode] = Field(default_factory=list)
|
|
116
|
+
edges: list[GraphEdge] = Field(default_factory=list)
|
|
File without changes
|