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/cache/store.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""SQLite-backed cache store for incremental analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sqlite3
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from osscodeiq.graph.store import GraphStore
|
|
14
|
+
from osscodeiq.models.graph import (
|
|
15
|
+
EdgeKind,
|
|
16
|
+
GraphEdge,
|
|
17
|
+
GraphNode,
|
|
18
|
+
NodeKind,
|
|
19
|
+
SourceLocation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_SCHEMA_SQL = """\
|
|
25
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
26
|
+
content_hash TEXT PRIMARY KEY,
|
|
27
|
+
path TEXT NOT NULL,
|
|
28
|
+
language TEXT NOT NULL,
|
|
29
|
+
parsed_at TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
content_hash TEXT NOT NULL,
|
|
35
|
+
kind TEXT NOT NULL,
|
|
36
|
+
data JSON NOT NULL,
|
|
37
|
+
FOREIGN KEY (content_hash) REFERENCES files(content_hash)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
41
|
+
source TEXT NOT NULL,
|
|
42
|
+
target TEXT NOT NULL,
|
|
43
|
+
content_hash TEXT NOT NULL,
|
|
44
|
+
kind TEXT NOT NULL,
|
|
45
|
+
data JSON NOT NULL,
|
|
46
|
+
FOREIGN KEY (content_hash) REFERENCES files(content_hash)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS analysis_runs (
|
|
50
|
+
run_id TEXT PRIMARY KEY,
|
|
51
|
+
commit_sha TEXT,
|
|
52
|
+
timestamp TEXT NOT NULL,
|
|
53
|
+
file_count INTEGER NOT NULL
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_content_hash ON nodes(content_hash);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_edges_content_hash ON edges(content_hash);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_analysis_runs_timestamp ON analysis_runs(timestamp);
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _node_to_dict(node: GraphNode) -> dict[str, Any]:
|
|
63
|
+
"""Serialize a GraphNode to a JSON-safe dictionary."""
|
|
64
|
+
return node.model_dump(mode="json")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _dict_to_node(data: dict[str, Any]) -> GraphNode:
|
|
68
|
+
"""Deserialize a dictionary to a GraphNode."""
|
|
69
|
+
return GraphNode.model_validate(data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _edge_to_dict(edge: GraphEdge) -> dict[str, Any]:
|
|
73
|
+
"""Serialize a GraphEdge to a JSON-safe dictionary."""
|
|
74
|
+
return edge.model_dump(mode="json")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dict_to_edge(data: dict[str, Any]) -> GraphEdge:
|
|
78
|
+
"""Deserialize a dictionary to a GraphEdge."""
|
|
79
|
+
return GraphEdge.model_validate(data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CacheStore:
|
|
83
|
+
"""SQLite-backed cache for analysis results.
|
|
84
|
+
|
|
85
|
+
Stores per-file parse results (nodes and edges) keyed by content hash,
|
|
86
|
+
enabling fast incremental re-analysis when only a subset of files change.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, db_path: Path) -> None:
|
|
90
|
+
self._db_path = db_path
|
|
91
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
self._conn = sqlite3.connect(str(db_path))
|
|
93
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
94
|
+
self._conn.execute("PRAGMA busy_timeout=5000")
|
|
95
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
96
|
+
self.init_db()
|
|
97
|
+
|
|
98
|
+
def init_db(self) -> None:
|
|
99
|
+
"""Create schema tables if they do not already exist."""
|
|
100
|
+
with self._conn:
|
|
101
|
+
self._conn.executescript(_SCHEMA_SQL)
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
"""Close the database connection."""
|
|
105
|
+
self._conn.close()
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Commit tracking
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def get_last_commit(self) -> str | None:
|
|
112
|
+
"""Return the commit SHA from the most recent analysis run, or None."""
|
|
113
|
+
row = self._conn.execute(
|
|
114
|
+
"SELECT commit_sha FROM analysis_runs "
|
|
115
|
+
"ORDER BY timestamp DESC LIMIT 1"
|
|
116
|
+
).fetchone()
|
|
117
|
+
if row is None or row[0] is None:
|
|
118
|
+
return None
|
|
119
|
+
return row[0]
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Cache lookups
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def is_cached(self, content_hash: str) -> bool:
|
|
126
|
+
"""Check whether results for *content_hash* are in the cache."""
|
|
127
|
+
row = self._conn.execute(
|
|
128
|
+
"SELECT 1 FROM files WHERE content_hash = ?", (content_hash,)
|
|
129
|
+
).fetchone()
|
|
130
|
+
return row is not None
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Store / load results
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def store_results(
|
|
137
|
+
self,
|
|
138
|
+
content_hash: str,
|
|
139
|
+
file_path: str,
|
|
140
|
+
language: str,
|
|
141
|
+
nodes: list[GraphNode],
|
|
142
|
+
edges: list[GraphEdge],
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Persist analysis results for a single file."""
|
|
145
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
146
|
+
with self._conn:
|
|
147
|
+
# Upsert file record
|
|
148
|
+
self._conn.execute(
|
|
149
|
+
"INSERT OR REPLACE INTO files (content_hash, path, language, parsed_at) "
|
|
150
|
+
"VALUES (?, ?, ?, ?)",
|
|
151
|
+
(content_hash, file_path, language, now),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Remove old nodes/edges for this hash (idempotent re-store)
|
|
155
|
+
self._conn.execute(
|
|
156
|
+
"DELETE FROM nodes WHERE content_hash = ?", (content_hash,)
|
|
157
|
+
)
|
|
158
|
+
self._conn.execute(
|
|
159
|
+
"DELETE FROM edges WHERE content_hash = ?", (content_hash,)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Insert nodes
|
|
163
|
+
for node in nodes:
|
|
164
|
+
self._conn.execute(
|
|
165
|
+
"INSERT OR IGNORE INTO nodes (id, content_hash, kind, data) "
|
|
166
|
+
"VALUES (?, ?, ?, ?)",
|
|
167
|
+
(
|
|
168
|
+
node.id,
|
|
169
|
+
content_hash,
|
|
170
|
+
node.kind.value,
|
|
171
|
+
json.dumps(_node_to_dict(node)),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Insert edges
|
|
176
|
+
for edge in edges:
|
|
177
|
+
self._conn.execute(
|
|
178
|
+
"INSERT INTO edges (source, target, content_hash, kind, data) "
|
|
179
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
180
|
+
(
|
|
181
|
+
edge.source,
|
|
182
|
+
edge.target,
|
|
183
|
+
content_hash,
|
|
184
|
+
edge.kind.value,
|
|
185
|
+
json.dumps(_edge_to_dict(edge)),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def load_cached_results(
|
|
190
|
+
self, content_hash: str
|
|
191
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
192
|
+
"""Load cached nodes and edges for a given content hash."""
|
|
193
|
+
node_rows = self._conn.execute(
|
|
194
|
+
"SELECT data FROM nodes WHERE content_hash = ?", (content_hash,)
|
|
195
|
+
).fetchall()
|
|
196
|
+
edge_rows = self._conn.execute(
|
|
197
|
+
"SELECT data FROM edges WHERE content_hash = ?", (content_hash,)
|
|
198
|
+
).fetchall()
|
|
199
|
+
|
|
200
|
+
nodes = [_dict_to_node(json.loads(row[0])) for row in node_rows]
|
|
201
|
+
edges = [_dict_to_edge(json.loads(row[0])) for row in edge_rows]
|
|
202
|
+
return nodes, edges
|
|
203
|
+
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
# Cache invalidation
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def remove_file(self, content_hash: str) -> None:
|
|
209
|
+
"""Delete all cached results associated with *content_hash*."""
|
|
210
|
+
with self._conn:
|
|
211
|
+
self._conn.execute(
|
|
212
|
+
"DELETE FROM nodes WHERE content_hash = ?", (content_hash,)
|
|
213
|
+
)
|
|
214
|
+
self._conn.execute(
|
|
215
|
+
"DELETE FROM edges WHERE content_hash = ?", (content_hash,)
|
|
216
|
+
)
|
|
217
|
+
self._conn.execute(
|
|
218
|
+
"DELETE FROM files WHERE content_hash = ?", (content_hash,)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def remove_by_path(self, file_path: str) -> None:
|
|
222
|
+
"""Remove all cache entries associated with *file_path*."""
|
|
223
|
+
with self._conn:
|
|
224
|
+
rows = self._conn.execute(
|
|
225
|
+
"SELECT content_hash FROM files WHERE path = ?", (file_path,)
|
|
226
|
+
).fetchall()
|
|
227
|
+
for (content_hash,) in rows:
|
|
228
|
+
self.remove_file(content_hash)
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Run tracking
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def record_run(self, commit_sha: str, file_count: int) -> None:
|
|
235
|
+
"""Record an analysis run with its commit SHA and file count."""
|
|
236
|
+
run_id = uuid.uuid4().hex
|
|
237
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
238
|
+
with self._conn:
|
|
239
|
+
self._conn.execute(
|
|
240
|
+
"INSERT INTO analysis_runs (run_id, commit_sha, timestamp, file_count) "
|
|
241
|
+
"VALUES (?, ?, ?, ?)",
|
|
242
|
+
(run_id, commit_sha, now, file_count),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
# Bulk operations
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def load_full_graph(self) -> GraphStore:
|
|
250
|
+
"""Load all cached nodes and edges into a fresh GraphStore."""
|
|
251
|
+
store = GraphStore()
|
|
252
|
+
|
|
253
|
+
cursor = self._conn.execute("SELECT data FROM nodes")
|
|
254
|
+
for (data_json,) in cursor:
|
|
255
|
+
store.add_node(_dict_to_node(json.loads(data_json)))
|
|
256
|
+
|
|
257
|
+
cursor = self._conn.execute("SELECT data FROM edges")
|
|
258
|
+
for (data_json,) in cursor:
|
|
259
|
+
store.add_edge(_dict_to_edge(json.loads(data_json)))
|
|
260
|
+
|
|
261
|
+
return store
|
|
262
|
+
|
|
263
|
+
# ------------------------------------------------------------------
|
|
264
|
+
# Statistics
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def get_stats(self) -> dict[str, Any]:
|
|
268
|
+
"""Return cache statistics."""
|
|
269
|
+
file_count = self._conn.execute(
|
|
270
|
+
"SELECT COUNT(*) FROM files"
|
|
271
|
+
).fetchone()[0]
|
|
272
|
+
node_count = self._conn.execute(
|
|
273
|
+
"SELECT COUNT(*) FROM nodes"
|
|
274
|
+
).fetchone()[0]
|
|
275
|
+
edge_count = self._conn.execute(
|
|
276
|
+
"SELECT COUNT(*) FROM edges"
|
|
277
|
+
).fetchone()[0]
|
|
278
|
+
run_count = self._conn.execute(
|
|
279
|
+
"SELECT COUNT(*) FROM analysis_runs"
|
|
280
|
+
).fetchone()[0]
|
|
281
|
+
|
|
282
|
+
last_run = self._conn.execute(
|
|
283
|
+
"SELECT timestamp, commit_sha, file_count FROM analysis_runs "
|
|
284
|
+
"ORDER BY timestamp DESC LIMIT 1"
|
|
285
|
+
).fetchone()
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
"cached_files": file_count,
|
|
289
|
+
"cached_nodes": node_count,
|
|
290
|
+
"cached_edges": edge_count,
|
|
291
|
+
"total_runs": run_count,
|
|
292
|
+
"last_run": {
|
|
293
|
+
"timestamp": last_run[0],
|
|
294
|
+
"commit_sha": last_run[1],
|
|
295
|
+
"file_count": last_run[2],
|
|
296
|
+
}
|
|
297
|
+
if last_run
|
|
298
|
+
else None,
|
|
299
|
+
"db_path": str(self._db_path),
|
|
300
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Deterministic layer classifier for OSSCodeIQ graph nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
from osscodeiq.models.graph import GraphNode, NodeKind
|
|
9
|
+
|
|
10
|
+
_FRONTEND_NODE_KINDS = {NodeKind.COMPONENT, NodeKind.HOOK}
|
|
11
|
+
_BACKEND_NODE_KINDS = {NodeKind.GUARD, NodeKind.MIDDLEWARE, NodeKind.ENDPOINT, NodeKind.REPOSITORY, NodeKind.DATABASE_CONNECTION, NodeKind.QUERY}
|
|
12
|
+
_INFRA_NODE_KINDS = {NodeKind.INFRA_RESOURCE, NodeKind.AZURE_RESOURCE, NodeKind.AZURE_FUNCTION}
|
|
13
|
+
_INFRA_LANGUAGES = {"terraform", "bicep", "dockerfile"}
|
|
14
|
+
_SHARED_NODE_KINDS = {NodeKind.CONFIG_FILE, NodeKind.CONFIG_KEY, NodeKind.CONFIG_DEFINITION}
|
|
15
|
+
|
|
16
|
+
_FRONTEND_PATH_RE = re.compile(
|
|
17
|
+
r"(?:^|/)(?:src/)?(?:components|pages|views|app/ui|public)/",
|
|
18
|
+
)
|
|
19
|
+
_BACKEND_PATH_RE = re.compile(
|
|
20
|
+
r"(?:^|/)(?:src/)?(?:server|api|controllers|services|routes|handlers)/",
|
|
21
|
+
)
|
|
22
|
+
_FRONTEND_EXT_RE = re.compile(r"\.(?:tsx|jsx)$")
|
|
23
|
+
|
|
24
|
+
_FRONTEND_FRAMEWORKS = {"react", "vue", "angular", "svelte", "nextjs"}
|
|
25
|
+
_BACKEND_FRAMEWORKS = {"express", "nestjs", "flask", "django", "fastapi", "spring"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LayerClassifier:
|
|
29
|
+
"""Assigns a deterministic 'layer' property to every graph node.
|
|
30
|
+
Rules are evaluated in order; first match wins.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def classify(self, nodes: Sequence[GraphNode]) -> None:
|
|
34
|
+
for node in nodes:
|
|
35
|
+
node.properties["layer"] = self._classify_one(node)
|
|
36
|
+
|
|
37
|
+
def classify_store(self, store: Any) -> None:
|
|
38
|
+
"""Classify nodes in a GraphStore, updating properties via public API."""
|
|
39
|
+
for node in store.all_nodes():
|
|
40
|
+
layer = self._classify_one(node)
|
|
41
|
+
store.update_node_properties(node.id, {"layer": layer})
|
|
42
|
+
|
|
43
|
+
def _classify_one(self, node: GraphNode) -> str:
|
|
44
|
+
if node.kind in _FRONTEND_NODE_KINDS:
|
|
45
|
+
return "frontend"
|
|
46
|
+
if node.kind in _BACKEND_NODE_KINDS:
|
|
47
|
+
return "backend"
|
|
48
|
+
if node.kind in _INFRA_NODE_KINDS:
|
|
49
|
+
return "infra"
|
|
50
|
+
lang = node.properties.get("language", "")
|
|
51
|
+
if lang in _INFRA_LANGUAGES:
|
|
52
|
+
return "infra"
|
|
53
|
+
file_path = ""
|
|
54
|
+
if node.location:
|
|
55
|
+
file_path = node.location.file_path
|
|
56
|
+
if _FRONTEND_EXT_RE.search(file_path):
|
|
57
|
+
return "frontend"
|
|
58
|
+
if _FRONTEND_PATH_RE.search(file_path):
|
|
59
|
+
return "frontend"
|
|
60
|
+
if _BACKEND_PATH_RE.search(file_path):
|
|
61
|
+
return "backend"
|
|
62
|
+
fw = node.properties.get("framework", "")
|
|
63
|
+
if fw in _FRONTEND_FRAMEWORKS:
|
|
64
|
+
return "frontend"
|
|
65
|
+
if fw in _BACKEND_FRAMEWORKS:
|
|
66
|
+
return "backend"
|
|
67
|
+
if node.kind in _SHARED_NODE_KINDS:
|
|
68
|
+
return "shared"
|
|
69
|
+
return "unknown"
|