codegraph-engine 2.1.2__py3-none-any.whl → 2.1.7__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 (61) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +1961 -0
  3. codegraph/agent_capabilities.py +3500 -0
  4. codegraph/agent_rules.py +530 -0
  5. codegraph/architecture.py +48 -5
  6. codegraph/binding_resolver.py +616 -0
  7. codegraph/cli.py +345 -5
  8. codegraph/config.py +2 -0
  9. codegraph/context.py +845 -93
  10. codegraph/database/__init__.py +58 -0
  11. codegraph/database/extractor.py +2956 -0
  12. codegraph/database/interrogation.py +1085 -0
  13. codegraph/database/models.py +310 -0
  14. codegraph/epistemic.py +95 -0
  15. codegraph/errors.py +7 -0
  16. codegraph/evidence/citations.py +22 -1
  17. codegraph/evidence_contract.py +455 -0
  18. codegraph/frameworks.py +372 -11
  19. codegraph/git.py +92 -10
  20. codegraph/graph/models.py +15 -0
  21. codegraph/graph/traversal.py +680 -121
  22. codegraph/indexing/__init__.py +15 -1
  23. codegraph/indexing/classifier.py +343 -58
  24. codegraph/indexing/indexer.py +1515 -141
  25. codegraph/indexing/models.py +75 -0
  26. codegraph/indexing/parser.py +1843 -70
  27. codegraph/indexing/scanner.py +4 -1
  28. codegraph/indexing/telemetry.py +326 -0
  29. codegraph/installer.py +1652 -0
  30. codegraph/interrogation.py +708 -122
  31. codegraph/mcp/server.py +525 -107
  32. codegraph/mcp_diagnostics.py +879 -0
  33. codegraph/models.py +14 -0
  34. codegraph/monorepo.py +922 -0
  35. codegraph/observability.py +113 -2
  36. codegraph/optimizer.py +647 -42
  37. codegraph/ranking.py +61 -16
  38. codegraph/resolver.py +2006 -77
  39. codegraph/resources/monitor.py +1 -1
  40. codegraph/retrieval_policy.py +96 -17
  41. codegraph/route_composer.py +363 -0
  42. codegraph/runtime/__init__.py +32 -0
  43. codegraph/runtime/ingestor.py +672 -0
  44. codegraph/runtime/models.py +130 -0
  45. codegraph/runtime/reconciliation.py +492 -0
  46. codegraph/search/__init__.py +2 -0
  47. codegraph/search/hybrid.py +572 -53
  48. codegraph/security/__init__.py +50 -2
  49. codegraph/security/paths.py +162 -8
  50. codegraph/security/redaction.py +633 -0
  51. codegraph/semantic_decorators.py +341 -0
  52. codegraph/target_resolver.py +6 -6
  53. codegraph/tool_selection_eval.py +1158 -0
  54. codegraph_engine-2.1.7.dist-info/METADATA +617 -0
  55. codegraph_engine-2.1.7.dist-info/RECORD +84 -0
  56. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/licenses/LICENSE +1 -1
  57. codegraph_engine-2.1.2.dist-info/METADATA +0 -336
  58. codegraph_engine-2.1.2.dist-info/RECORD +0 -63
  59. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/WHEEL +0 -0
  60. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/entry_points.txt +0 -0
  61. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,672 @@
