codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,353 @@
1
+ """Zero-Friction Development Server Runtime Interceptor (v2.2.0).
2
+
3
+ Enables 1-line development telemetry capture:
4
+ codegraph run npm run dev
5
+ codegraph run uvicorn main:app
6
+ codegraph run python app.py
7
+
8
+ Security & Reliability Invariants:
9
+ - Passes stdin/stdout/stderr directly to the user's terminal with zero distortion.
10
+ - Strips authorization headers, cookies, and sensitive credentials via existing redaction rules.
11
+ - Streams captured HTTP requests and uncaught exceptions directly into `.codegraph/runtime.sqlite3`
12
+ without requiring code changes to the user's application.
13
+ - Cleans up child processes and temporary interceptor hooks on exit.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import threading
21
+ import time
22
+ from pathlib import Path
23
+
24
+ from codegraph.indexing import Indexer
25
+ from codegraph.runtime.ingestor import ingest_runtime_traces
26
+
27
+
28
+ def _generate_node_interceptor(output_jsonl: Path) -> str:
29
+ """Generate lightweight Node.js HTTP/Express/Fastify request interceptor."""
30
+ escaped_log_path = json.dumps(str(output_jsonl))
31
+ return f"""// CodeGraph Node.js Runtime Interceptor
32
+ const http = require('http');
33
+ const https = require('https');
34
+ const fs = require('fs');
35
+
36
+ const logPath = {escaped_log_path};
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
+
53
+ function emitEvent(event) {{
54
+ try {{
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
+ }}
59
+ }} catch (_) {{}}
60
+ }}
61
+
62
+ setInterval(_cg_flush_buffer, _CG_FLUSH_INTERVAL_MS);
63
+ process.on('exit', _cg_flush_buffer);
64
+
65
+ function hookServer(serverProto) {{
66
+
67
+ const origEmit = serverProto.emit;
68
+ serverProto.emit = function(event, req, res) {{
69
+ if (event === 'request' && req && res) {{
70
+ const startTime = Date.now();
71
+ const method = req.method || 'GET';
72
+ const url = req.url ? req.url.split('?')[0] : '/';
73
+
74
+ res.on('finish', () => {{
75
+ const duration = Date.now() - startTime;
76
+ emitEvent({{
77
+ trace_id: 'tr_' + Math.random().toString(36).substring(2, 10),
78
+ span_id: 'sp_' + Math.random().toString(36).substring(2, 10),
79
+ timestamp: new Date().toISOString(),
80
+ http_method: method,
81
+ route_path: url,
82
+ status_code: res.statusCode,
83
+ duration_ms: duration,
84
+ service_name: 'node_dev_server',
85
+ source_format: 'json'
86
+ }});
87
+ }});
88
+ }}
89
+ return origEmit.apply(this, arguments);
90
+ }};
91
+ }}
92
+
93
+ try {{
94
+ hookServer(http.Server.prototype);
95
+ }} catch (_) {{}}
96
+
97
+ process.on('uncaughtExceptionMonitor', (err) => {{
98
+ emitEvent({{
99
+ trace_id: 'err_' + Math.random().toString(36).substring(2, 10),
100
+ span_id: 'sp_err',
101
+ timestamp: new Date().toISOString(),
102
+ exception_type: (err && err.name) ? err.name : 'UncaughtException',
103
+ service_name: 'node_dev_server',
104
+ status_code: 500,
105
+ source_format: 'json'
106
+ }});
107
+ }});
108
+ """
109
+
110
+
111
+ def _generate_python_interceptor(output_jsonl: Path) -> str:
112
+ """Generate lightweight Python WSGI/ASGI/FastAPI request interceptor with in-memory buffering."""
113
+ escaped_log_path = repr(str(output_jsonl))
114
+ return f"""# CodeGraph Python Runtime Interceptor
115
+ import atexit
116
+ import json
117
+ import os
118
+ import sys
119
+ import threading
120
+ import time
121
+
122
+ LOG_PATH = {escaped_log_path}
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
+
145
+ def emit_event(event):
146
+ try:
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()
155
+ except Exception:
156
+ pass
157
+
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
170
+ _orig_excepthook = sys.excepthook
171
+ def _cg_excepthook(exc_type, exc_value, exc_traceback):
172
+ emit_event({{
173
+ "trace_id": "err_" + str(int(time.time())),
174
+ "span_id": "sp_err",
175
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
176
+ "exception_type": getattr(exc_type, "__name__", "Exception"),
177
+ "service_name": "python_dev_server",
178
+ "status_code": 500,
179
+ "source_format": "json"
180
+ }})
181
+ return _orig_excepthook(exc_type, exc_value, exc_traceback)
182
+
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
270
+ """
271
+
272
+
273
+ def run_with_telemetry(
274
+ command: list[str],
275
+ repository: Path,
276
+ sample_rate: float = 1.0,
277
+ ) -> int:
278
+ """Execute a development command with transparent runtime trace capture."""
279
+ if not command:
280
+ raise ValueError("Command cannot be empty.")
281
+
282
+ hooks_dir = repository / ".codegraph" / "runtime_hooks"
283
+ hooks_dir.mkdir(parents=True, exist_ok=True)
284
+ trace_file = hooks_dir / "active_traces.jsonl"
285
+ # Ensure fresh trace buffer
286
+ if trace_file.exists():
287
+ try:
288
+ trace_file.unlink()
289
+ except OSError:
290
+ pass
291
+
292
+ node_hook = hooks_dir / "cg_node_hook.js"
293
+ node_hook.write_text(_generate_node_interceptor(trace_file), encoding="utf-8")
294
+
295
+ python_hook = hooks_dir / "cg_python_hook.py"
296
+ python_hook.write_text(_generate_python_interceptor(trace_file), encoding="utf-8")
297
+
298
+ env = dict(os.environ)
299
+
300
+ # Inject Node hook
301
+ old_node_opts = env.get("NODE_OPTIONS", "")
302
+ env["NODE_OPTIONS"] = f"--require {node_hook} {old_node_opts}".strip()
303
+
304
+ # Inject Python hook
305
+ old_py_start = env.get("PYTHONSTARTUP", "")
306
+ if not old_py_start:
307
+ env["PYTHONSTARTUP"] = str(python_hook)
308
+
309
+ # Background ingestion thread
310
+ stop_event = threading.Event()
311
+ indexer = Indexer(repository)
312
+
313
+ def _ingest_loop() -> None:
314
+ last_pos = 0
315
+ while not stop_event.is_set():
316
+ time.sleep(1.0)
317
+ if not trace_file.exists():
318
+ continue
319
+ try:
320
+ size = trace_file.stat().st_size
321
+ if size > last_pos:
322
+ with open(trace_file, encoding="utf-8", errors="replace") as f:
323
+ f.seek(last_pos)
324
+ new_content = f.read()
325
+ last_pos = f.tell()
326
+ if new_content.strip():
327
+ with indexer.session() as con:
328
+ ingest_runtime_traces(con, new_content, repository=repository, max_events=1000, sampling_rate=sample_rate)
329
+ except Exception:
330
+ pass
331
+
332
+ ingest_worker = threading.Thread(target=_ingest_loop, daemon=True)
333
+ ingest_worker.start()
334
+
335
+ # Spawn interactive dev process
336
+ try:
337
+ proc = subprocess.Popen(command, cwd=str(repository), env=env)
338
+ returncode = proc.wait()
339
+ except KeyboardInterrupt:
340
+ returncode = 0
341
+ finally:
342
+ stop_event.set()
343
+ # Drain remaining lines on shutdown
344
+ if trace_file.exists():
345
+ try:
346
+ content = trace_file.read_text(encoding="utf-8", errors="replace")
347
+ if content.strip():
348
+ with indexer.session() as con:
349
+ ingest_runtime_traces(con, content, repository=repository, max_events=1000, sampling_rate=sample_rate)
350
+ except Exception:
351
+ pass
352
+
353
+ return returncode
@@ -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
+ )
@@ -0,0 +1,25 @@
1
+ from .hybrid import (
2
+ SearchResult,
3
+ search,
4
+ search_code,
5
+ search_exact_canonical_id,
6
+ search_exact_qualified,
7
+ search_exact_symbol,
8
+ search_lexical,
9
+ search_route,
10
+ )
11
+ from .semantic import EmbeddingProvider, SemanticSearchUnavailable, status
12
+
13
+ __all__ = [
14
+ "EmbeddingProvider",
15
+ "SearchResult",
16
+ "SemanticSearchUnavailable",
17
+ "search",
18
+ "search_code",
19
+ "search_exact_canonical_id",
20
+ "search_exact_qualified",
21
+ "search_exact_symbol",
22
+ "search_lexical",
23
+ "search_route",
24
+ "status",
25
+ ]