codegraph-engine 2.3.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +320 -3
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/change_impact.py +320 -110
  20. codegraph/cli.py +776 -25
  21. codegraph/cli_output.py +17 -2
  22. codegraph/context.py +38 -11
  23. codegraph/context_freshness.py +2 -0
  24. codegraph/database/__init__.py +14 -0
  25. codegraph/database/extractor.py +712 -17
  26. codegraph/database/interrogation.py +388 -2
  27. codegraph/database/schema_drift.py +862 -0
  28. codegraph/epistemic.py +7 -0
  29. codegraph/evidence_contract.py +21 -0
  30. codegraph/frameworks.py +171 -6
  31. codegraph/frameworks_nestjs.py +502 -0
  32. codegraph/frameworks_nextjs.py +337 -0
  33. codegraph/frameworks_react.py +457 -0
  34. codegraph/freshness.py +2 -0
  35. codegraph/frontend_html.py +109 -0
  36. codegraph/git.py +2 -0
  37. codegraph/git_state.py +2 -0
  38. codegraph/indexing/__init__.py +7 -1
  39. codegraph/indexing/indexer.py +291 -38
  40. codegraph/indexing/models.py +6 -0
  41. codegraph/indexing/parser.py +239 -0
  42. codegraph/indexing/pool.py +136 -0
  43. codegraph/indexing/scanner.py +6 -0
  44. codegraph/indexing/templates.py +418 -0
  45. codegraph/interrogation.py +128 -20
  46. codegraph/mcp/server.py +190 -28
  47. codegraph/models.py +7 -0
  48. codegraph/monorepo.py +3 -0
  49. codegraph/optimizer.py +42 -5
  50. codegraph/process_lifecycle.py +31 -13
  51. codegraph/refactor/__init__.py +36 -0
  52. codegraph/refactor/models.py +114 -0
  53. codegraph/refactor/renamer.py +844 -0
  54. codegraph/refactor/transactions.py +403 -0
  55. codegraph/resolver.py +1202 -159
  56. codegraph/retrieval_policy.py +6 -0
  57. codegraph/route_topology.py +172 -0
  58. codegraph/runtime/__init__.py +19 -0
  59. codegraph/runtime/collector.py +155 -0
  60. codegraph/runtime/db_watcher.py +191 -0
  61. codegraph/runtime/ingestor.py +30 -10
  62. codegraph/runtime/port_inspector.py +304 -0
  63. codegraph/runtime/runner.py +155 -6
  64. codegraph/runtime/trace_linker.py +175 -0
  65. codegraph/search/hybrid.py +26 -0
  66. codegraph/semantic_decorators.py +34 -3
  67. codegraph/structural_diff.py +2 -0
  68. codegraph/styles.py +144 -0
  69. codegraph/symbol_history.py +2 -0
  70. codegraph/tsconfig.py +236 -0
  71. codegraph/ui/__init__.py +6 -0
  72. codegraph/ui/server.py +715 -0
  73. codegraph/watcher.py +323 -0
  74. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
  75. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  76. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  77. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  78. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  79. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  80. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,337 @@
