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,357 @@
1
+ """Flow view generators — each produces a small, clean FlowDiagram from the full graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from osscodeiq.flow.models import FlowDiagram, FlowEdge, FlowNode, FlowSubgraph
6
+ from osscodeiq.graph.store import GraphStore
7
+ from osscodeiq.models.graph import EdgeKind, NodeKind
8
+
9
+
10
+ _GITLAB_PREFIX = "gitlab:"
11
+
12
+
13
+ def build_overview(store: GraphStore) -> FlowDiagram:
14
+ """High-level overview with 4 subgraphs: CI, Infrastructure, Application, Security."""
15
+ subgraphs = []
16
+ edges = []
17
+
18
+ # CI/CD subgraph
19
+ ci_nodes = []
20
+ workflows = [n for n in store.all_nodes() if n.kind == NodeKind.MODULE and any(
21
+ p in n.id for p in ("gha:", _GITLAB_PREFIX)
22
+ )]
23
+ ci_jobs = [n for n in store.all_nodes() if n.kind == NodeKind.METHOD and any(
24
+ p in n.id for p in ("gha:", _GITLAB_PREFIX)
25
+ )]
26
+ if workflows or ci_jobs:
27
+ ci_nodes.append(FlowNode(id="ci_pipelines", label=f"Pipelines x{len(workflows)}", kind="pipeline",
28
+ properties={"count": len(workflows)}))
29
+ if ci_jobs:
30
+ ci_nodes.append(FlowNode(id="ci_jobs", label=f"Jobs x{len(ci_jobs)}", kind="job",
31
+ properties={"count": len(ci_jobs)}))
32
+ edges.append(FlowEdge(source="ci_pipelines", target="ci_jobs"))
33
+ subgraphs.append(FlowSubgraph(id="ci", label="CI/CD Pipeline", nodes=ci_nodes, drill_down_view="ci"))
34
+
35
+ # Infrastructure subgraph
36
+ infra_nodes_raw = store.nodes_by_kind(NodeKind.INFRA_RESOURCE) + store.nodes_by_kind(NodeKind.AZURE_RESOURCE)
37
+ if infra_nodes_raw:
38
+ # Group by type from properties or id prefix
39
+ k8s = [n for n in infra_nodes_raw if "k8s:" in n.id]
40
+ docker = [n for n in infra_nodes_raw if "compose:" in n.id or "dockerfile" in n.id.lower()]
41
+ terraform = [n for n in infra_nodes_raw if "tf:" in n.id]
42
+ other_infra = [n for n in infra_nodes_raw if n not in k8s and n not in docker and n not in terraform]
43
+
44
+ infra_flow_nodes = []
45
+ if k8s:
46
+ infra_flow_nodes.append(FlowNode(id="infra_k8s", label=f"K8s Resources x{len(k8s)}", kind="k8s",
47
+ properties={"count": len(k8s)}))
48
+ if docker:
49
+ infra_flow_nodes.append(FlowNode(id="infra_docker", label=f"Docker x{len(docker)}", kind="docker",
50
+ properties={"count": len(docker)}))
51
+ if terraform:
52
+ infra_flow_nodes.append(FlowNode(id="infra_tf", label=f"Terraform x{len(terraform)}", kind="terraform",
53
+ properties={"count": len(terraform)}))
54
+ if other_infra:
55
+ infra_flow_nodes.append(FlowNode(id="infra_other", label=f"Infra x{len(other_infra)}", kind="infra",
56
+ properties={"count": len(other_infra)}))
57
+ if infra_flow_nodes:
58
+ subgraphs.append(FlowSubgraph(id="infra", label="Infrastructure", nodes=infra_flow_nodes, drill_down_view="deploy"))
59
+
60
+ # Application subgraph
61
+ endpoints = store.nodes_by_kind(NodeKind.ENDPOINT)
62
+ entities = store.nodes_by_kind(NodeKind.ENTITY)
63
+ classes = store.nodes_by_kind(NodeKind.CLASS)
64
+ methods = store.nodes_by_kind(NodeKind.METHOD)
65
+ # Exclude CI methods from method count
66
+ app_methods = [m for m in methods if not any(p in m.id for p in ("gha:", _GITLAB_PREFIX))]
67
+ components = store.nodes_by_kind(NodeKind.COMPONENT)
68
+ topics = store.nodes_by_kind(NodeKind.TOPIC) + store.nodes_by_kind(NodeKind.QUEUE)
69
+ db_conns = store.nodes_by_kind(NodeKind.DATABASE_CONNECTION)
70
+
71
+ app_nodes = []
72
+ if endpoints:
73
+ app_nodes.append(FlowNode(id="app_endpoints", label=f"Endpoints x{len(endpoints)}", kind="endpoint",
74
+ properties={"count": len(endpoints)}))
75
+ if entities:
76
+ app_nodes.append(FlowNode(id="app_entities", label=f"Entities x{len(entities)}", kind="entity",
77
+ properties={"count": len(entities)}))
78
+ if components:
79
+ app_nodes.append(FlowNode(id="app_components", label=f"Components x{len(components)}", kind="component",
80
+ properties={"count": len(components)}))
81
+ if topics:
82
+ app_nodes.append(FlowNode(id="app_messaging", label=f"Topics/Queues x{len(topics)}", kind="messaging",
83
+ properties={"count": len(topics)}))
84
+ if db_conns:
85
+ app_nodes.append(FlowNode(id="app_database", label=f"DB Connections x{len(db_conns)}", kind="database",
86
+ properties={"count": len(db_conns)}))
87
+ if not app_nodes and (classes or app_methods):
88
+ app_nodes.append(FlowNode(id="app_code", label=f"Classes x{len(classes)}, Methods x{len(app_methods)}", kind="code",
89
+ properties={"classes": len(classes), "methods": len(app_methods)}))
90
+ if app_nodes:
91
+ subgraphs.append(FlowSubgraph(id="app", label="Application", nodes=app_nodes, drill_down_view="runtime"))
92
+ # Add internal edges
93
+ if endpoints and entities:
94
+ edges.append(FlowEdge(source="app_endpoints", target="app_entities", label="queries"))
95
+ if endpoints and any(n.id == "app_messaging" for n in app_nodes):
96
+ edges.append(FlowEdge(source="app_endpoints", target="app_messaging", style="dotted"))
97
+
98
+ # Security subgraph
99
+ guards = store.nodes_by_kind(NodeKind.GUARD)
100
+ middleware = store.nodes_by_kind(NodeKind.MIDDLEWARE)
101
+ if guards or middleware:
102
+ sec_nodes = []
103
+ if guards:
104
+ sec_nodes.append(FlowNode(id="sec_guards", label=f"Auth Guards x{len(guards)}", kind="guard",
105
+ properties={"count": len(guards)}))
106
+ if middleware:
107
+ sec_nodes.append(FlowNode(id="sec_middleware", label=f"Middleware x{len(middleware)}", kind="middleware",
108
+ properties={"count": len(middleware)}))
109
+ subgraphs.append(FlowSubgraph(id="security", label="Security", nodes=sec_nodes, drill_down_view="auth"))
110
+ # Guards protect endpoints
111
+ if guards and endpoints:
112
+ edges.append(FlowEdge(source="sec_guards", target="app_endpoints", label="protects", style="thick"))
113
+
114
+ # Cross-subgraph edges
115
+ if ci_nodes and infra_nodes_raw:
116
+ infra_flow_nodes_local = [sg for sg in subgraphs if sg.id == "infra"]
117
+ if infra_flow_nodes_local and infra_flow_nodes_local[0].nodes:
118
+ first_infra = infra_flow_nodes_local[0].nodes[0].id
119
+ edges.append(FlowEdge(source="ci_jobs" if ci_jobs else "ci_pipelines", target=first_infra, label="deploys"))
120
+ if infra_nodes_raw and app_nodes:
121
+ infra_flow_nodes_local = [sg for sg in subgraphs if sg.id == "infra"]
122
+ if infra_flow_nodes_local and infra_flow_nodes_local[0].nodes:
123
+ first_infra = infra_flow_nodes_local[0].nodes[0].id
124
+ edges.append(FlowEdge(source=first_infra, target=app_nodes[0].id, label="hosts"))
125
+
126
+ stats = {
127
+ "total_nodes": store.node_count,
128
+ "total_edges": store.edge_count,
129
+ "endpoints": len(endpoints),
130
+ "entities": len(entities),
131
+ "guards": len(guards),
132
+ "components": len(components),
133
+ "infra_resources": len(infra_nodes_raw),
134
+ }
135
+
136
+ return FlowDiagram(title="Architecture Overview", view="overview", subgraphs=subgraphs, edges=edges, stats=stats)
137
+
138
+
139
+ def build_ci_view(store: GraphStore) -> FlowDiagram:
140
+ """CI/CD pipeline detail -- shows workflows, jobs, dependencies."""
141
+ subgraphs = []
142
+ edges = []
143
+
144
+ # Find CI-related nodes
145
+ all_nodes = store.all_nodes()
146
+ workflows = sorted([n for n in all_nodes if n.kind == NodeKind.MODULE and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
147
+ jobs = sorted([n for n in all_nodes if n.kind == NodeKind.METHOD and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
148
+ triggers = sorted([n for n in all_nodes if n.kind == NodeKind.CONFIG_KEY and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
149
+
150
+ # Trigger nodes
151
+ if triggers:
152
+ trigger_flow = [FlowNode(id=f"trigger_{i}", label=t.label, kind="trigger",
153
+ properties={"source_id": t.id}) for i, t in enumerate(triggers[:10])]
154
+ subgraphs.append(FlowSubgraph(id="triggers", label="Triggers", nodes=trigger_flow))
155
+
156
+ # Group jobs by workflow
157
+ jobs_by_workflow: dict[str, list] = {}
158
+ for job in jobs:
159
+ # Determine workflow from job's module or id prefix
160
+ wf_id = job.module or (job.id.rsplit(":job:", 1)[0] if ":job:" in job.id else "unknown")
161
+ jobs_by_workflow.setdefault(wf_id, []).append(job)
162
+
163
+ for wf in workflows:
164
+ wf_jobs = jobs_by_workflow.get(wf.id, [])
165
+ job_nodes = [FlowNode(id=f"job_{j.id.replace(':', '_')}", label=j.label, kind="job",
166
+ properties={k: v for k, v in j.properties.items() if k in ("stage", "runs_on", "image")})
167
+ for j in wf_jobs[:20]]
168
+ subgraphs.append(FlowSubgraph(id=f"wf_{wf.id.replace(':', '_')}", label=wf.label, nodes=job_nodes))
169
+
170
+ # Job dependency edges
171
+ ci_edges = [e for e in store.all_edges() if e.kind == EdgeKind.DEPENDS_ON and any(p in e.source for p in ("gha:", _GITLAB_PREFIX))]
172
+ for e in sorted(ci_edges, key=lambda x: (x.source, x.target)):
173
+ edges.append(FlowEdge(source=f"job_{e.source.replace(':', '_')}", target=f"job_{e.target.replace(':', '_')}", label="needs"))
174
+
175
+ # Trigger -> workflow edges
176
+ if triggers and workflows:
177
+ for wf in workflows:
178
+ edges.append(FlowEdge(source="trigger_0", target=f"wf_{wf.id.replace(':', '_')}", style="dotted"))
179
+
180
+ return FlowDiagram(title="CI/CD Pipeline", view="ci", direction="TD", subgraphs=subgraphs, edges=edges,
181
+ stats={"workflows": len(workflows), "jobs": len(jobs), "triggers": len(triggers)})
182
+
183
+
184
+ def build_deploy_view(store: GraphStore) -> FlowDiagram:
185
+ """Deployment topology -- K8s, Docker, Terraform resources."""
186
+ subgraphs = []
187
+ edges = []
188
+
189
+ all_nodes = store.all_nodes()
190
+ all_edges = store.all_edges()
191
+ infra = sorted([n for n in all_nodes if n.kind in (NodeKind.INFRA_RESOURCE, NodeKind.AZURE_RESOURCE)], key=lambda n: n.id)
192
+
193
+ # Group by technology
194
+ k8s = [n for n in infra if "k8s:" in n.id]
195
+ compose = [n for n in infra if "compose:" in n.id]
196
+ tf = [n for n in infra if "tf:" in n.id]
197
+ docker = [n for n in infra if "dockerfile" in n.id.lower() or n.id.startswith("docker:")]
198
+ other = [n for n in infra if n not in k8s and n not in compose and n not in tf and n not in docker]
199
+
200
+ def _make_nodes(nodes, prefix, max_nodes=20):
201
+ return [FlowNode(id=f"{prefix}_{i}", label=n.label, kind=prefix,
202
+ properties={k: v for k, v in n.properties.items() if k in ("kind", "namespace", "image", "resource_type", "provider")})
203
+ for i, n in enumerate(nodes[:max_nodes])]
204
+
205
+ if k8s:
206
+ subgraphs.append(FlowSubgraph(id="k8s", label=f"Kubernetes ({len(k8s)} resources)", nodes=_make_nodes(k8s, "k8s")))
207
+ if compose:
208
+ subgraphs.append(FlowSubgraph(id="compose", label=f"Docker Compose ({len(compose)} services)", nodes=_make_nodes(compose, "compose")))
209
+ if tf:
210
+ subgraphs.append(FlowSubgraph(id="terraform", label=f"Terraform ({len(tf)} resources)", nodes=_make_nodes(tf, "tf")))
211
+ if docker:
212
+ subgraphs.append(FlowSubgraph(id="docker", label=f"Docker ({len(docker)} images)", nodes=_make_nodes(docker, "docker")))
213
+ if other:
214
+ subgraphs.append(FlowSubgraph(id="other_infra", label=f"Other ({len(other)})", nodes=_make_nodes(other, "other")))
215
+
216
+ # Add CONNECTS_TO and DEPENDS_ON edges between infra nodes
217
+ infra_ids = {n.id for n in infra}
218
+ for e in sorted(all_edges, key=lambda x: (x.source, x.target)):
219
+ if e.source in infra_ids and e.target in infra_ids and e.kind in (EdgeKind.CONNECTS_TO, EdgeKind.DEPENDS_ON):
220
+ # Map to flow node IDs
221
+ src_idx = next((i for i, n in enumerate(infra) if n.id == e.source), None)
222
+ tgt_idx = next((i for i, n in enumerate(infra) if n.id == e.target), None)
223
+ if src_idx is not None and tgt_idx is not None:
224
+ src_node = infra[src_idx]
225
+ tgt_node = infra[tgt_idx]
226
+ # Determine prefix and local index from group membership
227
+ src_prefix, src_local = _resolve_group_index(src_node, k8s, compose, tf, docker, other)
228
+ tgt_prefix, tgt_local = _resolve_group_index(tgt_node, k8s, compose, tf, docker, other)
229
+ edges.append(FlowEdge(source=f"{src_prefix}_{src_local}", target=f"{tgt_prefix}_{tgt_local}"))
230
+
231
+ return FlowDiagram(title="Deployment Topology", view="deploy", direction="TD", subgraphs=subgraphs, edges=edges,
232
+ stats={"k8s": len(k8s), "compose": len(compose), "terraform": len(tf), "docker": len(docker)})
233
+
234
+
235
+ def _resolve_group_index(node, k8s, compose, tf, docker, other):
236
+ """Return (prefix, local_index) for a node within its technology group."""
237
+ if node in k8s:
238
+ return "k8s", k8s.index(node)
239
+ if node in compose:
240
+ return "compose", compose.index(node)
241
+ if node in tf:
242
+ return "tf", tf.index(node)
243
+ if node in docker:
244
+ return "docker", docker.index(node)
245
+ return "other", other.index(node)
246
+
247
+
248
+ def build_runtime_view(store: GraphStore) -> FlowDiagram:
249
+ """Runtime architecture -- modules, endpoints, entities, messaging, grouped by layer."""
250
+ subgraphs = []
251
+ edges = []
252
+
253
+ endpoints = store.nodes_by_kind(NodeKind.ENDPOINT)
254
+ entities = store.nodes_by_kind(NodeKind.ENTITY)
255
+ topics = store.nodes_by_kind(NodeKind.TOPIC) + store.nodes_by_kind(NodeKind.QUEUE)
256
+ db_conns = store.nodes_by_kind(NodeKind.DATABASE_CONNECTION)
257
+
258
+ # Group by layer
259
+ frontend_nodes = []
260
+ backend_nodes = []
261
+ data_nodes = []
262
+
263
+ if endpoints:
264
+ # Group endpoints by layer
265
+ fe_ep = [e for e in endpoints if e.properties.get("layer") == "frontend"]
266
+ be_ep = [e for e in endpoints if e.properties.get("layer") != "frontend"]
267
+ if fe_ep:
268
+ frontend_nodes.append(FlowNode(id="rt_fe_endpoints", label=f"Frontend Routes x{len(fe_ep)}", kind="endpoint"))
269
+ if be_ep:
270
+ backend_nodes.append(FlowNode(id="rt_be_endpoints", label=f"API Endpoints x{len(be_ep)}", kind="endpoint",
271
+ properties={"count": len(be_ep)}))
272
+
273
+ components = store.nodes_by_kind(NodeKind.COMPONENT)
274
+ if components:
275
+ frontend_nodes.append(FlowNode(id="rt_components", label=f"Components x{len(components)}", kind="component"))
276
+
277
+ if entities:
278
+ data_nodes.append(FlowNode(id="rt_entities", label=f"Entities x{len(entities)}", kind="entity"))
279
+ if db_conns:
280
+ data_nodes.append(FlowNode(id="rt_database", label=f"DB Connections x{len(db_conns)}", kind="database"))
281
+ if topics:
282
+ backend_nodes.append(FlowNode(id="rt_messaging", label=f"Messaging x{len(topics)}", kind="messaging"))
283
+
284
+ if frontend_nodes:
285
+ subgraphs.append(FlowSubgraph(id="frontend", label="Frontend", nodes=frontend_nodes))
286
+ if backend_nodes:
287
+ subgraphs.append(FlowSubgraph(id="backend", label="Backend", nodes=backend_nodes))
288
+ if data_nodes:
289
+ subgraphs.append(FlowSubgraph(id="data", label="Data Layer", nodes=data_nodes))
290
+
291
+ # Edges
292
+ if frontend_nodes and backend_nodes:
293
+ fe_id = frontend_nodes[0].id
294
+ be_id = backend_nodes[0].id
295
+ edges.append(FlowEdge(source=fe_id, target=be_id, label="calls"))
296
+ if backend_nodes and data_nodes:
297
+ be_id = backend_nodes[0].id
298
+ dt_id = data_nodes[0].id
299
+ edges.append(FlowEdge(source=be_id, target=dt_id, label="queries"))
300
+
301
+ return FlowDiagram(title="Runtime Architecture", view="runtime", subgraphs=subgraphs, edges=edges,
302
+ stats={"endpoints": len(endpoints), "entities": len(entities), "components": len(components),
303
+ "topics": len(topics), "db_connections": len(db_conns)})
304
+
305
+
306
+ def build_auth_view(store: GraphStore) -> FlowDiagram:
307
+ """Auth overview -- guards, endpoints, protection coverage."""
308
+ subgraphs = []
309
+ edges = []
310
+
311
+ guards = sorted(store.nodes_by_kind(NodeKind.GUARD), key=lambda n: n.id)
312
+ middleware = sorted(store.nodes_by_kind(NodeKind.MIDDLEWARE), key=lambda n: n.id)
313
+ endpoints = sorted(store.nodes_by_kind(NodeKind.ENDPOINT), key=lambda n: n.id)
314
+ protects_edges = store.edges_by_kind(EdgeKind.PROTECTS)
315
+
316
+ protected_ids = {e.target for e in protects_edges}
317
+ protected_endpoints = [e for e in endpoints if e.id in protected_ids]
318
+ unprotected_endpoints = [e for e in endpoints if e.id not in protected_ids]
319
+
320
+ # Group guards by auth_type
321
+ guards_by_type: dict[str, list] = {}
322
+ for g in guards:
323
+ auth_type = g.properties.get("auth_type", "unknown")
324
+ guards_by_type.setdefault(auth_type, []).append(g)
325
+
326
+ guard_nodes = []
327
+ for auth_type, type_guards in sorted(guards_by_type.items()):
328
+ guard_nodes.append(FlowNode(id=f"auth_{auth_type}", label=f"{auth_type} x{len(type_guards)}", kind="guard",
329
+ properties={"auth_type": auth_type, "count": len(type_guards)}))
330
+ if middleware:
331
+ guard_nodes.append(FlowNode(id="auth_middleware", label=f"Middleware x{len(middleware)}", kind="middleware",
332
+ properties={"count": len(middleware)}))
333
+ if guard_nodes:
334
+ subgraphs.append(FlowSubgraph(id="guards", label="Auth Guards", nodes=guard_nodes))
335
+
336
+ # Endpoint coverage
337
+ ep_nodes = []
338
+ if protected_endpoints:
339
+ ep_nodes.append(FlowNode(id="ep_protected", label=f"Protected x{len(protected_endpoints)}", kind="endpoint",
340
+ style="success", properties={"count": len(protected_endpoints)}))
341
+ if unprotected_endpoints:
342
+ ep_nodes.append(FlowNode(id="ep_unprotected", label=f"Unprotected x{len(unprotected_endpoints)}", kind="endpoint",
343
+ style="danger", properties={"count": len(unprotected_endpoints)}))
344
+ if ep_nodes:
345
+ subgraphs.append(FlowSubgraph(id="endpoints", label="Endpoints", nodes=ep_nodes))
346
+
347
+ # Edges: guards -> protected
348
+ for gn in guard_nodes:
349
+ if any(n.id == "ep_protected" for n in ep_nodes):
350
+ edges.append(FlowEdge(source=gn.id, target="ep_protected", label="protects", style="thick"))
351
+
352
+ coverage = len(protected_endpoints) / len(endpoints) * 100 if endpoints else 0
353
+
354
+ return FlowDiagram(title="Auth & Security", view="auth", subgraphs=subgraphs, edges=edges,
355
+ stats={"guards": len(guards), "middleware": len(middleware),
356
+ "protected": len(protected_endpoints), "unprotected": len(unprotected_endpoints),
357
+ "coverage_pct": round(coverage, 1)})
File without changes
@@ -0,0 +1,52 @@
1
+ """Protocol definitions for graph storage backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+ from osscodeiq.models.graph import (
8
+ EdgeKind,
9
+ GraphEdge,
10
+ GraphNode,
11
+ NodeKind,
12
+ )
13
+
14
+
15
+ @runtime_checkable
16
+ class GraphBackend(Protocol):
17
+ """Contract that every graph storage backend must satisfy."""
18
+
19
+ def add_node(self, node: GraphNode) -> None: ...
20
+ def add_edge(self, edge: GraphEdge) -> None: ...
21
+ def clear(self) -> None: ...
22
+ def get_node(self, node_id: str) -> GraphNode | None: ...
23
+ def has_node(self, node_id: str) -> bool: ...
24
+ def get_edges_between(self, source: str, target: str) -> list[GraphEdge]: ...
25
+ def all_nodes(self) -> list[GraphNode]: ...
26
+ def all_edges(self) -> list[GraphEdge]: ...
27
+ def nodes_by_kind(self, kind: NodeKind) -> list[GraphNode]: ...
28
+ def edges_by_kind(self, kind: EdgeKind) -> list[GraphEdge]: ...
29
+
30
+ @property
31
+ def node_count(self) -> int: ...
32
+ @property
33
+ def edge_count(self) -> int: ...
34
+
35
+ def neighbors(
36
+ self, node_id: str,
37
+ edge_kinds: set[EdgeKind] | None = None,
38
+ direction: str = "both",
39
+ ) -> list[str]: ...
40
+
41
+ def find_cycles(self, limit: int = 100) -> list[list[str]]: ...
42
+ def shortest_path(self, source: str, target: str) -> list[str] | None: ...
43
+ def subgraph(self, node_ids: set[str]) -> GraphBackend: ...
44
+ def update_node_properties(self, node_id: str, properties: dict[str, Any]) -> None: ...
45
+ def close(self) -> None: ...
46
+
47
+
48
+ @runtime_checkable
49
+ class CypherBackend(Protocol):
50
+ """Optional capability for backends supporting Cypher queries."""
51
+
52
+ def query_cypher(self, cypher: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]: ...
@@ -0,0 +1,23 @@
1
+ """Graph backend factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from osscodeiq.graph.backend import GraphBackend
9
+
10
+
11
+ def create_backend(backend_name: str = "networkx", **kwargs) -> GraphBackend:
12
+ """Create a graph backend by name."""
13
+ if backend_name == "networkx":
14
+ from osscodeiq.graph.backends.networkx import NetworkXBackend
15
+ return NetworkXBackend()
16
+ elif backend_name == "kuzu":
17
+ from osscodeiq.graph.backends.kuzu import KuzuBackend
18
+ return KuzuBackend(db_path=kwargs.get("path", ".code-intelligence/graph.kuzu"))
19
+ elif backend_name == "sqlite":
20
+ from osscodeiq.graph.backends.sqlite_backend import SqliteGraphBackend
21
+ return SqliteGraphBackend(db_path=kwargs.get("path", ".code-intelligence/graph.db"))
22
+ else:
23
+ raise ValueError(f"Unknown graph backend: {backend_name}")