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,171 @@
1
+ """Graphviz DOT renderer for the OSSCodeIQ graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Literal
7
+
8
+ from osscodeiq.graph.store import GraphStore
9
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind
10
+
11
+
12
+ def _sanitize_id(raw: str) -> str:
13
+ """Replace characters invalid in DOT identifiers."""
14
+ return re.sub(r"[^a-zA-Z0-9_]", "_", raw)
15
+
16
+
17
+ def _quote(text: str) -> str:
18
+ """Escape a string for use inside DOT double-quotes."""
19
+ return text.replace("\\", "\\\\").replace('"', '\\"')
20
+
21
+
22
+ # -- Node shape and colour mapping -----------------------------------------
23
+
24
+ _NODE_STYLES: dict[NodeKind, dict[str, str]] = {
25
+ NodeKind.MODULE: {"shape": "box3d", "fillcolor": "#4A90D9", "fontcolor": "white"},
26
+ NodeKind.PACKAGE: {"shape": "box3d", "fillcolor": "#4A90D9", "fontcolor": "white"},
27
+ NodeKind.CLASS: {"shape": "box", "fillcolor": "#A8D8EA", "fontcolor": "black"},
28
+ NodeKind.METHOD: {"shape": "box", "fillcolor": "#D4E6F1", "fontcolor": "black"},
29
+ NodeKind.ENDPOINT: {"shape": "hexagon", "fillcolor": "#F9E79F", "fontcolor": "black"},
30
+ NodeKind.ENTITY: {"shape": "cylinder", "fillcolor": "#ABEBC6", "fontcolor": "black"},
31
+ NodeKind.REPOSITORY: {"shape": "cylinder", "fillcolor": "#ABEBC6", "fontcolor": "black"},
32
+ NodeKind.QUERY: {"shape": "box", "fillcolor": "#D5F5E3", "fontcolor": "black"},
33
+ NodeKind.MIGRATION: {"shape": "box", "fillcolor": "#D5F5E3", "fontcolor": "black"},
34
+ NodeKind.TOPIC: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
35
+ NodeKind.QUEUE: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
36
+ NodeKind.EVENT: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
37
+ NodeKind.RMI_INTERFACE: {"shape": "component", "fillcolor": "#D7BDE2", "fontcolor": "black"},
38
+ NodeKind.CONFIG_FILE: {"shape": "note", "fillcolor": "#FDEBD0", "fontcolor": "black"},
39
+ NodeKind.CONFIG_KEY: {"shape": "note", "fillcolor": "#FDEBD0", "fontcolor": "black"},
40
+ NodeKind.WEBSOCKET_ENDPOINT: {"shape": "hexagon", "fillcolor": "#F9E79F", "fontcolor": "black"},
41
+ }
42
+
43
+ # -- Edge styles -----------------------------------------------------------
44
+
45
+ _EDGE_STYLES: dict[EdgeKind, dict[str, str]] = {
46
+ EdgeKind.CALLS: {"style": "solid", "arrowhead": "normal"},
47
+ EdgeKind.DEPENDS_ON: {"style": "solid", "arrowhead": "normal"},
48
+ EdgeKind.IMPORTS: {"style": "solid", "arrowhead": "normal"},
49
+ EdgeKind.INJECTS: {"style": "solid", "arrowhead": "normal"},
50
+ EdgeKind.QUERIES: {"style": "solid", "arrowhead": "normal"},
51
+ EdgeKind.MAPS_TO: {"style": "solid", "arrowhead": "normal"},
52
+ EdgeKind.READS_CONFIG: {"style": "solid", "arrowhead": "normal"},
53
+ EdgeKind.MIGRATES: {"style": "solid", "arrowhead": "normal"},
54
+ EdgeKind.CONTAINS: {"style": "solid", "arrowhead": "normal"},
55
+ EdgeKind.EXPOSES: {"style": "solid", "arrowhead": "normal"},
56
+ EdgeKind.PRODUCES: {"style": "dashed", "arrowhead": "normal"},
57
+ EdgeKind.CONSUMES: {"style": "dashed", "arrowhead": "normal"},
58
+ EdgeKind.PUBLISHES: {"style": "dashed", "arrowhead": "normal"},
59
+ EdgeKind.LISTENS: {"style": "dashed", "arrowhead": "normal"},
60
+ EdgeKind.INVOKES_RMI: {"style": "dashed", "arrowhead": "normal"},
61
+ EdgeKind.EXPORTS_RMI: {"style": "dashed", "arrowhead": "normal"},
62
+ EdgeKind.EXTENDS: {"style": "solid", "arrowhead": "empty"},
63
+ EdgeKind.IMPLEMENTS: {"style": "solid", "arrowhead": "empty"},
64
+ }
65
+
66
+
67
+ ClusterBy = Literal["module", "domain", "node-type", None]
68
+
69
+
70
+ class DotRenderer:
71
+ """Render a :class:`GraphStore` as a Graphviz DOT graph."""
72
+
73
+ def __init__(
74
+ self,
75
+ rankdir: str = "LR",
76
+ cluster_by: ClusterBy = None,
77
+ fontname: str = "Helvetica",
78
+ fontsize: str = "11",
79
+ ) -> None:
80
+ self._rankdir = rankdir
81
+ self._cluster_by = cluster_by
82
+ self._fontname = fontname
83
+ self._fontsize = fontsize
84
+
85
+ # -- public API -------------------------------------------------
86
+
87
+ def render(self, store: GraphStore, cluster_by: ClusterBy | None = None) -> str:
88
+ """Return a DOT-language string."""
89
+ lines: list[str] = [
90
+ "digraph CodeIntelligence {",
91
+ f' rankdir={self._rankdir};',
92
+ f' fontname="{self._fontname}";',
93
+ f' fontsize={self._fontsize};',
94
+ f' node [fontname="{self._fontname}", fontsize={self._fontsize}, style=filled];',
95
+ f' edge [fontname="{self._fontname}", fontsize={self._fontsize}];',
96
+ "",
97
+ ]
98
+
99
+ nodes = store.all_nodes()
100
+ edges = store.all_edges()
101
+
102
+ if self._cluster_by:
103
+ lines.extend(self._render_clustered(nodes))
104
+ else:
105
+ for node in nodes:
106
+ lines.append(self._node_def(node))
107
+ lines.append("")
108
+
109
+ for edge in edges:
110
+ lines.append(self._edge_def(edge))
111
+
112
+ lines.append("}")
113
+ return "\n".join(lines) + "\n"
114
+
115
+ # -- internal ---------------------------------------------------
116
+
117
+ def _node_def(self, node: GraphNode) -> str:
118
+ sid = _sanitize_id(node.id)
119
+ style = _NODE_STYLES.get(node.kind, {"shape": "box", "fillcolor": "#FFFFFF", "fontcolor": "black"})
120
+ label = _quote(node.label)
121
+ attrs = (
122
+ f'label="{label}", '
123
+ f'shape={style["shape"]}, '
124
+ f'fillcolor="{style["fillcolor"]}", '
125
+ f'fontcolor="{style["fontcolor"]}"'
126
+ )
127
+ return f" {sid} [{attrs}];"
128
+
129
+ def _edge_def(self, edge: GraphEdge) -> str:
130
+ src = _sanitize_id(edge.source)
131
+ tgt = _sanitize_id(edge.target)
132
+ style = _EDGE_STYLES.get(
133
+ edge.kind,
134
+ {"style": "solid", "arrowhead": "normal"},
135
+ )
136
+ label = _quote(edge.label or edge.kind.value)
137
+ attrs = (
138
+ f'label="{label}", '
139
+ f'style={style["style"]}, '
140
+ f'arrowhead={style["arrowhead"]}'
141
+ )
142
+ return f" {src} -> {tgt} [{attrs}];"
143
+
144
+ def _cluster_key(self, node: GraphNode) -> str:
145
+ if self._cluster_by == "module":
146
+ return node.module or "unknown"
147
+ if self._cluster_by == "domain":
148
+ return node.properties.get("domain", node.module or "unknown") # type: ignore[return-value]
149
+ if self._cluster_by == "node-type":
150
+ return node.kind.value
151
+ return "default"
152
+
153
+ def _render_clustered(self, nodes: list[GraphNode]) -> list[str]:
154
+ clusters: dict[str, list[GraphNode]] = {}
155
+ for node in nodes:
156
+ key = self._cluster_key(node)
157
+ clusters.setdefault(key, []).append(node)
158
+
159
+ lines: list[str] = []
160
+ for idx, (cluster_name, cluster_nodes) in enumerate(sorted(clusters.items())):
161
+ sub_id = _sanitize_id(f"cluster_{cluster_name}")
162
+ lines.append(f" subgraph {sub_id} {{")
163
+ lines.append(f' label="{_quote(cluster_name)}";')
164
+ lines.append(' style=filled;')
165
+ lines.append(' color="#E8E8E8";')
166
+ for node in cluster_nodes:
167
+ lines.append(f" {self._node_def(node)}")
168
+ lines.append(" }")
169
+ lines.append("")
170
+
171
+ return lines
@@ -0,0 +1,160 @@
1
+ """Mermaid diagram renderer for the OSSCodeIQ graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Literal
7
+
8
+ from osscodeiq.graph.store import GraphStore
9
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind
10
+
11
+
12
+ def _sanitize_id(raw: str) -> str:
13
+ """Replace characters that are invalid in Mermaid node IDs."""
14
+ return re.sub(r"[^a-zA-Z0-9_]", "_", raw)
15
+
16
+
17
+ def _escape_label(text: str) -> str:
18
+ """Escape Mermaid special characters in labels."""
19
+ for ch in ('"', '|', '[', ']', '{', '}', '(', ')', '<', '>', '#'):
20
+ text = text.replace(ch, f"&#{ord(ch)};")
21
+ return text
22
+
23
+
24
+ # -- Node shape templates --------------------------------------------------
25
+ # Mermaid syntax: id[label], id([label]), id{{label}}, id[(label)], etc.
26
+
27
+ _NODE_SHAPES: dict[NodeKind, tuple[str, str]] = {
28
+ NodeKind.MODULE: ("[", "]"), # rectangle
29
+ NodeKind.PACKAGE: ("[", "]"),
30
+ NodeKind.CLASS: ("[", "]"),
31
+ NodeKind.METHOD: ("([", "])"), # stadium / pill
32
+ NodeKind.ENDPOINT: ("{{", "}}"), # hexagon
33
+ NodeKind.ENTITY: ("[(", ")]"), # cylinder
34
+ NodeKind.REPOSITORY: ("[(", ")]"),
35
+ NodeKind.QUERY: ("([", "])"),
36
+ NodeKind.MIGRATION: ("([", "])"),
37
+ NodeKind.TOPIC: ("[/", "/]"), # parallelogram
38
+ NodeKind.QUEUE: ("[/", "/]"),
39
+ NodeKind.EVENT: ("[/", "/]"),
40
+ NodeKind.RMI_INTERFACE: ("[[", "]]"), # subroutine
41
+ NodeKind.CONFIG_FILE: ("([", "])"),
42
+ NodeKind.CONFIG_KEY: ("([", "])"),
43
+ NodeKind.WEBSOCKET_ENDPOINT: ("{{", "}}"),
44
+ }
45
+
46
+ # -- Edge arrow styles ------------------------------------------------------
47
+
48
+ _EDGE_STYLES: dict[EdgeKind, str] = {
49
+ # Solid arrow -->
50
+ EdgeKind.CALLS: "-->",
51
+ EdgeKind.DEPENDS_ON: "-->",
52
+ EdgeKind.IMPORTS: "-->",
53
+ EdgeKind.INJECTS: "-->",
54
+ EdgeKind.QUERIES: "-->",
55
+ EdgeKind.MAPS_TO: "-->",
56
+ EdgeKind.READS_CONFIG: "-->",
57
+ EdgeKind.MIGRATES: "-->",
58
+ EdgeKind.CONTAINS: "-->",
59
+ EdgeKind.EXPOSES: "-->",
60
+ # Dotted arrow -.->
61
+ EdgeKind.PRODUCES: "-.->",
62
+ EdgeKind.CONSUMES: "-.->",
63
+ EdgeKind.PUBLISHES: "-.->",
64
+ EdgeKind.LISTENS: "-.->",
65
+ EdgeKind.INVOKES_RMI: "-.->",
66
+ EdgeKind.EXPORTS_RMI: "-.->",
67
+ # Open arrowhead --o
68
+ EdgeKind.EXTENDS: "--o",
69
+ EdgeKind.IMPLEMENTS: "--o",
70
+ }
71
+
72
+
73
+ ClusterBy = Literal["module", "domain", "node-type", None]
74
+
75
+
76
+ class MermaidRenderer:
77
+ """Render a :class:`GraphStore` as a Mermaid flowchart."""
78
+
79
+ def __init__(
80
+ self,
81
+ direction: str = "LR",
82
+ cluster_by: ClusterBy = None,
83
+ ) -> None:
84
+ self._direction = direction
85
+ self._cluster_by = cluster_by
86
+
87
+ # -- public API -------------------------------------------------
88
+
89
+ def render(self, store: GraphStore, cluster_by: ClusterBy | None = None) -> str:
90
+ """Return a Mermaid flowchart string."""
91
+ effective_cluster = cluster_by or self._cluster_by
92
+ lines: list[str] = [f"graph {self._direction}"]
93
+
94
+ nodes = store.all_nodes()
95
+ edges = store.all_edges()
96
+
97
+ if effective_cluster:
98
+ old = self._cluster_by
99
+ self._cluster_by = effective_cluster
100
+ lines.extend(self._render_clustered(nodes, edges))
101
+ self._cluster_by = old
102
+ else:
103
+ lines.extend(self._render_flat(nodes, edges))
104
+
105
+ return "\n".join(lines) + "\n"
106
+
107
+ # -- internal ---------------------------------------------------
108
+
109
+ def _node_def(self, node: GraphNode) -> str:
110
+ sid = _sanitize_id(node.id)
111
+ left, right = _NODE_SHAPES.get(node.kind, ("[", "]"))
112
+ label = _escape_label(node.label)
113
+ return f" {sid}{left}\"{label}\"{right}"
114
+
115
+ def _edge_def(self, edge: GraphEdge) -> str:
116
+ src = _sanitize_id(edge.source)
117
+ tgt = _sanitize_id(edge.target)
118
+ arrow = _EDGE_STYLES.get(edge.kind, "-->")
119
+ label = _escape_label(edge.label or edge.kind.value)
120
+ return f" {src} {arrow}|{label}| {tgt}"
121
+
122
+ def _cluster_key(self, node: GraphNode) -> str:
123
+ if self._cluster_by == "module":
124
+ return node.module or "unknown"
125
+ if self._cluster_by == "domain":
126
+ return node.properties.get("domain", node.module or "unknown") # type: ignore[return-value]
127
+ if self._cluster_by == "node-type":
128
+ return node.kind.value
129
+ return "default"
130
+
131
+ def _render_flat(
132
+ self, nodes: list[GraphNode], edges: list[GraphEdge]
133
+ ) -> list[str]:
134
+ lines: list[str] = []
135
+ for node in nodes:
136
+ lines.append(self._node_def(node))
137
+ for edge in edges:
138
+ lines.append(self._edge_def(edge))
139
+ return lines
140
+
141
+ def _render_clustered(
142
+ self, nodes: list[GraphNode], edges: list[GraphEdge]
143
+ ) -> list[str]:
144
+ clusters: dict[str, list[GraphNode]] = {}
145
+ for node in nodes:
146
+ key = self._cluster_key(node)
147
+ clusters.setdefault(key, []).append(node)
148
+
149
+ lines: list[str] = []
150
+ for idx, (cluster_name, cluster_nodes) in enumerate(sorted(clusters.items())):
151
+ sub_id = _sanitize_id(f"cluster_{cluster_name}")
152
+ lines.append(f" subgraph {sub_id}[\"{cluster_name}\"]")
153
+ for node in cluster_nodes:
154
+ lines.append(f" {self._node_def(node)}")
155
+ lines.append(" end")
156
+
157
+ for edge in edges:
158
+ lines.append(self._edge_def(edge))
159
+
160
+ return lines
@@ -0,0 +1,58 @@
1
+ """Graph size safety guard for the OSSCodeIQ CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from osscodeiq.graph.store import GraphStore
9
+
10
+
11
+ def check_graph_size(
12
+ store: GraphStore,
13
+ max_nodes: int,
14
+ console: Console,
15
+ ) -> None:
16
+ """Abort with a helpful message if *store* exceeds *max_nodes*.
17
+
18
+ Parameters
19
+ ----------
20
+ store:
21
+ The graph store to check.
22
+ max_nodes:
23
+ Maximum number of nodes allowed before the safety guard fires.
24
+ console:
25
+ Rich console used to print the error and suggestions.
26
+
27
+ Raises
28
+ ------
29
+ typer.Exit
30
+ If the node count exceeds *max_nodes*.
31
+ """
32
+ count = store.node_count
33
+ if count <= max_nodes:
34
+ return
35
+
36
+ console.print(
37
+ f"\n[bold red]Error:[/bold red] Graph contains "
38
+ f"[bold]{count:,}[/bold] nodes, which exceeds the "
39
+ f"safety limit of [bold]{max_nodes:,}[/bold].\n"
40
+ )
41
+ console.print("[bold yellow]Suggestions to reduce the graph size:[/bold yellow]\n")
42
+ console.print(
43
+ " 1. Use [cyan]--view architect[/cyan] to collapse detail "
44
+ "nodes into module-level nodes."
45
+ )
46
+ console.print(
47
+ " 2. Use [cyan]--focus \"node_id\" --hops 1[/cyan] to restrict "
48
+ "output to a small neighbourhood."
49
+ )
50
+ console.print(
51
+ " 3. Use [cyan]--module <name>[/cyan] to filter by module."
52
+ )
53
+ console.print(
54
+ " 4. Use [cyan]--max-nodes N[/cyan] to override this limit "
55
+ "(e.g. [cyan]--max-nodes 2000[/cyan]).\n"
56
+ )
57
+
58
+ raise typer.Exit(code=1)
@@ -0,0 +1,42 @@
1
+ """JSON and YAML serializers for the OSSCodeIQ graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import yaml
8
+
9
+ from osscodeiq.models.graph import CodeGraph
10
+
11
+
12
+ class JsonSerializer:
13
+ """Serialize a :class:`CodeGraph` to JSON."""
14
+
15
+ def serialize(self, graph: CodeGraph, pretty: bool = True) -> str:
16
+ """Return the graph as a JSON string.
17
+
18
+ Parameters
19
+ ----------
20
+ graph:
21
+ The code graph to serialize.
22
+ pretty:
23
+ When ``True`` (default), emit indented, human-readable JSON.
24
+ """
25
+ data = graph.model_dump(mode="json")
26
+ if pretty:
27
+ return json.dumps(data, indent=2, sort_keys=False)
28
+ return json.dumps(data, sort_keys=False)
29
+
30
+
31
+ class YamlSerializer:
32
+ """Serialize a :class:`CodeGraph` to YAML."""
33
+
34
+ def serialize(self, graph: CodeGraph) -> str:
35
+ """Return the graph as a YAML string."""
36
+ data = graph.model_dump(mode="json")
37
+ return yaml.dump(
38
+ data,
39
+ default_flow_style=False,
40
+ sort_keys=False,
41
+ allow_unicode=True,
42
+ )
@@ -0,0 +1,5 @@
1
+ from osscodeiq.parsing.parser_manager import ParserManager
2
+
3
+ __all__ = [
4
+ "ParserManager",
5
+ ]
File without changes
@@ -0,0 +1,23 @@
1
+ """Abstract language support protocol for tree-sitter based parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ import tree_sitter
8
+
9
+
10
+ @runtime_checkable
11
+ class LanguageSupport(Protocol):
12
+ """Protocol that language plugins must satisfy."""
13
+
14
+ name: str
15
+ extensions: tuple[str, ...]
16
+
17
+ def get_language(self) -> tree_sitter.Language:
18
+ """Return the tree-sitter Language object for this language."""
19
+ ...
20
+
21
+ def get_queries(self) -> dict[str, str]:
22
+ """Return a mapping of query-name to tree-sitter query source."""
23
+ ...
@@ -0,0 +1,68 @@
1
+ """Java language support for tree-sitter parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tree_sitter
6
+ import tree_sitter_java
7
+
8
+
9
+ class JavaLanguageSupport:
10
+ """Java language support with Spring-focused queries."""
11
+
12
+ name: str = "java"
13
+ extensions: tuple[str, ...] = (".java",)
14
+
15
+ def get_language(self) -> tree_sitter.Language:
16
+ return tree_sitter.Language(tree_sitter_java.language())
17
+
18
+ def get_queries(self) -> dict[str, str]:
19
+ return _JAVA_QUERIES.copy()
20
+
21
+
22
+ # ---------------------------------------------------------------
23
+ # Tree-sitter queries targeting Java + Spring annotation patterns
24
+ # ---------------------------------------------------------------
25
+
26
+ _JAVA_QUERIES: dict[str, str] = {
27
+ "annotations": """
28
+ (marker_annotation
29
+ name: (identifier) @annotation.name)
30
+ (annotation
31
+ name: (identifier) @annotation.name
32
+ arguments: (annotation_argument_list) @annotation.args)
33
+ """,
34
+ "class_declarations": """
35
+ (class_declaration
36
+ name: (identifier) @class.name
37
+ superclass: (superclass)? @class.superclass
38
+ interfaces: (super_interfaces)? @class.interfaces
39
+ body: (class_body) @class.body)
40
+ """,
41
+ "method_declarations": """
42
+ (method_declaration
43
+ (modifiers)? @method.modifiers
44
+ type: (_) @method.return_type
45
+ name: (identifier) @method.name
46
+ parameters: (formal_parameters) @method.params
47
+ body: (block)? @method.body)
48
+ """,
49
+ "interface_declarations": """
50
+ (interface_declaration
51
+ name: (identifier) @interface.name
52
+ body: (interface_body) @interface.body)
53
+ """,
54
+ "field_declarations": """
55
+ (field_declaration
56
+ (modifiers)? @field.modifiers
57
+ type: (_) @field.type
58
+ declarator: (variable_declarator
59
+ name: (identifier) @field.name))
60
+ """,
61
+ "import_declarations": """
62
+ (import_declaration
63
+ (scoped_identifier) @import.path)
64
+ """,
65
+ "string_literals": """
66
+ (string_literal) @string.value
67
+ """,
68
+ }
@@ -0,0 +1,57 @@
1
+ """Python language support for tree-sitter parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tree_sitter
6
+ import tree_sitter_python
7
+
8
+
9
+ class PythonLanguageSupport:
10
+ """Tree-sitter language support for Python."""
11
+
12
+ name: str = "python"
13
+ extensions: tuple[str, ...] = (".py",)
14
+
15
+ def get_language(self) -> tree_sitter.Language:
16
+ return tree_sitter.Language(tree_sitter_python.language())
17
+
18
+ def get_queries(self) -> dict[str, str]:
19
+ return PYTHON_QUERIES
20
+
21
+
22
+ PYTHON_QUERIES: dict[str, str] = {
23
+ "function_definitions": """
24
+ (function_definition
25
+ name: (identifier) @func.name
26
+ parameters: (parameters) @func.params
27
+ body: (block) @func.body)
28
+ """,
29
+ "class_definitions": """
30
+ (class_definition
31
+ name: (identifier) @class.name
32
+ body: (block) @class.body)
33
+ """,
34
+ "decorators": """
35
+ (decorator
36
+ (call
37
+ function: (_) @decorator.name
38
+ arguments: (argument_list)? @decorator.args)?)
39
+ """,
40
+ "import_statements": """
41
+ (import_statement
42
+ name: (dotted_name) @import.name)
43
+ """,
44
+ "import_from": """
45
+ (import_from_statement
46
+ module_name: (dotted_name)? @import.module
47
+ name: (_)? @import.name)
48
+ """,
49
+ "string_literals": """
50
+ (string) @string
51
+ """,
52
+ "assignments": """
53
+ (assignment
54
+ left: (_) @assign.target
55
+ right: (_) @assign.value)
56
+ """,
57
+ }
@@ -0,0 +1,95 @@
1
+ """TypeScript/JavaScript language support for tree-sitter parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tree_sitter
6
+ import tree_sitter_typescript
7
+
8
+
9
+ class TypeScriptLanguageSupport:
10
+ """Tree-sitter language support for TypeScript."""
11
+
12
+ name: str = "typescript"
13
+ extensions: tuple[str, ...] = (".ts", ".tsx")
14
+
15
+ def get_language(self) -> tree_sitter.Language:
16
+ return tree_sitter.Language(tree_sitter_typescript.language_typescript())
17
+
18
+ def get_queries(self) -> dict[str, str]:
19
+ return TYPESCRIPT_QUERIES
20
+
21
+
22
+ class JavaScriptLanguageSupport:
23
+ """Tree-sitter language support for JavaScript."""
24
+
25
+ name: str = "javascript"
26
+ extensions: tuple[str, ...] = (".js", ".jsx")
27
+
28
+ def get_language(self) -> tree_sitter.Language:
29
+ import tree_sitter_javascript
30
+ return tree_sitter.Language(tree_sitter_javascript.language())
31
+
32
+ def get_queries(self) -> dict[str, str]:
33
+ return JAVASCRIPT_QUERIES
34
+
35
+
36
+ TYPESCRIPT_QUERIES: dict[str, str] = {
37
+ "class_declarations": """
38
+ (class_declaration
39
+ name: (type_identifier) @class.name
40
+ body: (class_body) @class.body)
41
+ """,
42
+ "method_definitions": """
43
+ (method_definition
44
+ name: (property_identifier) @method.name
45
+ parameters: (formal_parameters) @method.params)
46
+ """,
47
+ "function_declarations": """
48
+ (function_declaration
49
+ name: (identifier) @func.name
50
+ parameters: (formal_parameters) @func.params)
51
+ """,
52
+ "decorators": """
53
+ (decorator
54
+ (call_expression
55
+ function: (_) @decorator.name
56
+ arguments: (arguments)? @decorator.args))
57
+ """,
58
+ "import_statements": """
59
+ (import_statement
60
+ source: (string) @import.source)
61
+ """,
62
+ "call_expressions": """
63
+ (call_expression
64
+ function: (_) @call.func
65
+ arguments: (arguments) @call.args)
66
+ """,
67
+ "string_literals": """
68
+ (string) @string
69
+ """,
70
+ }
71
+
72
+ JAVASCRIPT_QUERIES: dict[str, str] = {
73
+ "function_declarations": """
74
+ (function_declaration
75
+ name: (identifier) @func.name
76
+ parameters: (formal_parameters) @func.params)
77
+ """,
78
+ "class_declarations": """
79
+ (class_declaration
80
+ name: (identifier) @class.name
81
+ body: (class_body) @class.body)
82
+ """,
83
+ "call_expressions": """
84
+ (call_expression
85
+ function: (_) @call.func
86
+ arguments: (arguments) @call.args)
87
+ """,
88
+ "import_statements": """
89
+ (import_statement
90
+ source: (string) @import.source)
91
+ """,
92
+ "string_literals": """
93
+ (string) @string
94
+ """,
95
+ }