1
+ """OpenTelemetry span parser, adapter layer, and privacy sanitizer.
2
+
3
+ Guarantees:
4
+ 1. Adapters for both standard OTLP JSON (resourceSpans) and flat span lists.
5
+ 2. Privacy: Strips all message bodies, payloads, bearer tokens, cookies, and secrets.
6
+ 3. Hashing: Salted SHA-256 for correlation/message IDs without saving raw identifiers.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from typing import Any
13
+
14
+ from codegraph.async_queue.otel_models import OTelSpanRecord
15
+
16
+ _SENSITIVE_KEY_SUBSTRINGS = (
17
+ "auth",
18
+ "token",
19
+ "cookie",
20
+ "secret",
21
+ "password",
22
+ "credential",
23
+ "payload",
24
+ "body",
25
+ "message_body",
26
+ "raw_message",
27
+ "data",
28
+ )
29
+
30
+
31
+ def hash_correlation_id(raw_id: str | int) -> str:
32
+ """Generate a deterministic, salted SHA-256 hash for correlation identifiers."""
33
+ clean = str(raw_id).strip()
34
+ if not clean:
35
+ return ""
36
+ digest = hashlib.sha256(f"cg_corr_salt_v1:{clean}".encode()).hexdigest()
37
+ return digest[:16]
38
+
39
+
40
+ def sanitize_attributes(raw_attrs: dict[str, Any]) -> dict[str, Any]:
41
+ """Sanitize attributes dictionary, removing any payload bodies, headers, or secrets."""
42
+ sanitized: dict[str, Any] = {}
43
+ for k, v in raw_attrs.items():
44
+ k_lower = k.lower()
45
+ if any(bad in k_lower for bad in _SENSITIVE_KEY_SUBSTRINGS):
46
+ continue
47
+ # Preserve primitive values only
48
+ if isinstance(v, (int, float, bool)):
49
+ sanitized[k] = v
50
+ elif isinstance(v, str):
51
+ if len(v) > 256:
52
+ v = v[:256] + "...[truncated]"
53
+ sanitized[k] = v
54
+ elif isinstance(v, (list, tuple)):
55
+ if len(v) <= 10 and all(isinstance(x, (str, int, float, bool)) for x in v):
56
+ sanitized[k] = list(v)
57
+ return sanitized
58
+
59
+
60
+ def _extract_otlp_value(val_obj: Any) -> Any:
61
+ """Unpack OTLP AnyValue object (e.g. {'stringValue': 'kafka'})."""
62
+ if not isinstance(val_obj, dict):
63
+ return val_obj
64
+ if "stringValue" in val_obj:
65
+ return val_obj["stringValue"]
66
+ if "intValue" in val_obj:
67
+ try:
68
+ return int(val_obj["intValue"])
69
+ except (ValueError, TypeError):
70
+ return 0
71
+ if "doubleValue" in val_obj:
72
+ try:
73
+ return float(val_obj["doubleValue"])
74
+ except (ValueError, TypeError):
75
+ return 0.0
76
+ if "boolValue" in val_obj:
77
+ return bool(val_obj["boolValue"])
78
+ if "arrayValue" in val_obj and "values" in val_obj["arrayValue"]:
79
+ return [_extract_otlp_value(x) for x in val_obj["arrayValue"]["values"]]
80
+ return ""
81
+
82
+
83
+ def _unpack_otlp_key_values(kv_list: list[dict[str, Any]]) -> dict[str, Any]:
84
+ """Convert an OTLP KeyValue list [{'key': 'k', 'value': {...}}] to a dict."""
85
+ out: dict[str, Any] = {}
86
+ for item in kv_list:
87
+ if isinstance(item, dict) and "key" in item:
88
+ out[str(item["key"])] = _extract_otlp_value(item.get("value"))
89
+ return out
90
+
91
+
92
+ class OTLPJsonAdapter:
93
+ """Adapter for standard OpenTelemetry OTLP JSON (resourceSpans)."""
94
+
95
+ @classmethod
96
+ def can_handle(cls, data: dict[str, Any]) -> bool:
97
+ return "resourceSpans" in data
98
+
99
+ @classmethod
100
+ def parse(cls, data: dict[str, Any]) -> list[OTelSpanRecord]:
101
+ records: list[OTelSpanRecord] = []
102
+ resource_spans = data.get("resourceSpans", [])
103
+ if not isinstance(resource_spans, list):
104
+ return records
105
+
106
+ for rs in resource_spans:
107
+ service_name = ""
108
+ if isinstance(rs, dict):
109
+ res = rs.get("resource", {})
110
+ if isinstance(res, dict):
111
+ res_attrs = res.get("attributes", [])
112
+ if isinstance(res_attrs, list):
113
+ attrs_map = _unpack_otlp_key_values(res_attrs)
114
+ service_name = str(attrs_map.get("service.name", ""))
115
+
116
+ scope_spans = rs.get("scopeSpans", [])
117
+ if isinstance(scope_spans, list):
118
+ for ss in scope_spans:
119
+ if isinstance(ss, dict):
120
+ spans = ss.get("spans", [])
121
+ if isinstance(spans, list):
122
+ for s in spans:
123
+ rec = cls._parse_span(s, service_name)
124
+ if rec:
125
+ records.append(rec)
126
+ return records
127
+
128
+ @classmethod
129
+ def _parse_span(cls, s: dict[str, Any], default_service: str) -> OTelSpanRecord | None:
130
+ trace_id = str(s.get("traceId", "")).strip()
131
+ span_id = str(s.get("spanId", "")).strip()
132
+ if not trace_id or not span_id:
133
+ return None
134
+
135
+ parent_span_id = str(s.get("parentSpanId", "")).strip()
136
+ name = str(s.get("name", "")).strip()
137
+ kind_raw = s.get("kind", 0)
138
+
139
+ # Map OTLP SpanKind int or str
140
+ kind_map = {
141
+ 1: "INTERNAL",
142
+ 2: "SERVER",
143
+ 3: "CLIENT",
144
+ 4: "PRODUCER",
145
+ 5: "CONSUMER",
146
+ "SPAN_KIND_INTERNAL": "INTERNAL",
147
+ "SPAN_KIND_SERVER": "SERVER",
148
+ "SPAN_KIND_CLIENT": "CLIENT",
149
+ "SPAN_KIND_PRODUCER": "PRODUCER",
150
+ "SPAN_KIND_CONSUMER": "CONSUMER",
151
+ }
152
+ kind = kind_map.get(kind_raw, str(kind_raw).replace("SPAN_KIND_", ""))
153
+
154
+ start_nano = int(s.get("startTimeUnixNano", 0) or 0)
155
+ end_nano = int(s.get("endTimeUnixNano", 0) or 0)
156
+ duration_ms = (end_nano - start_nano) / 1_000_000.0 if end_nano > start_nano else 0.0
157
+
158
+ # Status
159
+ status_code = "OK"
160
+ status_dict = s.get("status", {})
161
+ if isinstance(status_dict, dict):
162
+ raw_code = str(status_dict.get("code", "")).upper()
163
+ if "ERROR" in raw_code or raw_code == "2":
164
+ status_code = "ERROR"
165
+
166
+ # Attributes
167
+ raw_attrs: dict[str, Any] = {}
168
+ if isinstance(s.get("attributes"), list):
169
+ raw_attrs = _unpack_otlp_key_values(s["attributes"])
170
+ elif isinstance(s.get("attributes"), dict):
171
+ raw_attrs = dict(s["attributes"])
172
+
173
+ service_name = str(raw_attrs.get("service.name") or default_service)
174
+
175
+ # Semantic conventions for messaging
176
+ msg_system = str(raw_attrs.get("messaging.system", "")).lower()
177
+ msg_dest = str(raw_attrs.get("messaging.destination", ""))
178
+ msg_op = str(raw_attrs.get("messaging.operation", "")).lower()
179
+
180
+ # If operation wasn't explicitly set, infer from span kind
181
+ if not msg_op:
182
+ if kind == "PRODUCER":
183
+ msg_op = "publish"
184
+ elif kind == "CONSUMER":
185
+ msg_op = "process"
186
+
187
+ # Correlation ID hashing
188
+ corr_id = raw_attrs.get("messaging.message_id") or raw_attrs.get("messaging.conversation_id") or raw_attrs.get("correlation_id") or ""
189
+ corr_hash = hash_correlation_id(corr_id) if corr_id else ""
190
+
191
+ # Span links
192
+ links_list: list[tuple[str, str]] = []
193
+ raw_links = s.get("links", [])
194
+ if isinstance(raw_links, list):
195
+ for lk in raw_links:
196
+ if isinstance(lk, dict):
197
+ l_trace = str(lk.get("traceId", "")).strip()
198
+ l_span = str(lk.get("spanId", "")).strip()
199
+ if l_trace and l_span:
200
+ links_list.append((l_trace, l_span))
201
+
202
+ sanitized = sanitize_attributes(raw_attrs)
203
+
204
+ return OTelSpanRecord(
205
+ trace_id=trace_id,
206
+ span_id=span_id,
207
+ parent_span_id=parent_span_id,
208
+ service_name=service_name,
209
+ name=name,
210
+ kind=kind,
211
+ start_time_unix_nano=start_nano,
212
+ end_time_unix_nano=end_nano,
213
+ duration_ms=duration_ms,
214
+ status_code=status_code,
215
+ messaging_system=msg_system,
216
+ messaging_destination=msg_dest,
217
+ messaging_operation=msg_op,
218
+ correlation_id_hash=corr_hash,
219
+ attributes=sanitized,
220
+ links=tuple(links_list),
221
+ )
222
+
223
+
224
+ class GenericSpanListAdapter:
225
+ """Adapter for flat list of spans (Jaeger, custom JSON, or SDK exports)."""
226
+
227
+ @classmethod
228
+ def can_handle(cls, data: Any) -> bool:
229
+ if isinstance(data, list):
230
+ return True
231
+ if isinstance(data, dict) and "spans" in data:
232
+ return True
233
+ return False
234
+
235
+ @classmethod
236
+ def parse(cls, data: Any) -> list[OTelSpanRecord]:
237
+ span_items = data if isinstance(data, list) else data.get("spans", [])
238
+ records: list[OTelSpanRecord] = []
239
+ if not isinstance(span_items, list):
240
+ return records
241
+
242
+ for s in span_items:
243
+ if not isinstance(s, dict):
244
+ continue
245
+ trace_id = str(s.get("trace_id") or s.get("traceId") or "").strip()
246
+ span_id = str(s.get("span_id") or s.get("spanId") or "").strip()
247
+ if not trace_id or not span_id:
248
+ continue
249
+
250
+ parent_span_id = str(s.get("parent_span_id") or s.get("parentSpanId") or "").strip()
251
+ name = str(s.get("name") or s.get("operation_name") or "").strip()
252
+ kind = str(s.get("kind") or "INTERNAL").upper()
253
+ service_name = str(s.get("service_name") or s.get("service") or "")
254
+
255
+ duration_ms = float(s.get("duration_ms") or 0.0)
256
+ if not duration_ms and "duration_us" in s:
257
+ duration_ms = float(s["duration_us"]) / 1000.0
258
+
259
+ status_code = str(s.get("status") or s.get("status_code") or "OK").upper()
260
+ if "ERR" in status_code:
261
+ status_code = "ERROR"
262
+
263
+ raw_attrs = dict(s.get("attributes") or s.get("tags") or {})
264
+ if not service_name and "service.name" in raw_attrs:
265
+ service_name = str(raw_attrs["service.name"])
266
+
267
+ msg_system = str(raw_attrs.get("messaging.system", "")).lower()
268
+ msg_dest = str(raw_attrs.get("messaging.destination", ""))
269
+ msg_op = str(raw_attrs.get("messaging.operation", "")).lower()
270
+
271
+ if not msg_op:
272
+ if "PROD" in kind:
273
+ msg_op = "publish"
274
+ elif "CONS" in kind:
275
+ msg_op = "process"
276
+
277
+ corr_id = raw_attrs.get("messaging.message_id") or raw_attrs.get("correlation_id") or ""
278
+ corr_hash = hash_correlation_id(corr_id) if corr_id else ""
279
+
280
+ links_list: list[tuple[str, str]] = []
281
+ for lk in s.get("links", []):
282
+ if isinstance(lk, dict):
283
+ l_trace = str(lk.get("trace_id") or lk.get("traceId") or "").strip()
284
+ l_span = str(lk.get("span_id") or lk.get("spanId") or "").strip()
285
+ if l_trace and l_span:
286
+ links_list.append((l_trace, l_span))
287
+
288
+ sanitized = sanitize_attributes(raw_attrs)
289
+
290
+ records.append(
291
+ OTelSpanRecord(
292
+ trace_id=trace_id,
293
+ span_id=span_id,
294
+ parent_span_id=parent_span_id,
295
+ service_name=service_name,
296
+ name=name,
297
+ kind=kind,
298
+ start_time_unix_nano=int(s.get("start_time_unix_nano", 0) or 0),
299
+ end_time_unix_nano=int(s.get("end_time_unix_nano", 0) or 0),
300
+ duration_ms=duration_ms,
301
+ status_code=status_code,
302
+ messaging_system=msg_system,
303
+ messaging_destination=msg_dest,
304
+ messaging_operation=msg_op,
305
+ correlation_id_hash=corr_hash,
306
+ attributes=sanitized,
307
+ links=tuple(links_list),
308
+ )
309
+ )
310
+ return records
311
+
312
+
313
+ def parse_otel_spans(data_or_text: Any) -> list[OTelSpanRecord]:
314
+ """Parse raw OpenTelemetry JSON string or dictionary/list into canonical records."""
315
+ if isinstance(data_or_text, str):
316
+ try:
317
+ data = json.loads(data_or_text)
318
+ except Exception:
319
+ return []
320
+ else:
321
+ data = data_or_text
322
+
323
+ if isinstance(data, dict) and OTLPJsonAdapter.can_handle(data):
324
+ raw_records = OTLPJsonAdapter.parse(data)
325
+ elif GenericSpanListAdapter.can_handle(data):
326
+ raw_records = GenericSpanListAdapter.parse(data)
327
+ else:
328
+ raw_records = []
329
+
330
+ # Deduplicate within batch by composite_id
331
+ seen: set[str] = set()
332
+ deduped: list[OTelSpanRecord] = []
333
+ for r in raw_records:
334
+ if r.composite_id not in seen:
335
+ seen.add(r.composite_id)
336
+ deduped.append(r)
337
+ return deduped