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,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()
@@ -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,17 @@
1
+ from osscodeiq.models.graph import (
2
+ CodeGraph,
3
+ EdgeKind,
4
+ GraphEdge,
5
+ GraphNode,
6
+ NodeKind,
7
+ SourceLocation,
8
+ )
9
+
10
+ __all__ = [
11
+ "CodeGraph",
12
+ "EdgeKind",
13
+ "GraphEdge",
14
+ "GraphNode",
15
+ "NodeKind",
16
+ "SourceLocation",
17
+ ]
@@ -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