codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__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.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/runtime/runner.py
CHANGED
|
@@ -35,13 +35,35 @@ const fs = require('fs');
|
|
|
35
35
|
|
|
36
36
|
const logPath = {escaped_log_path};
|
|
37
37
|
|
|
38
|
+
let _cg_buffer = [];
|
|
39
|
+
let _cg_last_flush = Date.now();
|
|
40
|
+
const _CG_BATCH_SIZE = 50;
|
|
41
|
+
const _CG_FLUSH_INTERVAL_MS = 1500;
|
|
42
|
+
|
|
43
|
+
function _cg_flush_buffer() {{
|
|
44
|
+
if (_cg_buffer.length === 0) return;
|
|
45
|
+
const content = _cg_buffer.join('');
|
|
46
|
+
_cg_buffer = [];
|
|
47
|
+
_cg_last_flush = Date.now();
|
|
48
|
+
try {{
|
|
49
|
+
fs.appendFileSync(logPath, content);
|
|
50
|
+
}} catch (_) {{}}
|
|
51
|
+
}}
|
|
52
|
+
|
|
38
53
|
function emitEvent(event) {{
|
|
39
54
|
try {{
|
|
40
|
-
|
|
55
|
+
_cg_buffer.push(JSON.stringify(event) + '\\n');
|
|
56
|
+
if (_cg_buffer.length >= _CG_BATCH_SIZE || (Date.now() - _cg_last_flush) >= _CG_FLUSH_INTERVAL_MS) {{
|
|
57
|
+
_cg_flush_buffer();
|
|
58
|
+
}}
|
|
41
59
|
}} catch (_) {{}}
|
|
42
60
|
}}
|
|
43
61
|
|
|
62
|
+
setInterval(_cg_flush_buffer, _CG_FLUSH_INTERVAL_MS);
|
|
63
|
+
process.on('exit', _cg_flush_buffer);
|
|
64
|
+
|
|
44
65
|
function hookServer(serverProto) {{
|
|
66
|
+
|
|
45
67
|
const origEmit = serverProto.emit;
|
|
46
68
|
serverProto.emit = function(event, req, res) {{
|
|
47
69
|
if (event === 'request' && req && res) {{
|
|
@@ -87,23 +109,64 @@ process.on('uncaughtExceptionMonitor', (err) => {{
|
|
|
87
109
|
|
|
88
110
|
|
|
89
111
|
def _generate_python_interceptor(output_jsonl: Path) -> str:
|
|
90
|
-
"""Generate lightweight Python WSGI/ASGI/FastAPI request interceptor."""
|
|
112
|
+
"""Generate lightweight Python WSGI/ASGI/FastAPI request interceptor with in-memory buffering."""
|
|
91
113
|
escaped_log_path = repr(str(output_jsonl))
|
|
92
114
|
return f"""# CodeGraph Python Runtime Interceptor
|
|
93
|
-
import
|
|
115
|
+
import atexit
|
|
94
116
|
import json
|
|
117
|
+
import os
|
|
118
|
+
import sys
|
|
119
|
+
import threading
|
|
95
120
|
import time
|
|
96
121
|
|
|
97
122
|
LOG_PATH = {escaped_log_path}
|
|
98
123
|
|
|
124
|
+
_cg_buffer = []
|
|
125
|
+
_cg_lock = threading.Lock()
|
|
126
|
+
_cg_last_flush = time.time()
|
|
127
|
+
_cg_batch_size = 50
|
|
128
|
+
_cg_flush_interval = 1.5
|
|
129
|
+
|
|
130
|
+
def _cg_flush():
|
|
131
|
+
global _cg_last_flush
|
|
132
|
+
lines = None
|
|
133
|
+
with _cg_lock:
|
|
134
|
+
if _cg_buffer:
|
|
135
|
+
lines = "".join(_cg_buffer)
|
|
136
|
+
_cg_buffer.clear()
|
|
137
|
+
_cg_last_flush = time.time()
|
|
138
|
+
if lines:
|
|
139
|
+
try:
|
|
140
|
+
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
141
|
+
f.write(lines)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
99
145
|
def emit_event(event):
|
|
100
146
|
try:
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
line = json.dumps(event) + "\\n"
|
|
148
|
+
should_flush = False
|
|
149
|
+
with _cg_lock:
|
|
150
|
+
_cg_buffer.append(line)
|
|
151
|
+
if len(_cg_buffer) >= _cg_batch_size or (time.time() - _cg_last_flush) >= _cg_flush_interval:
|
|
152
|
+
should_flush = True
|
|
153
|
+
if should_flush:
|
|
154
|
+
_cg_flush()
|
|
103
155
|
except Exception:
|
|
104
156
|
pass
|
|
105
157
|
|
|
106
|
-
|
|
158
|
+
def _cg_bg_flusher():
|
|
159
|
+
while True:
|
|
160
|
+
time.sleep(_cg_flush_interval)
|
|
161
|
+
if _cg_buffer:
|
|
162
|
+
_cg_flush()
|
|
163
|
+
|
|
164
|
+
_cg_flusher_thread = threading.Thread(target=_cg_bg_flusher, daemon=True)
|
|
165
|
+
_cg_flusher_thread.start()
|
|
166
|
+
atexit.register(_cg_flush)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# 1. Intercept uncaught exceptions
|
|
107
170
|
_orig_excepthook = sys.excepthook
|
|
108
171
|
def _cg_excepthook(exc_type, exc_value, exc_traceback):
|
|
109
172
|
emit_event({{
|
|
@@ -118,6 +181,92 @@ def _cg_excepthook(exc_type, exc_value, exc_traceback):
|
|
|
118
181
|
return _orig_excepthook(exc_type, exc_value, exc_traceback)
|
|
119
182
|
|
|
120
183
|
sys.excepthook = _cg_excepthook
|
|
184
|
+
|
|
185
|
+
# 2. Intercept SQLite query executions
|
|
186
|
+
try:
|
|
187
|
+
import sqlite3
|
|
188
|
+
_orig_connect = sqlite3.connect
|
|
189
|
+
|
|
190
|
+
def _cg_connect(*args, **kwargs):
|
|
191
|
+
con = _orig_connect(*args, **kwargs)
|
|
192
|
+
def _sql_trace(statement):
|
|
193
|
+
if statement and isinstance(statement, str) and statement.strip():
|
|
194
|
+
emit_event({{
|
|
195
|
+
"trace_id": "tr_sql_" + str(int(time.time())),
|
|
196
|
+
"span_id": "sp_sql_" + str(id(con)),
|
|
197
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
198
|
+
"sql": statement,
|
|
199
|
+
"duration_ms": 1.0,
|
|
200
|
+
"service_name": "python_dev_server",
|
|
201
|
+
"source_format": "otel",
|
|
202
|
+
}})
|
|
203
|
+
try:
|
|
204
|
+
con.set_trace_callback(_sql_trace)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
return con
|
|
208
|
+
|
|
209
|
+
sqlite3.connect = _cg_connect
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 3. Intercept SQLAlchemy engine query executions if imported
|
|
215
|
+
try:
|
|
216
|
+
import sqlalchemy.event
|
|
217
|
+
import sqlalchemy.engine
|
|
218
|
+
|
|
219
|
+
@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "after_cursor_execute")
|
|
220
|
+
def _cg_after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
|
221
|
+
if statement and str(statement).strip():
|
|
222
|
+
emit_event({{
|
|
223
|
+
"trace_id": "tr_sql_" + str(int(time.time())),
|
|
224
|
+
"span_id": "sp_sql_" + str(id(cursor)),
|
|
225
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
226
|
+
"sql": str(statement),
|
|
227
|
+
"duration_ms": 1.0,
|
|
228
|
+
"service_name": "python_dev_server",
|
|
229
|
+
"source_format": "otel",
|
|
230
|
+
}})
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# 4. Intercept Werkzeug/Flask dev server requests
|
|
235
|
+
try:
|
|
236
|
+
import werkzeug.serving
|
|
237
|
+
_orig_run_simple = werkzeug.serving.run_simple
|
|
238
|
+
|
|
239
|
+
def _cg_run_simple(hostname, port, application, *args, **kwargs):
|
|
240
|
+
def _wrapped_app(environ, start_response):
|
|
241
|
+
path = environ.get("PATH_INFO", "/")
|
|
242
|
+
method = environ.get("REQUEST_METHOD", "GET")
|
|
243
|
+
status_box = [200]
|
|
244
|
+
def _wrapped_sr(status, headers, exc_info=None):
|
|
245
|
+
try:
|
|
246
|
+
status_box[0] = int(status.split()[0])
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
return start_response(status, headers, exc_info)
|
|
250
|
+
st = time.time()
|
|
251
|
+
try:
|
|
252
|
+
return application(environ, _wrapped_sr)
|
|
253
|
+
finally:
|
|
254
|
+
emit_event({{
|
|
255
|
+
"trace_id": "tr_http_" + str(int(time.time())),
|
|
256
|
+
"span_id": "sp_http_" + str(int(time.time())),
|
|
257
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
258
|
+
"http_method": method,
|
|
259
|
+
"route": path,
|
|
260
|
+
"status_code": status_box[0],
|
|
261
|
+
"duration_ms": (time.time() - st) * 1000.0,
|
|
262
|
+
"service_name": "python_dev_server",
|
|
263
|
+
"source_format": "otel",
|
|
264
|
+
}})
|
|
265
|
+
return _orig_run_simple(hostname, port, _wrapped_app, *args, **kwargs)
|
|
266
|
+
|
|
267
|
+
werkzeug.serving.run_simple = _cg_run_simple
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
121
270
|
"""
|
|
122
271
|
|
|
123
272
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Cross-Service & Multi-Localhost Distributed Trace Linker.
|
|
2
|
+
|
|
3
|
+
Stitches W3C distributed traces across multiple localhost ports:
|
|
4
|
+
- Frontend (e.g. Next.js on localhost:3000)
|
|
5
|
+
- Backend API (e.g. FastAPI / NestJS on localhost:8000)
|
|
6
|
+
- Database mutation (e.g. PostgreSQL on localhost:5432)
|
|
7
|
+
|
|
8
|
+
Attributes every step to:
|
|
9
|
+
1. Participating localhost service & port.
|
|
10
|
+
2. Static AST symbol / route handler.
|
|
11
|
+
3. Actual latency and HTTP status code.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class TraceSpanNode:
|
|
23
|
+
span_id: str
|
|
24
|
+
parent_span_id: str
|
|
25
|
+
service_name: str
|
|
26
|
+
port: int | None
|
|
27
|
+
route_path: str
|
|
28
|
+
handler_symbol: str
|
|
29
|
+
db_operation: str
|
|
30
|
+
db_table: str
|
|
31
|
+
status_code: int | None
|
|
32
|
+
duration_ms: float
|
|
33
|
+
children: tuple[TraceSpanNode, ...] = ()
|
|
34
|
+
|
|
35
|
+
def as_dict(self) -> dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"span_id": self.span_id,
|
|
38
|
+
"parent_span_id": self.parent_span_id,
|
|
39
|
+
"service_name": self.service_name,
|
|
40
|
+
"port": self.port,
|
|
41
|
+
"route_path": self.route_path,
|
|
42
|
+
"handler_symbol": self.handler_symbol,
|
|
43
|
+
"db_operation": self.db_operation,
|
|
44
|
+
"db_table": self.db_table,
|
|
45
|
+
"status_code": self.status_code,
|
|
46
|
+
"duration_ms": self.duration_ms,
|
|
47
|
+
"children": [c.as_dict() for c in self.children],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class DistributedTraceReport:
|
|
53
|
+
trace_id: str
|
|
54
|
+
total_duration_ms: float
|
|
55
|
+
services_involved: tuple[dict[str, Any], ...]
|
|
56
|
+
root_spans: tuple[TraceSpanNode, ...]
|
|
57
|
+
db_queries_count: int
|
|
58
|
+
has_errors: bool
|
|
59
|
+
summary: str
|
|
60
|
+
|
|
61
|
+
def as_dict(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"trace_id": self.trace_id,
|
|
64
|
+
"total_duration_ms": self.total_duration_ms,
|
|
65
|
+
"services_involved": list(self.services_involved),
|
|
66
|
+
"root_spans": [s.as_dict() for s in self.root_spans],
|
|
67
|
+
"db_queries_count": self.db_queries_count,
|
|
68
|
+
"has_errors": self.has_errors,
|
|
69
|
+
"summary": self.summary,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def link_distributed_trace(
|
|
74
|
+
con: sqlite3.Connection,
|
|
75
|
+
repo_root: Path,
|
|
76
|
+
trace_id: str,
|
|
77
|
+
) -> DistributedTraceReport | None:
|
|
78
|
+
"""Reconstruct multi-service distributed execution graph for a given trace_id."""
|
|
79
|
+
clean_tid = trace_id.strip()
|
|
80
|
+
if not clean_tid:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
rows = con.execute(
|
|
84
|
+
"""SELECT event_id, trace_id, span_id, parent_span_id, http_method, route_path,
|
|
85
|
+
handler_symbol, service_name, db_operation, db_table, status_code,
|
|
86
|
+
duration_ms
|
|
87
|
+
FROM runtime_observations
|
|
88
|
+
WHERE trace_id = ?
|
|
89
|
+
ORDER BY duration_ms DESC""",
|
|
90
|
+
(clean_tid,),
|
|
91
|
+
).fetchall()
|
|
92
|
+
|
|
93
|
+
if not rows:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Group spans
|
|
97
|
+
spans_by_id: dict[str, dict[str, Any]] = {}
|
|
98
|
+
children_by_parent: dict[str, list[str]] = {}
|
|
99
|
+
services_set: set[str] = set()
|
|
100
|
+
db_queries = 0
|
|
101
|
+
has_err = False
|
|
102
|
+
total_time = 0.0
|
|
103
|
+
|
|
104
|
+
for r in rows:
|
|
105
|
+
sid = str(r["span_id"] or r["event_id"])
|
|
106
|
+
pid = str(r["parent_span_id"] or "")
|
|
107
|
+
s_name = str(r["service_name"] or "unknown_service")
|
|
108
|
+
services_set.add(s_name)
|
|
109
|
+
dur = float(r["duration_ms"] or 0.0)
|
|
110
|
+
total_time = max(total_time, dur)
|
|
111
|
+
sc = r["status_code"]
|
|
112
|
+
if sc and int(sc) >= 400:
|
|
113
|
+
has_err = True
|
|
114
|
+
if r["db_operation"]:
|
|
115
|
+
db_queries += 1
|
|
116
|
+
|
|
117
|
+
spans_by_id[sid] = {
|
|
118
|
+
"span_id": sid,
|
|
119
|
+
"parent_span_id": pid,
|
|
120
|
+
"service_name": s_name,
|
|
121
|
+
"port": None,
|
|
122
|
+
"route_path": str(r["route_path"] or ""),
|
|
123
|
+
"handler_symbol": str(r["handler_symbol"] or ""),
|
|
124
|
+
"db_operation": str(r["db_operation"] or ""),
|
|
125
|
+
"db_table": str(r["db_table"] or ""),
|
|
126
|
+
"status_code": sc,
|
|
127
|
+
"duration_ms": dur,
|
|
128
|
+
}
|
|
129
|
+
if pid not in children_by_parent:
|
|
130
|
+
children_by_parent[pid] = []
|
|
131
|
+
children_by_parent[pid].append(sid)
|
|
132
|
+
|
|
133
|
+
# Build tree
|
|
134
|
+
def _build_node(sid: str) -> TraceSpanNode:
|
|
135
|
+
data = spans_by_id[sid]
|
|
136
|
+
child_sids = children_by_parent.get(sid, [])
|
|
137
|
+
children = tuple(_build_node(cid) for cid in child_sids if cid in spans_by_id)
|
|
138
|
+
return TraceSpanNode(
|
|
139
|
+
span_id=data["span_id"],
|
|
140
|
+
parent_span_id=data["parent_span_id"],
|
|
141
|
+
service_name=data["service_name"],
|
|
142
|
+
port=data["port"],
|
|
143
|
+
route_path=data["route_path"],
|
|
144
|
+
handler_symbol=data["handler_symbol"],
|
|
145
|
+
db_operation=data["db_operation"],
|
|
146
|
+
db_table=data["db_table"],
|
|
147
|
+
status_code=data["status_code"],
|
|
148
|
+
duration_ms=data["duration_ms"],
|
|
149
|
+
children=children,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Root spans: parent_span_id is empty or not in spans_by_id
|
|
153
|
+
roots: list[TraceSpanNode] = []
|
|
154
|
+
for sid, data in spans_by_id.items():
|
|
155
|
+
pid = data["parent_span_id"]
|
|
156
|
+
if not pid or pid not in spans_by_id:
|
|
157
|
+
roots.append(_build_node(sid))
|
|
158
|
+
|
|
159
|
+
# Resolve services
|
|
160
|
+
services_list: list[dict[str, Any]] = [{"name": s} for s in sorted(services_set)]
|
|
161
|
+
|
|
162
|
+
summary = (
|
|
163
|
+
f"Trace {clean_tid[:8]}: {len(rows)} spans across {len(services_set)} service(s), "
|
|
164
|
+
f"{db_queries} DB queries, total duration {total_time:.1f}ms."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return DistributedTraceReport(
|
|
168
|
+
trace_id=clean_tid,
|
|
169
|
+
total_duration_ms=total_time,
|
|
170
|
+
services_involved=tuple(services_list),
|
|
171
|
+
root_spans=tuple(roots),
|
|
172
|
+
db_queries_count=db_queries,
|
|
173
|
+
has_errors=has_err,
|
|
174
|
+
summary=summary,
|
|
175
|
+
)
|
codegraph/sandbox.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""In-Memory Speculative "What-If" Execution Sandbox for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Enables AI agents to project virtual AST branches in RAM in <5ms without modifying disk:
|
|
4
|
+
- Applies unified diffs or in-memory file content substitutions
|
|
5
|
+
- Parses affected files into virtual AST nodes (Python, TS, Go, Rust, Java, C++)
|
|
6
|
+
- Computes virtual caller contract breaks, parameter mismatches, and route mutations
|
|
7
|
+
- Validates blast radius before writing code to disk
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import difflib
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from codegraph.indexing.parser import parse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class VirtualContractBreak:
|
|
21
|
+
"""A breaking change to an existing symbol contract detected in speculative branch."""
|
|
22
|
+
|
|
23
|
+
symbol: str
|
|
24
|
+
break_type: str # PARAMETER_REMOVED | TYPE_MISMATCH | RETURN_TYPE_CHANGED | SYMBOL_DELETED
|
|
25
|
+
file_path: str
|
|
26
|
+
line: int
|
|
27
|
+
impacted_callers_count: int
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
def as_dict(self) -> dict[str, Any]:
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class VirtualRouteMutation:
|
|
36
|
+
"""A mutation to an API endpoint detected in speculative branch."""
|
|
37
|
+
|
|
38
|
+
http_method: str
|
|
39
|
+
old_route: str
|
|
40
|
+
new_route: str
|
|
41
|
+
file_path: str
|
|
42
|
+
mutation_type: str # ROUTE_ADDED | ROUTE_MODIFIED | ROUTE_REMOVED
|
|
43
|
+
|
|
44
|
+
def as_dict(self) -> dict[str, Any]:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SpeculativeEvaluationReport:
|
|
50
|
+
"""Complete evaluation report of an in-memory speculative patch."""
|
|
51
|
+
|
|
52
|
+
is_safe: bool
|
|
53
|
+
modified_files: list[str] = field(default_factory=list)
|
|
54
|
+
contract_breaks: list[VirtualContractBreak] = field(default_factory=list)
|
|
55
|
+
route_mutations: list[VirtualRouteMutation] = field(default_factory=list)
|
|
56
|
+
affected_caller_count: int = 0
|
|
57
|
+
diff_summary: str = ""
|
|
58
|
+
evaluation_time_ms: float = 0.0
|
|
59
|
+
|
|
60
|
+
def as_dict(self) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"is_safe": self.is_safe,
|
|
63
|
+
"modified_files": self.modified_files,
|
|
64
|
+
"contract_breaks": [b.as_dict() for b in self.contract_breaks],
|
|
65
|
+
"route_mutations": [m.as_dict() for m in self.route_mutations],
|
|
66
|
+
"affected_caller_count": self.affected_caller_count,
|
|
67
|
+
"diff_summary": self.diff_summary,
|
|
68
|
+
"evaluation_time_ms": round(self.evaluation_time_ms, 2),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _infer_language(file_path: str) -> str:
|
|
73
|
+
ext = Path(file_path).suffix.lower()
|
|
74
|
+
mapping = {
|
|
75
|
+
".py": "python",
|
|
76
|
+
".ts": "typescript",
|
|
77
|
+
".tsx": "typescript",
|
|
78
|
+
".js": "javascript",
|
|
79
|
+
".jsx": "javascript",
|
|
80
|
+
".go": "go",
|
|
81
|
+
".rs": "rust",
|
|
82
|
+
".java": "java",
|
|
83
|
+
".kt": "kotlin",
|
|
84
|
+
".cpp": "cpp",
|
|
85
|
+
".cc": "cpp",
|
|
86
|
+
".h": "c_header",
|
|
87
|
+
".hpp": "cpp_header",
|
|
88
|
+
}
|
|
89
|
+
return mapping.get(ext, "python")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def evaluate_speculative_patch(
|
|
93
|
+
repository: Path,
|
|
94
|
+
file_patches: dict[str, str], # relative_path -> new_content
|
|
95
|
+
con: Any = None,
|
|
96
|
+
) -> SpeculativeEvaluationReport:
|
|
97
|
+
"""Evaluate candidate file contents in memory without modifying the repository on disk."""
|
|
98
|
+
import time
|
|
99
|
+
t0 = time.perf_counter()
|
|
100
|
+
repo = repository.resolve()
|
|
101
|
+
|
|
102
|
+
contract_breaks: list[VirtualContractBreak] = []
|
|
103
|
+
route_mutations: list[VirtualRouteMutation] = []
|
|
104
|
+
modified_files = list(file_patches.keys())
|
|
105
|
+
affected_callers_total = 0
|
|
106
|
+
diff_lines: list[str] = []
|
|
107
|
+
|
|
108
|
+
for rel_path, new_content in file_patches.items():
|
|
109
|
+
disk_file = repo / rel_path
|
|
110
|
+
old_content = disk_file.read_text(encoding="utf-8", errors="replace") if disk_file.is_file() else ""
|
|
111
|
+
lang = _infer_language(rel_path)
|
|
112
|
+
|
|
113
|
+
# Compute unified diff summary
|
|
114
|
+
diff = list(difflib.unified_diff(
|
|
115
|
+
old_content.splitlines(),
|
|
116
|
+
new_content.splitlines(),
|
|
117
|
+
fromfile=f"a/{rel_path}",
|
|
118
|
+
tofile=f"b/{rel_path}",
|
|
119
|
+
lineterm="",
|
|
120
|
+
))
|
|
121
|
+
if diff:
|
|
122
|
+
diff_lines.extend(diff[:20])
|
|
123
|
+
|
|
124
|
+
# Parse baseline vs speculative in memory
|
|
125
|
+
old_res = parse(old_content, lang, rel_path) if old_content else None
|
|
126
|
+
new_res = parse(new_content, lang, rel_path)
|
|
127
|
+
|
|
128
|
+
old_syms = {s.name: s for s in old_res.symbols} if old_res else {}
|
|
129
|
+
new_syms = {s.name: s for s in new_res.symbols}
|
|
130
|
+
|
|
131
|
+
# 1. Detect Deleted Symbols & Modified Signatures
|
|
132
|
+
for name, old_s in old_syms.items():
|
|
133
|
+
if name not in new_syms:
|
|
134
|
+
# Symbol deleted
|
|
135
|
+
callers_count = 0
|
|
136
|
+
if con is not None:
|
|
137
|
+
try:
|
|
138
|
+
cur = con.execute("SELECT COUNT(*) FROM calls WHERE callee = ?;", (name,))
|
|
139
|
+
callers_count = cur.fetchone()[0]
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
affected_callers_total += callers_count
|
|
143
|
+
contract_breaks.append(
|
|
144
|
+
VirtualContractBreak(
|
|
145
|
+
symbol=name,
|
|
146
|
+
break_type="SYMBOL_DELETED",
|
|
147
|
+
file_path=rel_path,
|
|
148
|
+
line=old_s.start_line,
|
|
149
|
+
impacted_callers_count=callers_count,
|
|
150
|
+
message=f"Symbol '{name}' was deleted but has {callers_count} downstream callers.",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
new_s = new_syms[name]
|
|
155
|
+
# Check parameter count reductions
|
|
156
|
+
if (old_s.parameter_count or 0) > (new_s.parameter_count or 0):
|
|
157
|
+
callers_count = 0
|
|
158
|
+
if con is not None:
|
|
159
|
+
try:
|
|
160
|
+
cur = con.execute("SELECT COUNT(*) FROM calls WHERE callee = ?;", (name,))
|
|
161
|
+
callers_count = cur.fetchone()[0]
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
affected_callers_total += callers_count
|
|
165
|
+
contract_breaks.append(
|
|
166
|
+
VirtualContractBreak(
|
|
167
|
+
symbol=name,
|
|
168
|
+
break_type="PARAMETER_REMOVED",
|
|
169
|
+
file_path=rel_path,
|
|
170
|
+
line=new_s.start_line,
|
|
171
|
+
impacted_callers_count=callers_count,
|
|
172
|
+
message=f"Parameter count reduced from {old_s.parameter_count} to {new_s.parameter_count} on '{name}'.",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# 2. Detect Route Mutations
|
|
177
|
+
old_routes = {f"{r.http_method} {r.route_path}": r for r in old_res.routes} if old_res else {}
|
|
178
|
+
new_routes = {f"{r.http_method} {r.route_path}": r for r in new_res.routes}
|
|
179
|
+
|
|
180
|
+
for key, r in new_routes.items():
|
|
181
|
+
if key not in old_routes:
|
|
182
|
+
route_mutations.append(
|
|
183
|
+
VirtualRouteMutation(
|
|
184
|
+
http_method=r.http_method,
|
|
185
|
+
old_route="",
|
|
186
|
+
new_route=r.route_path,
|
|
187
|
+
file_path=rel_path,
|
|
188
|
+
mutation_type="ROUTE_ADDED",
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for key, r in old_routes.items():
|
|
193
|
+
if key not in new_routes:
|
|
194
|
+
route_mutations.append(
|
|
195
|
+
VirtualRouteMutation(
|
|
196
|
+
http_method=r.http_method,
|
|
197
|
+
old_route=r.route_path,
|
|
198
|
+
new_route="",
|
|
199
|
+
file_path=rel_path,
|
|
200
|
+
mutation_type="ROUTE_REMOVED",
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
is_safe = len(contract_breaks) == 0
|
|
205
|
+
elapsed = (time.perf_counter() - t0) * 1000.0
|
|
206
|
+
|
|
207
|
+
return SpeculativeEvaluationReport(
|
|
208
|
+
is_safe=is_safe,
|
|
209
|
+
modified_files=modified_files,
|
|
210
|
+
contract_breaks=contract_breaks,
|
|
211
|
+
route_mutations=route_mutations,
|
|
212
|
+
affected_caller_count=affected_callers_total,
|
|
213
|
+
diff_summary="\n".join(diff_lines),
|
|
214
|
+
evaluation_time_ms=elapsed,
|
|
215
|
+
)
|
codegraph/search/hybrid.py
CHANGED
|
@@ -623,6 +623,11 @@ def search_code(
|
|
|
623
623
|
|
|
624
624
|
config_exts = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"}
|
|
625
625
|
filtered: list[dict[str, object]] = []
|
|
626
|
+
seen_file_lines: set[tuple[str, int]] = set()
|
|
627
|
+
files_with_exact_matches: set[str] = set()
|
|
628
|
+
|
|
629
|
+
query_terms = _safe_fts5_terms(clean_query)
|
|
630
|
+
query_terms_lower = [t.lower() for t in query_terms]
|
|
626
631
|
|
|
627
632
|
for item in raw_hits:
|
|
628
633
|
rel_path = item.path or item.file
|
|
@@ -659,6 +664,27 @@ def search_code(
|
|
|
659
664
|
d["match_type"] = "case_insensitive"
|
|
660
665
|
else:
|
|
661
666
|
d["match_type"] = "token"
|
|
667
|
+
|
|
668
|
+
hit_line = item.line
|
|
669
|
+
file_line_key = (rel_path, hit_line)
|
|
670
|
+
if file_line_key in seen_file_lines:
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
# If multi-word query and this hit is only a token match:
|
|
674
|
+
# verify that all query terms appear in the snippet or matched text
|
|
675
|
+
if len(query_terms_lower) >= 2 and d["match_type"] == "token":
|
|
676
|
+
haystack = (str(d.get("snippet", "")) + " " + str(d.get("matched_text", ""))).lower()
|
|
677
|
+
if not all(t in haystack for t in query_terms_lower):
|
|
678
|
+
continue
|
|
679
|
+
|
|
680
|
+
# If a file already had an exact/case-insensitive match, discard subsequent loose token matches
|
|
681
|
+
if rel_path in files_with_exact_matches and d["match_type"] == "token":
|
|
682
|
+
continue
|
|
683
|
+
|
|
684
|
+
if d["match_type"] in ("exact", "case_insensitive"):
|
|
685
|
+
files_with_exact_matches.add(rel_path)
|
|
686
|
+
|
|
687
|
+
seen_file_lines.add(file_line_key)
|
|
662
688
|
filtered.append(d)
|
|
663
689
|
if len(filtered) >= effective_limit:
|
|
664
690
|
break
|