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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""Query and graph traversal engine for asynchronous message queues and tasks.
|
|
2
|
+
|
|
3
|
+
Epistemic Invariants:
|
|
4
|
+
1. Preserves distinct semantic relationships:
|
|
5
|
+
- PRODUCES_TO_QUEUE
|
|
6
|
+
- CONSUMES_FROM_QUEUE
|
|
7
|
+
- STATIC_QUEUE_LINKED
|
|
8
|
+
- OTEL_DISTRIBUTED_VERIFIED
|
|
9
|
+
2. Bridges entrypoint handlers -> producers -> queues -> consumer workers -> downstream DB/operations.
|
|
10
|
+
3. Fully deterministic ordering.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sqlite3
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_async_queues(con: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
19
|
+
"""List all statically detected async queues, topics, and channels."""
|
|
20
|
+
con.row_factory = sqlite3.Row
|
|
21
|
+
try:
|
|
22
|
+
rows = con.execute(
|
|
23
|
+
"""
|
|
24
|
+
SELECT q.queue_id, q.system, q.destination, q.service, q.environment, q.is_dlq,
|
|
25
|
+
COUNT(DISTINCT p.id) as producer_count,
|
|
26
|
+
COUNT(DISTINCT c.id) as consumer_count,
|
|
27
|
+
COUNT(DISTINCT l.link_id) as link_count
|
|
28
|
+
FROM async_queues q
|
|
29
|
+
LEFT JOIN async_producers p ON q.queue_id = p.queue_id
|
|
30
|
+
LEFT JOIN async_consumers c ON q.queue_id = c.queue_id
|
|
31
|
+
LEFT JOIN async_links l ON q.queue_id = l.queue_id
|
|
32
|
+
GROUP BY q.queue_id
|
|
33
|
+
ORDER BY q.queue_id ASC
|
|
34
|
+
"""
|
|
35
|
+
).fetchall()
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
"queue_id": str(r["queue_id"]),
|
|
39
|
+
"system": str(r["system"]),
|
|
40
|
+
"destination": str(r["destination"]),
|
|
41
|
+
"service": str(r["service"]),
|
|
42
|
+
"environment": str(r["environment"]),
|
|
43
|
+
"is_dlq": bool(r["is_dlq"]),
|
|
44
|
+
"producer_count": int(r["producer_count"]),
|
|
45
|
+
"consumer_count": int(r["consumer_count"]),
|
|
46
|
+
"link_count": int(r["link_count"]),
|
|
47
|
+
}
|
|
48
|
+
for r in rows
|
|
49
|
+
]
|
|
50
|
+
except sqlite3.OperationalError:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_queue_producers(
|
|
55
|
+
con: sqlite3.Connection,
|
|
56
|
+
queue_or_topic: str,
|
|
57
|
+
) -> list[dict[str, Any]]:
|
|
58
|
+
"""Find all producer call sites for a given queue, topic, or destination."""
|
|
59
|
+
con.row_factory = sqlite3.Row
|
|
60
|
+
clean = queue_or_topic.strip()
|
|
61
|
+
if not clean:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
rows = con.execute(
|
|
66
|
+
"""
|
|
67
|
+
SELECT p.id, p.queue_id, p.system, p.destination, p.caller_canonical_id,
|
|
68
|
+
p.file_path, p.line, p.call_snippet, p.confidence, s.name as caller_name
|
|
69
|
+
FROM async_producers p
|
|
70
|
+
LEFT JOIN symbols s ON p.caller_canonical_id = s.canonical_id
|
|
71
|
+
WHERE p.queue_id = ? OR p.destination = ? OR p.destination LIKE ?
|
|
72
|
+
ORDER BY p.file_path ASC, p.line ASC
|
|
73
|
+
""",
|
|
74
|
+
(clean, clean, f"%{clean}%"),
|
|
75
|
+
).fetchall()
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
"id": int(r["id"]),
|
|
79
|
+
"queue_id": str(r["queue_id"]),
|
|
80
|
+
"system": str(r["system"]),
|
|
81
|
+
"destination": str(r["destination"]),
|
|
82
|
+
"caller_canonical_id": str(r["caller_canonical_id"]),
|
|
83
|
+
"caller_name": str(r["caller_name"] or r["caller_canonical_id"]),
|
|
84
|
+
"file_path": str(r["file_path"]),
|
|
85
|
+
"line": int(r["line"]),
|
|
86
|
+
"call_snippet": str(r["call_snippet"]),
|
|
87
|
+
"confidence": str(r["confidence"]),
|
|
88
|
+
}
|
|
89
|
+
for r in rows
|
|
90
|
+
]
|
|
91
|
+
except sqlite3.OperationalError:
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_queue_consumers(
|
|
96
|
+
con: sqlite3.Connection,
|
|
97
|
+
queue_or_topic: str,
|
|
98
|
+
) -> list[dict[str, Any]]:
|
|
99
|
+
"""Find all consumer workers and handlers subscribed to a given queue or topic."""
|
|
100
|
+
con.row_factory = sqlite3.Row
|
|
101
|
+
clean = queue_or_topic.strip()
|
|
102
|
+
if not clean:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
rows = con.execute(
|
|
107
|
+
"""
|
|
108
|
+
SELECT c.id, c.queue_id, c.system, c.destination, c.handler_canonical_id,
|
|
109
|
+
c.file_path, c.line, c.handler_snippet, c.confidence, s.name as handler_name
|
|
110
|
+
FROM async_consumers c
|
|
111
|
+
LEFT JOIN symbols s ON c.handler_canonical_id = s.canonical_id
|
|
112
|
+
WHERE c.queue_id = ? OR c.destination = ? OR c.destination LIKE ?
|
|
113
|
+
ORDER BY c.file_path ASC, c.line ASC
|
|
114
|
+
""",
|
|
115
|
+
(clean, clean, f"%{clean}%"),
|
|
116
|
+
).fetchall()
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
"id": int(r["id"]),
|
|
120
|
+
"queue_id": str(r["queue_id"]),
|
|
121
|
+
"system": str(r["system"]),
|
|
122
|
+
"destination": str(r["destination"]),
|
|
123
|
+
"handler_canonical_id": str(r["handler_canonical_id"]),
|
|
124
|
+
"handler_name": str(r["handler_name"] or r["handler_canonical_id"]),
|
|
125
|
+
"file_path": str(r["file_path"]),
|
|
126
|
+
"line": int(r["line"]),
|
|
127
|
+
"handler_snippet": str(r["handler_snippet"]),
|
|
128
|
+
"confidence": str(r["confidence"]),
|
|
129
|
+
}
|
|
130
|
+
for r in rows
|
|
131
|
+
]
|
|
132
|
+
except sqlite3.OperationalError:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def trace_async_flow(
|
|
137
|
+
con: sqlite3.Connection,
|
|
138
|
+
entrypoint: str,
|
|
139
|
+
max_depth: int = 5,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""Trace cross-process asynchronous execution flow starting from an entrypoint.
|
|
142
|
+
|
|
143
|
+
Entrypoint can be:
|
|
144
|
+
- An HTTP endpoint or service function that publishes to a queue
|
|
145
|
+
- A queue or topic name directly
|
|
146
|
+
- A background consumer worker function
|
|
147
|
+
|
|
148
|
+
Returns deterministic flow steps across the async boundary, including
|
|
149
|
+
downstream database queries and callees invoked by consumer workers.
|
|
150
|
+
"""
|
|
151
|
+
con.row_factory = sqlite3.Row
|
|
152
|
+
clean = entrypoint.strip()
|
|
153
|
+
if not clean:
|
|
154
|
+
return {
|
|
155
|
+
"status": "error",
|
|
156
|
+
"message": "Entrypoint cannot be empty.",
|
|
157
|
+
"entrypoint": entrypoint,
|
|
158
|
+
"steps": [],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# 1. Check if entrypoint is a queue or topic directly
|
|
162
|
+
queue_matches = con.execute(
|
|
163
|
+
"SELECT queue_id, system, destination FROM async_queues WHERE queue_id=? OR destination=?",
|
|
164
|
+
(clean, clean),
|
|
165
|
+
).fetchall()
|
|
166
|
+
|
|
167
|
+
producers: list[dict[str, Any]] = []
|
|
168
|
+
consumers: list[dict[str, Any]] = []
|
|
169
|
+
flow_steps: list[dict[str, Any]] = []
|
|
170
|
+
downstream_ops: list[dict[str, Any]] = []
|
|
171
|
+
|
|
172
|
+
if queue_matches:
|
|
173
|
+
for q in queue_matches:
|
|
174
|
+
q_id = str(q["queue_id"])
|
|
175
|
+
q_prods = find_queue_producers(con, q_id)
|
|
176
|
+
q_cons = find_queue_consumers(con, q_id)
|
|
177
|
+
producers.extend(q_prods)
|
|
178
|
+
consumers.extend(q_cons)
|
|
179
|
+
|
|
180
|
+
for p in q_prods:
|
|
181
|
+
flow_steps.append({
|
|
182
|
+
"step": len(flow_steps) + 1,
|
|
183
|
+
"source": p["caller_canonical_id"],
|
|
184
|
+
"target": q_id,
|
|
185
|
+
"relationship": "PRODUCES_TO_QUEUE",
|
|
186
|
+
"system": p["system"],
|
|
187
|
+
"evidence_class": "STATIC_QUEUE_LINKED",
|
|
188
|
+
"confidence": p["confidence"],
|
|
189
|
+
"file": p["file_path"],
|
|
190
|
+
"line": p["line"],
|
|
191
|
+
"snippet": p["call_snippet"],
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
for c in q_cons:
|
|
195
|
+
flow_steps.append({
|
|
196
|
+
"step": len(flow_steps) + 1,
|
|
197
|
+
"source": q_id,
|
|
198
|
+
"target": c["handler_canonical_id"],
|
|
199
|
+
"relationship": "CONSUMES_FROM_QUEUE",
|
|
200
|
+
"system": c["system"],
|
|
201
|
+
"evidence_class": "STATIC_QUEUE_LINKED",
|
|
202
|
+
"confidence": c["confidence"],
|
|
203
|
+
"file": c["file_path"],
|
|
204
|
+
"line": c["line"],
|
|
205
|
+
"snippet": c["handler_snippet"],
|
|
206
|
+
})
|
|
207
|
+
else:
|
|
208
|
+
# 2. Check if entrypoint is a producer symbol or calls a producer
|
|
209
|
+
# Look up directly matching producers
|
|
210
|
+
direct_prods = con.execute(
|
|
211
|
+
"""
|
|
212
|
+
SELECT p.id, p.queue_id, p.system, p.destination, p.caller_canonical_id,
|
|
213
|
+
p.file_path, p.line, p.call_snippet, p.confidence
|
|
214
|
+
FROM async_producers p
|
|
215
|
+
WHERE p.caller_canonical_id = ? OR p.caller_canonical_id LIKE ?
|
|
216
|
+
ORDER BY p.file_path ASC, p.line ASC
|
|
217
|
+
""",
|
|
218
|
+
(clean, f"%{clean}%"),
|
|
219
|
+
).fetchall()
|
|
220
|
+
|
|
221
|
+
# If none direct, check if entrypoint calls a producer function (e.g. controller calling service)
|
|
222
|
+
if not direct_prods:
|
|
223
|
+
callee_prods = con.execute(
|
|
224
|
+
"""
|
|
225
|
+
SELECT p.id, p.queue_id, p.system, p.destination, p.caller_canonical_id,
|
|
226
|
+
p.file_path, p.line, p.call_snippet, p.confidence, ge.source as entry_source
|
|
227
|
+
FROM graph_edges ge
|
|
228
|
+
JOIN async_producers p ON ge.target = p.caller_canonical_id
|
|
229
|
+
WHERE (ge.source = ? OR ge.source LIKE ?) AND ge.relationship = 'CALLS'
|
|
230
|
+
ORDER BY p.file_path ASC, p.line ASC
|
|
231
|
+
""",
|
|
232
|
+
(clean, f"%{clean}%"),
|
|
233
|
+
).fetchall()
|
|
234
|
+
for cp in callee_prods:
|
|
235
|
+
flow_steps.append({
|
|
236
|
+
"step": len(flow_steps) + 1,
|
|
237
|
+
"source": str(cp["entry_source"]),
|
|
238
|
+
"target": str(cp["caller_canonical_id"]),
|
|
239
|
+
"relationship": "CALLS",
|
|
240
|
+
"evidence_class": "AST_VERIFIED",
|
|
241
|
+
"confidence": "HIGH",
|
|
242
|
+
"file": str(cp["file_path"]),
|
|
243
|
+
"line": int(cp["line"]),
|
|
244
|
+
"snippet": "",
|
|
245
|
+
})
|
|
246
|
+
direct_prods.append(cp)
|
|
247
|
+
|
|
248
|
+
for p in direct_prods:
|
|
249
|
+
q_id = str(p["queue_id"])
|
|
250
|
+
producers.append({
|
|
251
|
+
"queue_id": q_id,
|
|
252
|
+
"system": str(p["system"]),
|
|
253
|
+
"destination": str(p["destination"]),
|
|
254
|
+
"caller_canonical_id": str(p["caller_canonical_id"]),
|
|
255
|
+
"file_path": str(p["file_path"]),
|
|
256
|
+
"line": int(p["line"]),
|
|
257
|
+
"call_snippet": str(p["call_snippet"]),
|
|
258
|
+
"confidence": str(p["confidence"]),
|
|
259
|
+
})
|
|
260
|
+
flow_steps.append({
|
|
261
|
+
"step": len(flow_steps) + 1,
|
|
262
|
+
"source": str(p["caller_canonical_id"]),
|
|
263
|
+
"target": q_id,
|
|
264
|
+
"relationship": "PRODUCES_TO_QUEUE",
|
|
265
|
+
"system": str(p["system"]),
|
|
266
|
+
"evidence_class": "STATIC_QUEUE_LINKED",
|
|
267
|
+
"confidence": str(p["confidence"]),
|
|
268
|
+
"file": str(p["file_path"]),
|
|
269
|
+
"line": int(p["line"]),
|
|
270
|
+
"snippet": str(p["call_snippet"]),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# Find matching static consumers for this queue
|
|
274
|
+
q_cons = find_queue_consumers(con, q_id)
|
|
275
|
+
consumers.extend(q_cons)
|
|
276
|
+
|
|
277
|
+
# Look up runtime links to augment evidence and latency metrics
|
|
278
|
+
try:
|
|
279
|
+
rt_links = con.execute(
|
|
280
|
+
"SELECT * FROM async_links WHERE queue_id = ? OR destination = ?",
|
|
281
|
+
(q_id, str(p["destination"])),
|
|
282
|
+
).fetchall()
|
|
283
|
+
except sqlite3.OperationalError:
|
|
284
|
+
rt_links = []
|
|
285
|
+
|
|
286
|
+
links_by_consumer = {str(rk["consumer_id"]): rk for rk in rt_links}
|
|
287
|
+
|
|
288
|
+
for c in q_cons:
|
|
289
|
+
matched_link = links_by_consumer.get(c["handler_canonical_id"])
|
|
290
|
+
ev_class = str(matched_link["evidence_class"]) if matched_link else "STATIC_QUEUE_LINKED"
|
|
291
|
+
step_dict: dict[str, Any] = {
|
|
292
|
+
"step": len(flow_steps) + 1,
|
|
293
|
+
"source": q_id,
|
|
294
|
+
"target": c["handler_canonical_id"],
|
|
295
|
+
"relationship": "CONSUMES_FROM_QUEUE",
|
|
296
|
+
"system": c["system"],
|
|
297
|
+
"evidence_class": ev_class,
|
|
298
|
+
"confidence": c["confidence"],
|
|
299
|
+
"file": c["file_path"],
|
|
300
|
+
"line": c["line"],
|
|
301
|
+
"snippet": c["handler_snippet"],
|
|
302
|
+
}
|
|
303
|
+
if matched_link and ev_class == "OTEL_DISTRIBUTED_VERIFIED":
|
|
304
|
+
step_dict["latency_p50_ms"] = float(matched_link["p50_ms"])
|
|
305
|
+
step_dict["latency_p95_ms"] = float(matched_link["p95_ms"])
|
|
306
|
+
step_dict["trace_count"] = int(matched_link["trace_count"])
|
|
307
|
+
flow_steps.append(step_dict)
|
|
308
|
+
|
|
309
|
+
# Include cross-repository / external runtime consumers
|
|
310
|
+
for rk in rt_links:
|
|
311
|
+
c_id = str(rk["consumer_id"])
|
|
312
|
+
if c_id.startswith("external:"):
|
|
313
|
+
consumers.append({
|
|
314
|
+
"queue_id": q_id,
|
|
315
|
+
"system": str(rk["system"]),
|
|
316
|
+
"destination": str(rk["destination"]),
|
|
317
|
+
"handler_canonical_id": c_id,
|
|
318
|
+
"file_path": "",
|
|
319
|
+
"line": 0,
|
|
320
|
+
"handler_snippet": f"External consumer ({rk['system']})",
|
|
321
|
+
"confidence": "HIGH",
|
|
322
|
+
})
|
|
323
|
+
flow_steps.append({
|
|
324
|
+
"step": len(flow_steps) + 1,
|
|
325
|
+
"source": q_id,
|
|
326
|
+
"target": c_id,
|
|
327
|
+
"relationship": "CONSUMES_FROM_QUEUE",
|
|
328
|
+
"system": str(rk["system"]),
|
|
329
|
+
"evidence_class": str(rk["evidence_class"]),
|
|
330
|
+
"confidence": "HIGH",
|
|
331
|
+
"file": "",
|
|
332
|
+
"line": 0,
|
|
333
|
+
"snippet": f"Observed via OTel (p50={rk['p50_ms']}ms, p95={rk['p95_ms']}ms)",
|
|
334
|
+
"latency_p50_ms": float(rk["p50_ms"]),
|
|
335
|
+
"latency_p95_ms": float(rk["p95_ms"]),
|
|
336
|
+
"trace_count": int(rk["trace_count"]),
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
# 3. If still no producers found, check if entrypoint is a consumer worker handler
|
|
340
|
+
if not producers:
|
|
341
|
+
direct_cons = con.execute(
|
|
342
|
+
"""
|
|
343
|
+
SELECT c.id, c.queue_id, c.system, c.destination, c.handler_canonical_id,
|
|
344
|
+
c.file_path, c.line, c.handler_snippet, c.confidence
|
|
345
|
+
FROM async_consumers c
|
|
346
|
+
WHERE c.handler_canonical_id = ? OR c.handler_canonical_id LIKE ?
|
|
347
|
+
ORDER BY c.file_path ASC, c.line ASC
|
|
348
|
+
""",
|
|
349
|
+
(clean, f"%{clean}%"),
|
|
350
|
+
).fetchall()
|
|
351
|
+
|
|
352
|
+
for c in direct_cons:
|
|
353
|
+
q_id = str(c["queue_id"])
|
|
354
|
+
consumers.append({
|
|
355
|
+
"queue_id": q_id,
|
|
356
|
+
"system": str(c["system"]),
|
|
357
|
+
"destination": str(c["destination"]),
|
|
358
|
+
"handler_canonical_id": str(c["handler_canonical_id"]),
|
|
359
|
+
"file_path": str(c["file_path"]),
|
|
360
|
+
"line": int(c["line"]),
|
|
361
|
+
"handler_snippet": str(c["handler_snippet"]),
|
|
362
|
+
"confidence": str(c["confidence"]),
|
|
363
|
+
})
|
|
364
|
+
# Find upstream producers
|
|
365
|
+
q_prods = find_queue_producers(con, q_id)
|
|
366
|
+
producers.extend(q_prods)
|
|
367
|
+
for p in q_prods:
|
|
368
|
+
flow_steps.append({
|
|
369
|
+
"step": len(flow_steps) + 1,
|
|
370
|
+
"source": p["caller_canonical_id"],
|
|
371
|
+
"target": q_id,
|
|
372
|
+
"relationship": "PRODUCES_TO_QUEUE",
|
|
373
|
+
"system": p["system"],
|
|
374
|
+
"evidence_class": "STATIC_QUEUE_LINKED",
|
|
375
|
+
"confidence": p["confidence"],
|
|
376
|
+
"file": p["file_path"],
|
|
377
|
+
"line": p["line"],
|
|
378
|
+
"snippet": p["call_snippet"],
|
|
379
|
+
})
|
|
380
|
+
flow_steps.append({
|
|
381
|
+
"step": len(flow_steps) + 1,
|
|
382
|
+
"source": q_id,
|
|
383
|
+
"target": str(c["handler_canonical_id"]),
|
|
384
|
+
"relationship": "CONSUMES_FROM_QUEUE",
|
|
385
|
+
"system": str(c["system"]),
|
|
386
|
+
"evidence_class": "STATIC_QUEUE_LINKED",
|
|
387
|
+
"confidence": str(c["confidence"]),
|
|
388
|
+
"file": str(c["file_path"]),
|
|
389
|
+
"line": int(c["line"]),
|
|
390
|
+
"snippet": str(c["handler_snippet"]),
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
# For each consumer handler discovered, find downstream database queries and operations
|
|
394
|
+
visited_handlers: set[str] = set()
|
|
395
|
+
for c in consumers:
|
|
396
|
+
h_id = c["handler_canonical_id"]
|
|
397
|
+
if h_id in visited_handlers:
|
|
398
|
+
continue
|
|
399
|
+
visited_handlers.add(h_id)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
db_rows = con.execute(
|
|
403
|
+
"""
|
|
404
|
+
SELECT query_id, operation, relationship, table_name, file_path, start_line, evidence
|
|
405
|
+
FROM db_queries
|
|
406
|
+
WHERE caller_symbol_id = ?
|
|
407
|
+
ORDER BY file_path ASC, start_line ASC
|
|
408
|
+
""",
|
|
409
|
+
(h_id,),
|
|
410
|
+
).fetchall()
|
|
411
|
+
for db in db_rows:
|
|
412
|
+
op_dict = {
|
|
413
|
+
"consumer": h_id,
|
|
414
|
+
"operation": str(db["operation"]),
|
|
415
|
+
"relationship": str(db["relationship"]),
|
|
416
|
+
"table_name": str(db["table_name"]),
|
|
417
|
+
"file": str(db["file_path"]),
|
|
418
|
+
"line": int(db["start_line"]),
|
|
419
|
+
"evidence": str(db["evidence"]),
|
|
420
|
+
}
|
|
421
|
+
downstream_ops.append(op_dict)
|
|
422
|
+
flow_steps.append({
|
|
423
|
+
"step": len(flow_steps) + 1,
|
|
424
|
+
"source": h_id,
|
|
425
|
+
"target": f"table:{db['table_name']}",
|
|
426
|
+
"relationship": str(db["relationship"]),
|
|
427
|
+
"evidence_class": "AST_VERIFIED",
|
|
428
|
+
"confidence": "HIGH",
|
|
429
|
+
"file": str(db["file_path"]),
|
|
430
|
+
"line": int(db["start_line"]),
|
|
431
|
+
"snippet": f"{db['operation']} on {db['table_name']}",
|
|
432
|
+
})
|
|
433
|
+
except sqlite3.OperationalError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
"status": "ok" if flow_steps else "not_found",
|
|
438
|
+
"entrypoint": clean,
|
|
439
|
+
"step_count": len(flow_steps),
|
|
440
|
+
"producers_count": len(producers),
|
|
441
|
+
"consumers_count": len(consumers),
|
|
442
|
+
"downstream_ops_count": len(downstream_ops),
|
|
443
|
+
"producers": producers,
|
|
444
|
+
"consumers": consumers,
|
|
445
|
+
"downstream_ops": downstream_ops,
|
|
446
|
+
"flow_steps": flow_steps,
|
|
447
|
+
}
|
codegraph/audit.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Optional local audit log for MCP tool operations.
|
|
2
|
+
|
|
3
|
+
Records: timestamp, tool, operation, repository, files accessed, duration.
|
|
4
|
+
File contents are NEVER recorded.
|
|
5
|
+
Log can be cleared via CLI. Disabled by default.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuditLog:
|
|
18
|
+
"""SQLite-backed local audit log. Contents never include source code."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, db_path: Path, enabled: bool = False) -> None:
|
|
21
|
+
self.db_path = db_path
|
|
22
|
+
self.enabled = enabled
|
|
23
|
+
if enabled:
|
|
24
|
+
self._ensure_table()
|
|
25
|
+
|
|
26
|
+
def _connect(self) -> sqlite3.Connection:
|
|
27
|
+
con = sqlite3.connect(self.db_path)
|
|
28
|
+
con.row_factory = sqlite3.Row
|
|
29
|
+
return con
|
|
30
|
+
|
|
31
|
+
def _ensure_table(self) -> None:
|
|
32
|
+
with self._session() as con:
|
|
33
|
+
con.execute("""
|
|
34
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
35
|
+
id INTEGER PRIMARY KEY,
|
|
36
|
+
ts INTEGER NOT NULL,
|
|
37
|
+
tool TEXT NOT NULL,
|
|
38
|
+
operation TEXT NOT NULL,
|
|
39
|
+
repository TEXT NOT NULL,
|
|
40
|
+
files_accessed TEXT NOT NULL DEFAULT '[]',
|
|
41
|
+
duration_ms REAL NOT NULL DEFAULT 0
|
|
42
|
+
)
|
|
43
|
+
""")
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def _session(self) -> Iterator[sqlite3.Connection]:
|
|
47
|
+
con = self._connect()
|
|
48
|
+
try:
|
|
49
|
+
yield con
|
|
50
|
+
con.commit()
|
|
51
|
+
except BaseException:
|
|
52
|
+
con.rollback()
|
|
53
|
+
raise
|
|
54
|
+
finally:
|
|
55
|
+
con.close()
|
|
56
|
+
|
|
57
|
+
def record(
|
|
58
|
+
self,
|
|
59
|
+
tool: str,
|
|
60
|
+
operation: str,
|
|
61
|
+
repository: str,
|
|
62
|
+
files_accessed: list[str] | None = None,
|
|
63
|
+
duration_ms: float = 0.0,
|
|
64
|
+
) -> None:
|
|
65
|
+
if not self.enabled:
|
|
66
|
+
return
|
|
67
|
+
with self._session() as con:
|
|
68
|
+
con.execute(
|
|
69
|
+
"INSERT INTO audit_log(ts, tool, operation, repository, files_accessed, duration_ms) "
|
|
70
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
71
|
+
(
|
|
72
|
+
int(time.time()),
|
|
73
|
+
tool,
|
|
74
|
+
operation,
|
|
75
|
+
repository,
|
|
76
|
+
json.dumps(files_accessed or []),
|
|
77
|
+
duration_ms,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def recent(self, limit: int = 100) -> list[dict[str, object]]:
|
|
82
|
+
if not self.enabled:
|
|
83
|
+
return []
|
|
84
|
+
with self._session() as con:
|
|
85
|
+
rows = con.execute(
|
|
86
|
+
"SELECT ts, tool, operation, repository, files_accessed, duration_ms "
|
|
87
|
+
"FROM audit_log ORDER BY id DESC LIMIT ?",
|
|
88
|
+
(limit,),
|
|
89
|
+
).fetchall()
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
"ts": r["ts"],
|
|
93
|
+
"tool": r["tool"],
|
|
94
|
+
"operation": r["operation"],
|
|
95
|
+
"repository": r["repository"],
|
|
96
|
+
"files_accessed": json.loads(r["files_accessed"]),
|
|
97
|
+
"duration_ms": r["duration_ms"],
|
|
98
|
+
}
|
|
99
|
+
for r in rows
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
if not self.enabled:
|
|
104
|
+
return
|
|
105
|
+
with self._session() as con:
|
|
106
|
+
con.execute("DELETE FROM audit_log")
|