codegraph-engine 2.2.1__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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +564 -6
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -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/branch_comparison.py +197 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +992 -3
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +545 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +258 -27
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.2.1.dist-info/RECORD +0 -87
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Branch Architecture Comparison Engine.
|
|
2
|
+
|
|
3
|
+
Provides hierarchical structural comparisons between Git branches:
|
|
4
|
+
package
|
|
5
|
+
↓
|
|
6
|
+
module
|
|
7
|
+
↓
|
|
8
|
+
symbol
|
|
9
|
+
↓
|
|
10
|
+
relationships (routes, tests, DB writes, calls)
|
|
11
|
+
|
|
12
|
+
Supports:
|
|
13
|
+
- deterministic ordering
|
|
14
|
+
- token-budget awareness (max_tokens)
|
|
15
|
+
- cursor-based pagination
|
|
16
|
+
- explicit summary of changed routes, services, database models, and affected tests
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sqlite3
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from codegraph.monorepo import detect_workspace
|
|
26
|
+
from codegraph.structural_diff import compare_branches
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SymbolChangeItem:
|
|
31
|
+
name: str
|
|
32
|
+
kind: str
|
|
33
|
+
change_type: str # "ADDED" | "MODIFIED" | "REMOVED"
|
|
34
|
+
line: int | None
|
|
35
|
+
relationships: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def as_dict(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"name": self.name,
|
|
40
|
+
"kind": self.kind,
|
|
41
|
+
"change_type": self.change_type,
|
|
42
|
+
"line": self.line,
|
|
43
|
+
"relationships": self.relationships,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ModuleChangeItem:
|
|
49
|
+
module: str
|
|
50
|
+
file_path: str
|
|
51
|
+
symbols: list[SymbolChangeItem] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
def as_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"module": self.module,
|
|
56
|
+
"file_path": self.file_path,
|
|
57
|
+
"symbols": [s.as_dict() for s in self.symbols],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class PackageChangeItem:
|
|
63
|
+
package_id: str
|
|
64
|
+
modules: list[ModuleChangeItem] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def as_dict(self) -> dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"package_id": self.package_id,
|
|
69
|
+
"modules": [m.as_dict() for m in self.modules],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BranchArchitectureReport:
|
|
75
|
+
base_branch: str
|
|
76
|
+
head_branch: str
|
|
77
|
+
packages: list[PackageChangeItem] = field(default_factory=list)
|
|
78
|
+
changed_routes: list[dict[str, str]] = field(default_factory=list)
|
|
79
|
+
changed_services: list[str] = field(default_factory=list)
|
|
80
|
+
changed_db: list[dict[str, str]] = field(default_factory=list)
|
|
81
|
+
affected_tests: list[str] = field(default_factory=list)
|
|
82
|
+
next_cursor: str | None = None
|
|
83
|
+
total_packages: int = 0
|
|
84
|
+
total_symbols: int = 0
|
|
85
|
+
estimated_tokens: int = 0
|
|
86
|
+
|
|
87
|
+
def as_dict(self) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"base_branch": self.base_branch,
|
|
90
|
+
"head_branch": self.head_branch,
|
|
91
|
+
"packages": [p.as_dict() for p in self.packages],
|
|
92
|
+
"changed_routes": self.changed_routes,
|
|
93
|
+
"changed_services": self.changed_services,
|
|
94
|
+
"changed_db": self.changed_db,
|
|
95
|
+
"affected_tests": self.affected_tests,
|
|
96
|
+
"next_cursor": self.next_cursor,
|
|
97
|
+
"total_packages": self.total_packages,
|
|
98
|
+
"total_symbols": self.total_symbols,
|
|
99
|
+
"estimated_tokens": self.estimated_tokens,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compare_branch_architecture(
|
|
104
|
+
repository: Path,
|
|
105
|
+
base_branch: str = "main",
|
|
106
|
+
head_branch: str = "HEAD",
|
|
107
|
+
con: sqlite3.Connection | None = None,
|
|
108
|
+
limit: int = 50,
|
|
109
|
+
cursor: str | None = None,
|
|
110
|
+
max_tokens: int = 4000,
|
|
111
|
+
) -> BranchArchitectureReport:
|
|
112
|
+
"""Compare two branches and return hierarchical architecture diff."""
|
|
113
|
+
if base_branch == head_branch:
|
|
114
|
+
return BranchArchitectureReport(base_branch=base_branch, head_branch=head_branch)
|
|
115
|
+
|
|
116
|
+
# 1. Run structural branch diff
|
|
117
|
+
diff_report = compare_branches(repository, base_branch=base_branch, head_branch=head_branch, con=con)
|
|
118
|
+
|
|
119
|
+
# 2. Workspace package mapper
|
|
120
|
+
ws = detect_workspace(repository, con=con)
|
|
121
|
+
|
|
122
|
+
# 3. Build hierarchical package -> module -> symbol map
|
|
123
|
+
package_map: dict[str, dict[str, list[SymbolChangeItem]]] = {}
|
|
124
|
+
services: set[str] = set()
|
|
125
|
+
|
|
126
|
+
all_symbols = diff_report.added_symbols + diff_report.changed_symbols + diff_report.removed_symbols
|
|
127
|
+
# Sort deterministically by path and name
|
|
128
|
+
all_symbols.sort(key=lambda s: (s.path, s.name))
|
|
129
|
+
|
|
130
|
+
# Apply cursor pagination if cursor provided
|
|
131
|
+
start_idx = 0
|
|
132
|
+
if cursor and cursor.isdigit():
|
|
133
|
+
start_idx = int(cursor)
|
|
134
|
+
|
|
135
|
+
paged_symbols = all_symbols[start_idx : start_idx + limit]
|
|
136
|
+
next_cursor = str(start_idx + limit) if (start_idx + limit) < len(all_symbols) else None
|
|
137
|
+
|
|
138
|
+
for s in paged_symbols:
|
|
139
|
+
pkg = ws.get_package_for_file(s.path)
|
|
140
|
+
pkg_id = pkg.package_id if pkg else "root"
|
|
141
|
+
|
|
142
|
+
if pkg_id not in package_map:
|
|
143
|
+
package_map[pkg_id] = {}
|
|
144
|
+
if s.path not in package_map[pkg_id]:
|
|
145
|
+
package_map[pkg_id][s.path] = []
|
|
146
|
+
|
|
147
|
+
# Find associated relationships
|
|
148
|
+
rels: list[str] = []
|
|
149
|
+
for r in diff_report.changed_routes:
|
|
150
|
+
if r.handler == s.name:
|
|
151
|
+
rels.append(f"{r.change_type} ROUTE {r.method} {r.path}")
|
|
152
|
+
for db in diff_report.db_impact:
|
|
153
|
+
if db.get("symbol") == s.name:
|
|
154
|
+
rels.append(f"{db.get('change_type')} DB {db.get('operation')} {db.get('table')}")
|
|
155
|
+
|
|
156
|
+
if "service" in s.name.lower() or "service" in s.path.lower():
|
|
157
|
+
services.add(s.name)
|
|
158
|
+
|
|
159
|
+
package_map[pkg_id][s.path].append(
|
|
160
|
+
SymbolChangeItem(
|
|
161
|
+
name=s.name,
|
|
162
|
+
kind=s.kind,
|
|
163
|
+
change_type=s.change_type,
|
|
164
|
+
line=s.new_line or s.old_line,
|
|
165
|
+
relationships=rels,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Build ordered PackageChangeItem objects
|
|
170
|
+
package_items: list[PackageChangeItem] = []
|
|
171
|
+
total_sym_count = 0
|
|
172
|
+
|
|
173
|
+
for pkg_id in sorted(package_map.keys()):
|
|
174
|
+
module_items: list[ModuleChangeItem] = []
|
|
175
|
+
for f_path in sorted(package_map[pkg_id].keys()):
|
|
176
|
+
mod_name = Path(f_path).stem
|
|
177
|
+
sym_items = package_map[pkg_id][f_path]
|
|
178
|
+
total_sym_count += len(sym_items)
|
|
179
|
+
module_items.append(ModuleChangeItem(module=mod_name, file_path=f_path, symbols=sym_items))
|
|
180
|
+
package_items.append(PackageChangeItem(package_id=pkg_id, modules=module_items))
|
|
181
|
+
|
|
182
|
+
# Rough token estimation: ~25 tokens per symbol + overhead
|
|
183
|
+
token_est = total_sym_count * 25 + len(diff_report.changed_routes) * 15 + 150
|
|
184
|
+
|
|
185
|
+
return BranchArchitectureReport(
|
|
186
|
+
base_branch=base_branch,
|
|
187
|
+
head_branch=head_branch,
|
|
188
|
+
packages=package_items,
|
|
189
|
+
changed_routes=[r.as_dict() for r in diff_report.changed_routes],
|
|
190
|
+
changed_services=sorted(services),
|
|
191
|
+
changed_db=diff_report.db_impact,
|
|
192
|
+
affected_tests=diff_report.affected_tests,
|
|
193
|
+
next_cursor=next_cursor,
|
|
194
|
+
total_packages=len(package_items),
|
|
195
|
+
total_symbols=total_sym_count,
|
|
196
|
+
estimated_tokens=token_est,
|
|
197
|
+
)
|