1
+ """Privacy-Safe, Bounded Runtime Ingestion Adapters (Phases 11, 15, 16).
2
+
3
+ Supported Formats:
4
+ 1. OpenTelemetry (`otel`) — OTLP JSON (`resourceSpans` / `scopeSpans` / `spans` or flat span list)
5
+ 2. Structured JSON Runtime Traces (`json` / `jsonl`) — explicit event objects
6
+ 3. SQL / Query Logs (`sql_log`) — raw or structured SQL query log lines
7
+
8
+ Security & Storage Invariants:
9
+ - Never executes application code or connects to live databases.
10
+ - Strips and redacts Authorization headers, Cookies, request/response bodies, tokens, API keys,
11
+ passwords, connection strings, and SQL bound parameters (`VALUES (?, ?)`).
12
+ - Enforces `max_events`, deterministic `sampling_rate`, `retention_limit`, and compact aggregation
13
+ into `runtime_edges` with `observation_count`, `first_seen`, `last_seen`, and `runtime_generation`.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import re
20
+ import sqlite3
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from codegraph.database.extractor import parse_sql_statement
25
+ from codegraph.security.paths import safe_read_text
26
+ from codegraph.security.redaction import (
27
+ SENSITIVE_RUNTIME_KEYS,
28
+ is_secret_variable_name,
29
+ normalize_and_redact_sql,
30
+ redact_payload,
31
+ redact_secrets,
32
+ )
33
+
34
+ from .models import RuntimeEvent
35
+
36
+ DEFAULT_MAX_INGEST_EVENTS = 5_000
37
+ DEFAULT_RETENTION_OBSERVATIONS = 1_000
38
+
39
+
40
+ def _deterministic_sample(trace_id: str, span_id: str, sampling_rate: float) -> bool:
41
+ if sampling_rate >= 1.0:
42
+ return True
43
+ if sampling_rate <= 0.0:
44
+ return False
45
+ digest = hashlib.sha256(f"{trace_id}:{span_id}".encode()).hexdigest()
46
+ bucket = int(digest[:8], 16) / 0xFFFFFFFF
47
+ return bucket <= sampling_rate
48
+
49
+
50
+ def _resolve_symbol_in_db(con: sqlite3.Connection, raw_sym: str) -> str:
51
+ """Resolve a short or qualified function/method name to its indexed canonical_id if unique."""
52
+ if not raw_sym:
53
+ return ""
54
+ cleaned = raw_sym.strip()
55
+ row = con.execute(
56
+ "SELECT canonical_id FROM symbols WHERE canonical_id = ? OR qualified_name = ? ORDER BY canonical_id LIMIT 1",
57
+ (cleaned, cleaned),
58
+ ).fetchone()
59
+ if row:
60
+ return str(row["canonical_id"])
61
+ rows = con.execute(
62
+ "SELECT canonical_id FROM symbols WHERE name = ? ORDER BY canonical_id",
63
+ (cleaned.split(".")[-1],),
64
+ ).fetchall()
65
+ if len(rows) == 1:
66
+ return str(rows[0]["canonical_id"])
67
+ return cleaned
68
+
69
+
70
+ def _resolve_table_in_db(con: sqlite3.Connection, raw_table: str) -> str:
71
+ """Resolve a table name to its indexed canonical table ID if present, else `db.UNKNOWN.UNKNOWN.<table>`."""
72
+ if not raw_table:
73
+ return ""
74
+ tbl = raw_table.strip().lower().split(".")[-1]
75
+ row = con.execute(
76
+ "SELECT canonical_id FROM db_entities WHERE kind = 'Table' AND table_name = ? ORDER BY canonical_id LIMIT 1",
77
+ (tbl,),
78
+ ).fetchone()
79
+ if row:
80
+ return str(row["canonical_id"])
81
+ return f"db.UNKNOWN.UNKNOWN.{tbl}"
82
+
83
+
84
+ def parse_otel_traces(
85
+ payload: dict[str, Any] | list[Any],
86
+ runtime_generation: int = 1,
87
+ max_events: int = DEFAULT_MAX_INGEST_EVENTS,
88
+ sampling_rate: float = 1.0,
89
+ ) -> list[RuntimeEvent]:
90
+ """Parse OpenTelemetry JSON (`resourceSpans` or list of spans) into sanitized `RuntimeEvent`s."""
91
+ spans_raw: list[tuple[str, dict[str, Any]]] = []
92
+ if isinstance(payload, dict) and "resourceSpans" in payload:
93
+ for rspan in payload.get("resourceSpans", []):
94
+ if not isinstance(rspan, dict):
95
+ continue
96
+ svc_name = ""
97
+ res_attrs = rspan.get("resource", {}).get("attributes", [])
98
+ for attr in res_attrs if isinstance(res_attrs, list) else []:
99
+ if isinstance(attr, dict) and attr.get("key") == "service.name":
100
+ val_obj = attr.get("value", {})
101
+ svc_name = str(val_obj.get("stringValue", "") if isinstance(val_obj, dict) else val_obj)
102
+ scope_spans = rspan.get("scopeSpans") or rspan.get("instrumentationLibrarySpans") or []
103
+ for sspan in scope_spans if isinstance(scope_spans, list) else []:
104
+ if not isinstance(sspan, dict):
105
+ continue
106
+ for sp in sspan.get("spans", []):
107
+ if isinstance(sp, dict):
108
+ spans_raw.append((svc_name, sp))
109
+ elif isinstance(payload, list):
110
+ for item in payload:
111
+ if isinstance(item, dict):
112
+ spans_raw.append((str(item.get("service_name") or item.get("service") or ""), item))
113
+
114
+ events: list[RuntimeEvent] = []
115
+ for idx, (svc_name, sp) in enumerate(spans_raw):
116
+ if len(events) >= max_events:
117
+ break
118
+ trace_id = str(sp.get("traceId") or sp.get("trace_id") or f"trace_{idx}")
119
+ span_id = str(sp.get("spanId") or sp.get("span_id") or f"span_{idx}")
120
+ parent_id = str(sp.get("parentSpanId") or sp.get("parent_span_id") or "")
121
+ if not _deterministic_sample(trace_id, span_id, sampling_rate):
122
+ continue
123
+
124
+ # Parse attributes safely, dropping sensitive keys
125
+ attrs: dict[str, Any] = {}
126
+ raw_attrs = sp.get("attributes", {})
127
+ if isinstance(raw_attrs, list):
128
+ for entry in raw_attrs:
129
+ if isinstance(entry, dict) and "key" in entry:
130
+ k = str(entry["key"])
131
+ v_obj = entry.get("value", {})
132
+ if isinstance(v_obj, dict):
133
+ v = (
134
+ v_obj.get("stringValue")
135
+ or v_obj.get("intValue")
136
+ or v_obj.get("doubleValue")
137
+ or v_obj.get("boolValue")
138
+ or ""
139
+ )
140
+ else:
141
+ v = v_obj
142
+ if k.lower() not in SENSITIVE_RUNTIME_KEYS and not is_secret_variable_name(k):
143
+ attrs[k] = v
144
+ elif isinstance(raw_attrs, dict):
145
+ for k, v in raw_attrs.items():
146
+ if str(k).lower() not in SENSITIVE_RUNTIME_KEYS and not is_secret_variable_name(str(k)):
147
+ attrs[str(k)] = v
148
+
149
+ http_method = str(attrs.get("http.method") or attrs.get("http.request.method") or sp.get("http_method") or "").upper()
150
+ route_path = str(attrs.get("http.route") or attrs.get("url.path") or sp.get("route") or "")
151
+ code_fn = str(attrs.get("code.function") or sp.get("function") or sp.get("handler") or "")
152
+ code_ns = str(attrs.get("code.namespace") or "")
153
+ full_fn = f"{code_ns}.{code_fn}" if code_ns and code_fn and not code_fn.startswith(code_ns) else code_fn
154
+ db_stmt = str(attrs.get("db.statement") or sp.get("sql") or "")
155
+ db_op = str(attrs.get("db.operation") or sp.get("operation") or "").upper()
156
+ db_tbl = str(attrs.get("db.sql.table") or attrs.get("db.collection.name") or sp.get("table") or "").lower()
157
+ db_cols: list[str] = []
158
+
159
+ if db_stmt:
160
+ parsed_sql = parse_sql_statement(db_stmt)
161
+ if parsed_sql:
162
+ first_op = parsed_sql[0]
163
+ if not db_op:
164
+ db_op = str(first_op.get("operation") or "")
165
+ if not db_tbl and first_op.get("tables"):
166
+ db_tbl = str(first_op["tables"][0]).lower()
167
+ db_cols = [str(c) for c in first_op.get("columns", [])]
168
+
169
+ start_ns = sp.get("startTimeUnixNano")
170
+ end_ns = sp.get("endTimeUnixNano")
171
+ duration_ms = float(sp.get("duration_ms") or 0.0)
172
+ if not duration_ms and start_ns and end_ns:
173
+ try:
174
+ duration_ms = max(0.0, (int(end_ns) - int(start_ns)) / 1_000_000.0)
175
+ except (ValueError, TypeError):
176
+ duration_ms = 0.0
177
+
178
+ status_code_raw = attrs.get("http.status_code") or attrs.get("http.response.status_code") or sp.get("status_code")
179
+ status_code = int(status_code_raw) if status_code_raw is not None and str(status_code_raw).isdigit() else None
180
+
181
+ events.append(
182
+ RuntimeEvent(
183
+ event_id=f"otel:{trace_id}:{span_id}",
184
+ trace_id=trace_id,
185
+ span_id=span_id,
186
+ parent_span_id=parent_id,
187
+ timestamp=str(sp.get("timestamp") or start_ns or ""),
188
+ source_format="otel",
189
+ http_method=http_method,
190
+ route_path=route_path,
191
+ handler_symbol=full_fn if route_path else "",
192
+ caller_symbol= str(sp.get("caller") or full_fn),
193
+ callee_symbol= str(sp.get("callee") or ""),
194
+ service_name=svc_name or str(attrs.get("service.name") or ""),
195
+ db_operation=db_op,
196
+ db_table=db_tbl,
197
+ db_columns=tuple(db_cols),
198
+ normalized_sql=normalize_and_redact_sql(db_stmt) if db_stmt else "",
199
+ status_code=status_code,
200
+ exception_type=str(attrs.get("exception.type") or sp.get("exception") or ""),
201
+ duration_ms=duration_ms,
202
+ runtime_generation=runtime_generation,
203
+ metadata=redact_payload(attrs),
204
+ )
205
+ )
206
+
207
+ return events
208
+
209
+
210
+ def parse_json_runtime_events(
211
+ items: list[dict[str, Any]],
212
+ runtime_generation: int = 1,
213
+ max_events: int = DEFAULT_MAX_INGEST_EVENTS,
214
+ sampling_rate: float = 1.0,
215
+ ) -> list[RuntimeEvent]:
216
+ """Parse structured JSON runtime events into normalized, redacted `RuntimeEvent` records."""
217
+ events: list[RuntimeEvent] = []
218
+ for idx, item in enumerate(items):
219
+ if len(events) >= max_events:
220
+ break
221
+ if not isinstance(item, dict):
222
+ continue
223
+ trace_id = str(item.get("trace_id") or item.get("traceId") or f"trace_{idx}")
224
+ span_id = str(item.get("span_id") or item.get("spanId") or f"span_{idx}")
225
+ if not _deterministic_sample(trace_id, span_id, sampling_rate):
226
+ continue
227
+
228
+ raw_sql = str(item.get("sql") or item.get("query") or item.get("statement") or "")
229
+ op = str(item.get("operation") or item.get("db_operation") or "").upper()
230
+ tbl = str(item.get("table") or item.get("db_table") or "").lower()
231
+ cols_raw = item.get("columns") or item.get("db_columns") or []
232
+ cols: list[str] = [str(c).lower() for c in cols_raw] if isinstance(cols_raw, list) else []
233
+
234
+ if raw_sql:
235
+ parsed_sql = parse_sql_statement(raw_sql)
236
+ if parsed_sql:
237
+ first_op = parsed_sql[0]
238
+ if not op:
239
+ op = str(first_op.get("operation") or "")
240
+ if not tbl and first_op.get("tables"):
241
+ tbl = str(first_op["tables"][0]).lower()
242
+ if not cols:
243
+ cols = [str(c) for c in first_op.get("columns", [])]
244
+
245
+ route_str = str(item.get("route") or item.get("route_path") or item.get("path") or "")
246
+ method_str = str(item.get("http_method") or item.get("method") or "").upper()
247
+ if route_str and " " in route_str and not method_str:
248
+ parts = route_str.split(" ", 1)
249
+ if parts[0].upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"):
250
+ method_str = parts[0].upper()
251
+ route_str = parts[1].strip()
252
+
253
+ handler = str(item.get("handler") or item.get("handler_symbol") or "")
254
+ caller = str(item.get("caller") or item.get("function") or item.get("source_symbol") or handler or "")
255
+ callee = str(item.get("callee") or item.get("target_symbol") or "")
256
+ service = str(item.get("service") or item.get("service_name") or "")
257
+
258
+ safe_meta: dict[str, Any] = {}
259
+ for k, v in item.items():
260
+ kl = str(k).lower()
261
+ if kl in (
262
+ "trace_id",
263
+ "span_id",
264
+ "parent_span_id",
265
+ "timestamp",
266
+ "operation",
267
+ "table",
268
+ "columns",
269
+ "sql",
270
+ "query",
271
+ "statement",
272
+ "route",
273
+ "route_path",
274
+ "http_method",
275
+ "method",
276
+ "handler",
277
+ "caller",
278
+ "callee",
279
+ "function",
280
+ "service",
281
+ "duration_ms",
282
+ "status_code",
283
+ "exception",
284
+ ):
285
+ continue
286
+ if kl in SENSITIVE_RUNTIME_KEYS or is_secret_variable_name(str(k)):
287
+ safe_meta[str(k)] = "[REDACTED]"
288
+ else:
289
+ safe_meta[str(k)] = redact_payload(v)
290
+
291
+ status_raw = item.get("status_code") or item.get("status")
292
+ status_code = int(status_raw) if status_raw is not None and str(status_raw).isdigit() else None
293
+
294
+ events.append(
295
+ RuntimeEvent(
296
+ event_id=f"json:{trace_id}:{span_id}:{idx}",
297
+ trace_id=trace_id,
298
+ span_id=span_id,
299
+ parent_span_id=str(item.get("parent_span_id") or ""),
300
+ timestamp=str(item.get("timestamp") or ""),
301
+ source_format="json",
302
+ http_method=method_str,
303
+ route_path=route_str,
304
+ handler_symbol=handler,
305
+ caller_symbol=caller,
306
+ callee_symbol=callee,
307
+ service_name=service,
308
+ db_operation=op,
309
+ db_table=tbl,
310
+ db_columns=tuple(cols),
311
+ normalized_sql=normalize_and_redact_sql(raw_sql) if raw_sql else (f"{op} {tbl}".strip() if op and tbl else ""),
312
+ status_code=status_code,
313
+ exception_type=str(item.get("exception") or item.get("exception_type") or ""),
314
+ duration_ms=float(item.get("duration_ms") or 0.0),
315
+ runtime_generation=runtime_generation,
316
+ metadata=safe_meta,
317
+ )
318
+ )
319
+ return events
320
+
321
+
322
+ def parse_sql_query_logs(
323
+ log_text: str,
324
+ runtime_generation: int = 1,
325
+ max_events: int = DEFAULT_MAX_INGEST_EVENTS,
326
+ ) -> list[RuntimeEvent]:
327
+ """Parse SQL query log lines into normalized, parameter-redacted `RuntimeEvent` records."""
328
+ events: list[RuntimeEvent] = []
329
+ for idx, raw_line in enumerate(log_text.splitlines()):
330
+ if len(events) >= max_events:
331
+ break
332
+ line = raw_line.strip()
333
+ if not line or line.startswith("#"):
334
+ continue
335
+
336
+ # Extract optional structured tags like [trace_id=...] [caller=...] [duration_ms=...]
337
+ trace_m = re.search(r"trace_id=([A-Za-z0-9_-]+)", line)
338
+ caller_m = re.search(r"(?:caller|function|symbol)=([A-Za-z0-9_.:]+)", line)
339
+ dur_m = re.search(r"duration(?:_ms)?=([0-9.]+)", line)
340
+
341
+ trace_id = trace_m.group(1) if trace_m else f"sqllog_{idx}"
342
+ caller = caller_m.group(1) if caller_m else ""
343
+ dur = float(dur_m.group(1)) if dur_m else 0.0
344
+
345
+ parsed_ops = parse_sql_statement(line)
346
+ for op_idx, op_info in enumerate(parsed_ops):
347
+ op = str(op_info.get("operation") or "")
348
+ tables = op_info.get("tables", [])
349
+ cols = tuple(str(c) for c in op_info.get("columns", []))
350
+ norm_sql = str(op_info.get("normalized_sql") or "")
351
+ for tbl in tables:
352
+ events.append(
353
+ RuntimeEvent(
354
+ event_id=f"sqllog:{idx}:{op_idx}:{tbl}",
355
+ trace_id=trace_id,
356
+ span_id=f"sqlspan_{idx}_{op_idx}",
357
+ timestamp="",
358
+ source_format="sql_log",
359
+ caller_symbol=caller,
360
+ db_operation=op,
361
+ db_table=str(tbl).lower(),
362
+ db_columns=cols,
363
+ normalized_sql=norm_sql,
364
+ duration_ms=dur,
365
+ runtime_generation=runtime_generation,
366
+ )
367
+ )
368
+ return events
369
+
370
+
371
+ def ingest_runtime_traces(
372
+ con: sqlite3.Connection,
373
+ data_or_path: str | dict[str, Any] | list[Any] | Path | None = None,
374
+ repository: Path | None = None,
375
+ format: str = "auto",
376
+ max_events: int = DEFAULT_MAX_INGEST_EVENTS,
377
+ sampling_rate: float = 1.0,
378
+ retention_limit: int = DEFAULT_RETENTION_OBSERVATIONS,
379
+ *,
380
+ source_path: str = "",
381
+ payload: str = "",
382
+ sample_rate: float | None = None,
383
+ ) -> dict[str, Any]:
384
+ """Ingest runtime observations from OpenTelemetry JSON, structured JSON/JSONL events, or SQL logs.
385
+
386
+ Persists bounded, deduplicated, redacted records into `runtime_observations` and `runtime_edges`.
387
+ """
388
+ if sample_rate is not None:
389
+ sampling_rate = float(sample_rate)
390
+ if isinstance(data_or_path, Path):
391
+ if repository is None:
392
+ repository = data_or_path
393
+ data_or_path = payload or source_path
394
+ elif data_or_path is None:
395
+ data_or_path = payload or source_path
396
+
397
+ # Increment runtime_generation
398
+ curr_gen = 0
399
+ try:
400
+ row = con.execute("SELECT value FROM metadata WHERE key='runtime_generation'").fetchone()
401
+ if row and row[0]:
402
+ curr_gen = int(row[0])
403
+ except (sqlite3.OperationalError, ValueError):
404
+ pass
405
+ next_gen = curr_gen + 1
406
+
407
+ raw_payload: Any = data_or_path
408
+ if isinstance(data_or_path, str):
409
+ stripped = data_or_path.strip()
410
+ if stripped.startswith(("{", "[")):
411
+ try:
412
+ raw_payload = json.loads(stripped)
413
+ except json.JSONDecodeError:
414
+ # Could be JSONL
415
+ jsonl_items: list[dict[str, Any]] = []
416
+ for ln in stripped.splitlines():
417
+ ln_s = ln.strip()
418
+ if ln_s.startswith("{"):
419
+ try:
420
+ jsonl_items.append(json.loads(ln_s))
421
+ except json.JSONDecodeError:
422
+ pass
423
+ if jsonl_items:
424
+ raw_payload = jsonl_items
425
+ elif repository is not None and stripped and not stripped.upper().startswith(("SELECT ", "INSERT ", "UPDATE ", "DELETE ")):
426
+ # Treat as repository-relative file path validated via safe_read_text
427
+ _, _, file_text = safe_read_text(repository, stripped)
428
+ try:
429
+ raw_payload = json.loads(file_text)
430
+ except json.JSONDecodeError:
431
+ jsonl_list = [
432
+ json.loads(ln)
433
+ for ln in file_text.splitlines()
434
+ if ln.strip().startswith("{")
435
+ ]
436
+ raw_payload = jsonl_list if jsonl_list else file_text
437
+
438
+ # Parse into RuntimeEvent list based on format
439
+ events: list[RuntimeEvent] = []
440
+ fmt = format.strip().lower()
441
+ if isinstance(raw_payload, dict):
442
+ if fmt in ("auto", "otel") and "resourceSpans" in raw_payload:
443
+ events = parse_otel_traces(raw_payload, next_gen, max_events, sampling_rate)
444
+ else:
445
+ events = parse_json_runtime_events([raw_payload], next_gen, max_events, sampling_rate)
446
+ elif isinstance(raw_payload, list):
447
+ if fmt == "otel" or (
448
+ fmt == "auto"
449
+ and raw_payload
450
+ and isinstance(raw_payload[0], dict)
451
+ and ("traceId" in raw_payload[0] or "attributes" in raw_payload[0])
452
+ ):
453
+ events = parse_otel_traces(raw_payload, next_gen, max_events, sampling_rate)
454
+ else:
455
+ events = parse_json_runtime_events(raw_payload, next_gen, max_events, sampling_rate)
456
+ elif isinstance(raw_payload, str):
457
+ events = parse_sql_query_logs(raw_payload, next_gen, max_events)
458
+
459
+ edges_upserted = 0
460
+ spans_by_id: dict[tuple[str, str], RuntimeEvent] = {(e.trace_id, e.span_id): e for e in events}
461
+
462
+ for ev in events:
463
+ caller_resolved = _resolve_symbol_in_db(con, ev.caller_symbol or ev.handler_symbol or ev.service_name)
464
+ handler_resolved = _resolve_symbol_in_db(con, ev.handler_symbol) if ev.handler_symbol else caller_resolved
465
+ callee_resolved = _resolve_symbol_in_db(con, ev.callee_symbol) if ev.callee_symbol else ""
466
+ tbl_resolved = _resolve_table_in_db(con, ev.db_table) if ev.db_table else ""
467
+
468
+ con.execute(
469
+ "INSERT OR REPLACE INTO runtime_observations("
470
+ "event_id, trace_id, span_id, parent_span_id, timestamp, source_format, "
471
+ "http_method, route_path, handler_symbol, caller_symbol, callee_symbol, service_name, "
472
+ "db_operation, db_table, db_columns_json, normalized_sql, status_code, exception_type, "
473
+ "duration_ms, evidence_class, runtime_generation) "
474
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
475
+ (
476
+ ev.event_id,
477
+ ev.trace_id,
478
+ ev.span_id,
479
+ ev.parent_span_id,
480
+ ev.timestamp,
481
+ ev.source_format,
482
+ ev.http_method,
483
+ redact_secrets(ev.route_path),
484
+ handler_resolved,
485
+ caller_resolved,
486
+ callee_resolved,
487
+ ev.service_name,
488
+ ev.db_operation,
489
+ ev.db_table,
490
+ json.dumps(list(ev.db_columns)),
491
+ ev.normalized_sql,
492
+ ev.status_code,
493
+ redact_secrets(ev.exception_type),
494
+ ev.duration_ms,
495
+ "RUNTIME_OBSERVED",
496
+ next_gen,
497
+ ),
498
+ )
499
+
500
+ # 1. Route -> Handler runtime edge
501
+ if ev.route_path and handler_resolved:
502
+ ep_label = f"ENDPOINT:{ev.http_method or 'ANY'}:{ev.route_path}"
503
+ _upsert_runtime_edge(
504
+ con,
505
+ source=ep_label,
506
+ target=handler_resolved,
507
+ relationship="HANDLED_BY",
508
+ operation=ev.http_method or "HTTP",
509
+ ev=ev,
510
+ next_gen=next_gen,
511
+ )
512
+ edges_upserted += 1
513
+
514
+ # 2. Parent span caller -> child span caller edge
515
+ if ev.parent_span_id and (ev.trace_id, ev.parent_span_id) in spans_by_id:
516
+ parent_ev = spans_by_id[(ev.trace_id, ev.parent_span_id)]
517
+ parent_sym = _resolve_symbol_in_db(con, parent_ev.caller_symbol or parent_ev.handler_symbol)
518
+ if parent_sym and caller_resolved and parent_sym != caller_resolved:
519
+ _upsert_runtime_edge(
520
+ con,
521
+ source=parent_sym,
522
+ target=caller_resolved,
523
+ relationship="CALLS",
524
+ operation="CALL",
525
+ ev=ev,
526
+ next_gen=next_gen,
527
+ )
528
+ edges_upserted += 1
529
+
530
+ # 3. Explicit caller -> callee runtime edge
531
+ if caller_resolved and callee_resolved:
532
+ _upsert_runtime_edge(
533
+ con,
534
+ source=caller_resolved,
535
+ target=callee_resolved,
536
+ relationship="CALLS",
537
+ operation="CALL",
538
+ ev=ev,
539
+ next_gen=next_gen,
540
+ )
541
+ edges_upserted += 1
542
+
543
+ # 4. Caller / Service -> Database Table & Column runtime edges
544
+ if ev.db_table and tbl_resolved:
545
+ src_actor = caller_resolved or ev.service_name or (f"ENDPOINT:{ev.http_method or 'ANY'}:{ev.route_path}" if ev.route_path else "runtime.anonymous")
546
+ op_upper = ev.db_operation.upper()
547
+ rel = "READS_TABLE" if op_upper in ("SELECT", "READ", "FIND", "GET") else "WRITES_TABLE"
548
+ _upsert_runtime_edge(
549
+ con,
550
+ source=src_actor,
551
+ target=tbl_resolved,
552
+ relationship=rel,
553
+ operation=op_upper or "QUERY",
554
+ ev=ev,
555
+ next_gen=next_gen,
556
+ )
557
+ edges_upserted += 1
558
+
559
+ col_rel = "READS_COLUMN" if rel == "READS_TABLE" else "WRITES_COLUMN"
560
+ for col in ev.db_columns:
561
+ col_cid = f"{tbl_resolved}.{col.lower()}"
562
+ _upsert_runtime_edge(
563
+ con,
564
+ source=src_actor,
565
+ target=col_cid,
566
+ relationship=col_rel,
567
+ operation=op_upper or "QUERY",
568
+ ev=ev,
569
+ next_gen=next_gen,
570
+ )
571
+ edges_upserted += 1
572
+
573
+ # Enforce retention limit on raw `runtime_observations`
574
+ obs_count = int(con.execute("SELECT count(*) FROM runtime_observations").fetchone()[0])
575
+ if obs_count > retention_limit:
576
+ excess = obs_count - retention_limit
577
+ con.execute(
578
+ "DELETE FROM runtime_observations WHERE event_id IN ("
579
+ "SELECT event_id FROM runtime_observations ORDER BY runtime_generation ASC, event_id ASC LIMIT ?"
580
+ ")",
581
+ (excess,),
582
+ )
583
+
584
+ con.execute(
585
+ "INSERT INTO metadata(key, value) VALUES('runtime_generation', ?) "
586
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
587
+ (str(next_gen),),
588
+ )
589
+ con.commit()
590
+
591
+ return {
592
+ "status": "ok",
593
+ "ingested_events": len(events),
594
+ "events_ingested": len(events),
595
+ "edges_updated": edges_upserted,
596
+ "runtime_generation": next_gen,
597
+ "evidence_class": "RUNTIME_OBSERVED",
598
+ "retention_limit": retention_limit,
599
+ }
600
+
601
+
602
+ def _upsert_runtime_edge(
603
+ con: sqlite3.Connection,
604
+ source: str,
605
+ target: str,
606
+ relationship: str,
607
+ operation: str,
608
+ ev: RuntimeEvent,
609
+ next_gen: int,
610
+ ) -> None:
611
+ edge_id = f"rtedge:{source}->{target}:{relationship}:{operation}"
612
+ existing = con.execute(
613
+ "SELECT observation_count, first_seen, avg_duration_ms, max_duration_ms "
614
+ "FROM runtime_edges WHERE edge_id = ?",
615
+ (edge_id,),
616
+ ).fetchone()
617
+
618
+ if existing:
619
+ old_cnt = int(existing["observation_count"])
620
+ new_cnt = old_cnt + 1
621
+ first_seen = str(existing["first_seen"] or ev.timestamp)
622
+ last_seen = str(ev.timestamp or existing["first_seen"] or "")
623
+ old_avg = float(existing["avg_duration_ms"] or 0.0)
624
+ new_avg = ((old_avg * old_cnt) + ev.duration_ms) / new_cnt
625
+ new_max = max(float(existing["max_duration_ms"] or 0.0), ev.duration_ms)
626
+ con.execute(
627
+ "UPDATE runtime_edges SET observation_count=?, first_seen=?, last_seen=?, "
628
+ "avg_duration_ms=?, max_duration_ms=?, sample_trace_id=?, sample_span_id=?, "
629
+ "normalized_sql=?, status_code=?, exception_type=?, runtime_generation=? "
630
+ "WHERE edge_id=?",
631
+ (
632
+ new_cnt,
633
+ first_seen,
634
+ last_seen,
635
+ new_avg,
636
+ new_max,
637
+ ev.trace_id,
638
+ ev.span_id,
639
+ ev.normalized_sql,
640
+ ev.status_code,
641
+ redact_secrets(ev.exception_type),
642
+ next_gen,
643
+ edge_id,
644
+ ),
645
+ )
646
+ else:
647
+ con.execute(
648
+ "INSERT INTO runtime_edges("
649
+ "edge_id, source, target, relationship, operation, evidence_class, "
650
+ "observation_count, first_seen, last_seen, avg_duration_ms, max_duration_ms, "
651
+ "sample_trace_id, sample_span_id, normalized_sql, status_code, exception_type, runtime_generation) "
652
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
653
+ (
654
+ edge_id,
655
+ source,
656
+ target,
657
+ relationship,
658
+ operation,
659
+ "RUNTIME_OBSERVED",
660
+ 1,
661
+ ev.timestamp,
662
+ ev.timestamp,
663
+ ev.duration_ms,
664
+ ev.duration_ms,
665
+ ev.trace_id,
666
+ ev.span_id,
667
+ ev.normalized_sql,
668
+ ev.status_code,
669
+ redact_secrets(ev.exception_type),
670
+ next_gen,
671
+ ),
672
+ )