codegraph-engine 3.0.0__2-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,547 @@
1
+ """Production-Grade Semantic Change Impact & PR Blast Radius Engine.
2
+
3
+ Extends Git and AST impact analysis by tracing complete downstream chains:
4
+ changed symbol / git diff
5
+ ↓
6
+ direct & transitive callers
7
+ ↓
8
+ frontend UI components (React/Next.js/Vue)
9
+ ↓
10
+ framework routes & public endpoints
11
+ ↓
12
+ database readers & writers (mutations, migrations)
13
+ ↓
14
+ covering tests & smart test execution recommendation
15
+ ↓
16
+ monorepo package boundaries & PR risk score
17
+
18
+ Every item carries explicit evidence, relationship type, source location,
19
+ epistemic confidence (FACT, POSSIBLE, UNKNOWN), and breaking change risk.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import sqlite3
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from codegraph.graph.traversal import find_callees, find_callers, find_related_tests
29
+ from codegraph.monorepo import detect_workspace
30
+ from codegraph.structural_diff import compare_revisions
31
+
32
+ _FRONTEND_EXTENSIONS = (".tsx", ".jsx", ".vue", ".svelte")
33
+ _MUTATING_SQL_OPS = frozenset({"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "MIGRATE", "TRUNCATE"})
34
+
35
+
36
+ @dataclass
37
+ class ImpactedEntity:
38
+ name: str
39
+ kind: str # "CALLER" | "CALLEE" | "FRONTEND_COMPONENT" | "ROUTE" | "TEST" | "DB_WRITER" | "DB_READER" | "PACKAGE"
40
+ file: str
41
+ line: int | None
42
+ relationship: str # "CALLS" | "HANDLED_BY" | "TESTS" | "WRITES_TABLE" | "READS_TABLE" | "DEPENDS_ON" | "FETCHES_ROUTE"
43
+ depth: int
44
+ confidence: str # "FACT" | "POSSIBLE" | "UNKNOWN"
45
+ reason: str
46
+ evidence: str
47
+
48
+ def as_dict(self) -> dict[str, Any]:
49
+ return {
50
+ "name": self.name,
51
+ "kind": self.kind,
52
+ "file": self.file,
53
+ "line": self.line,
54
+ "relationship": self.relationship,
55
+ "depth": self.depth,
56
+ "confidence": self.confidence,
57
+ "reason": self.reason,
58
+ "evidence": self.evidence,
59
+ }
60
+
61
+
62
+ @dataclass
63
+ class DeepImpactReport:
64
+ base_ref: str
65
+ head_ref: str
66
+ target_symbol: str | None = None
67
+ changed_files: list[str] = field(default_factory=list)
68
+ changed_symbols: list[dict[str, Any]] = field(default_factory=list)
69
+ direct_callers: list[ImpactedEntity] = field(default_factory=list)
70
+ transitive_callers: list[ImpactedEntity] = field(default_factory=list)
71
+ affected_callees: list[ImpactedEntity] = field(default_factory=list)
72
+ affected_frontend_components: list[ImpactedEntity] = field(default_factory=list)
73
+ affected_routes: list[ImpactedEntity] = field(default_factory=list)
74
+ affected_tests: list[ImpactedEntity] = field(default_factory=list)
75
+ db_readers: list[ImpactedEntity] = field(default_factory=list)
76
+ db_writers: list[ImpactedEntity] = field(default_factory=list)
77
+ affected_packages: list[str] = field(default_factory=list)
78
+ total_impacted_count: int = 0
79
+ blast_radius_score: float = 0.0 # 0.0 - 100.0 scale
80
+ risk_level: str = "LOW" # "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"
81
+ breaking_change_flags: list[str] = field(default_factory=list)
82
+ recommended_test_command: str = ""
83
+
84
+ def as_dict(self) -> dict[str, Any]:
85
+ return {
86
+ "base_ref": self.base_ref,
87
+ "head_ref": self.head_ref,
88
+ "target_symbol": self.target_symbol,
89
+ "changed_files": self.changed_files,
90
+ "changed_symbols": self.changed_symbols,
91
+ "direct_callers": [c.as_dict() for c in self.direct_callers],
92
+ "transitive_callers": [c.as_dict() for c in self.transitive_callers],
93
+ "affected_callees": [c.as_dict() for c in self.affected_callees],
94
+ "affected_frontend_components": [f.as_dict() for f in self.affected_frontend_components],
95
+ "affected_routes": [r.as_dict() for r in self.affected_routes],
96
+ "affected_tests": [t.as_dict() for t in self.affected_tests],
97
+ "db_readers": [d.as_dict() for d in self.db_readers],
98
+ "db_writers": [d.as_dict() for d in self.db_writers],
99
+ "affected_packages": self.affected_packages,
100
+ "total_impacted_count": self.total_impacted_count,
101
+ "blast_radius_score": self.blast_radius_score,
102
+ "risk_level": self.risk_level,
103
+ "breaking_change_flags": self.breaking_change_flags,
104
+ "recommended_test_command": self.recommended_test_command,
105
+ }
106
+
107
+
108
+ def _is_frontend_file(path: str) -> bool:
109
+ return any(path.endswith(ext) for ext in _FRONTEND_EXTENSIONS)
110
+
111
+
112
+ def _generate_test_command(affected_tests: list[ImpactedEntity]) -> str:
113
+ """Generate smart targeted test execution command for only the affected test suite."""
114
+ if not affected_tests:
115
+ return ""
116
+
117
+ test_files = sorted({t.file for t in affected_tests if t.file})
118
+ py_files = [f for f in test_files if f.endswith(".py")]
119
+ js_files = [f for f in test_files if f.endswith((".ts", ".tsx", ".js", ".jsx"))]
120
+
121
+ commands: list[str] = []
122
+
123
+ if py_files:
124
+ test_fn_names = sorted({
125
+ t.name for t in affected_tests
126
+ if t.name and t.file.endswith(".py") and t.name.startswith("test_")
127
+ })
128
+ if test_fn_names and len(test_fn_names) <= 6:
129
+ k_expr = " or ".join(test_fn_names)
130
+ commands.append(f"pytest {' '.join(py_files[:4])} -k \"{k_expr}\"")
131
+ else:
132
+ commands.append(f"pytest {' '.join(py_files[:5])}")
133
+
134
+ if js_files:
135
+ commands.append(f"npm test -- {' '.join(js_files[:4])}")
136
+
137
+ return " && ".join(commands)
138
+
139
+
140
+ def get_deep_change_impact(
141
+ repository: Path,
142
+ con: sqlite3.Connection,
143
+ base: str = "HEAD~1",
144
+ head: str = "HEAD",
145
+ max_depth: int = 2,
146
+ max_results: int = 50,
147
+ symbol: str | None = None,
148
+ ) -> DeepImpactReport:
149
+ """Analyze full blast radius of git revision changes or a specific symbol across AST, UI, routes, tests, DB, and packages."""
150
+ changed_symbols_list: list[dict[str, Any]] = []
151
+ changed_files: list[str] = []
152
+
153
+ # 1. Target symbol mode: analyze blast radius of a specific symbol on demand
154
+ if symbol:
155
+ clean_sym = symbol.strip()
156
+ sym_rows = con.execute(
157
+ "SELECT name, path, canonical_id, start_line, kind FROM symbols "
158
+ "WHERE canonical_id = ? OR qualified_name = ? OR name = ? LIMIT 10",
159
+ (clean_sym, clean_sym, clean_sym),
160
+ ).fetchall()
161
+ if sym_rows:
162
+ for sr in sym_rows:
163
+ changed_symbols_list.append({
164
+ "name": str(sr["name"]),
165
+ "path": str(sr["path"]),
166
+ "canonical_id": str(sr["canonical_id"]),
167
+ "line": int(sr["start_line"] or 1),
168
+ "kind": str(sr["kind"]),
169
+ "change_type": "TARGET",
170
+ })
171
+ changed_files.append(str(sr["path"]))
172
+ else:
173
+ changed_symbols_list.append({
174
+ "name": clean_sym,
175
+ "path": "",
176
+ "canonical_id": clean_sym,
177
+ "change_type": "TARGET",
178
+ })
179
+ else:
180
+ # Fast path: identical refs have zero changes
181
+ if base == head and head != "WORKING_TREE":
182
+ return DeepImpactReport(base_ref=base, head_ref=head)
183
+
184
+ diff_report = compare_revisions(repository, base=base, head=head, con=con)
185
+ changed_symbols_list = [
186
+ s.as_dict() for s in (diff_report.changed_symbols + diff_report.added_symbols + diff_report.removed_symbols)
187
+ ]
188
+ changed_files = diff_report.modified_files + diff_report.added_files + diff_report.deleted_files
189
+
190
+ if not changed_symbols_list and not changed_files:
191
+ return DeepImpactReport(base_ref=base, head_ref=head)
192
+
193
+ direct_callers: list[ImpactedEntity] = []
194
+ transitive_callers: list[ImpactedEntity] = []
195
+ affected_callees: list[ImpactedEntity] = []
196
+ affected_frontend_components: list[ImpactedEntity] = []
197
+ affected_routes: list[ImpactedEntity] = []
198
+ affected_tests: list[ImpactedEntity] = []
199
+ db_readers: list[ImpactedEntity] = []
200
+ db_writers: list[ImpactedEntity] = []
201
+
202
+ seen_keys: set[str] = set()
203
+ processed_symbols: set[str] = set()
204
+ processed_transitive: set[str] = set()
205
+
206
+ for s_info in changed_symbols_list[:max_results]:
207
+ s_name = s_info["name"]
208
+ s_path = s_info.get("path") or ""
209
+ s_cid = s_info.get("canonical_id") or s_name
210
+
211
+ if not s_name or s_name in processed_symbols:
212
+ continue
213
+ processed_symbols.add(s_name)
214
+
215
+ # 1. Direct callers (1-hop)
216
+ raw_callers = find_callers(con, s_name, max_results=max_results)
217
+ # Also check canonical_id callers if distinct
218
+ if s_cid != s_name:
219
+ raw_callers.extend(find_callers(con, s_cid, max_results=max_results))
220
+
221
+ for c in raw_callers:
222
+ c_name = c.get("caller") or c.get("symbol") or ""
223
+ c_file = c.get("file") or ""
224
+ c_line = c.get("line")
225
+
226
+ # Check if this caller is a frontend UI component (Pillar 1 bridge)
227
+ is_frontend = _is_frontend_file(c_file)
228
+ c_kind = "FRONTEND_COMPONENT" if is_frontend else "CALLER"
229
+ c_key = f"{c_kind}:1:{c_file}:{c_name}"
230
+
231
+ if c_key not in seen_keys:
232
+ seen_keys.add(c_key)
233
+ caller_entity = ImpactedEntity(
234
+ name=c_name,
235
+ kind=c_kind,
236
+ file=c_file,
237
+ line=c_line,
238
+ relationship="CALLS",
239
+ depth=1,
240
+ confidence="FACT",
241
+ reason=f"{'Frontend component' if is_frontend else 'Caller'} invokes modified symbol {s_name}",
242
+ evidence=f"{c_file}:{c_line} -> {s_name}",
243
+ )
244
+ if is_frontend:
245
+ affected_frontend_components.append(caller_entity)
246
+ else:
247
+ direct_callers.append(caller_entity)
248
+
249
+ # 2. Transitive callers (2-hop) if max_depth >= 2
250
+ if max_depth >= 2 and c_name and c_name not in processed_transitive:
251
+ processed_transitive.add(c_name)
252
+ raw_trans = find_callers(con, c_name, max_results=10)
253
+ for tc in raw_trans:
254
+ tc_name = tc.get("caller") or tc.get("symbol") or ""
255
+ tc_file = tc.get("file") or ""
256
+ tc_line = tc.get("line")
257
+ tc_fe = _is_frontend_file(tc_file)
258
+ tc_kind = "FRONTEND_COMPONENT" if tc_fe else "CALLER"
259
+ tc_key = f"{tc_kind}:2:{tc_file}:{tc_name}"
260
+
261
+ if tc_key not in seen_keys and tc_name != s_name:
262
+ seen_keys.add(tc_key)
263
+ trans_entity = ImpactedEntity(
264
+ name=tc_name,
265
+ kind=tc_kind,
266
+ file=tc_file,
267
+ line=tc_line,
268
+ relationship="CALLS",
269
+ depth=2,
270
+ confidence="FACT",
271
+ reason=f"Transitively invokes {s_name} via {c_name}",
272
+ evidence=f"{tc_file}:{tc_line} -> {c_name} -> {s_name}",
273
+ )
274
+ if tc_fe:
275
+ affected_frontend_components.append(trans_entity)
276
+ else:
277
+ transitive_callers.append(trans_entity)
278
+
279
+ # 3. Direct callees
280
+ raw_callees = find_callees(con, s_name, max_results=max_results)
281
+ for ce in raw_callees:
282
+ ce_name = ce.get("callee") or ce.get("symbol") or ""
283
+ ce_file = ce.get("file") or ""
284
+ ce_line = ce.get("line")
285
+ ce_key = f"CALLEE:{ce_file}:{ce_name}"
286
+ if ce_key not in seen_keys:
287
+ seen_keys.add(ce_key)
288
+ affected_callees.append(
289
+ ImpactedEntity(
290
+ name=ce_name,
291
+ kind="CALLEE",
292
+ file=ce_file,
293
+ line=ce_line,
294
+ relationship="CALLED_BY",
295
+ depth=1,
296
+ confidence="FACT",
297
+ reason=f"Called by modified symbol {s_name}",
298
+ evidence=f"{s_path} -> {ce_name}",
299
+ )
300
+ )
301
+
302
+ # 4. Route impact across framework_routes, raw_framework_routes, and routes
303
+ candidate_handlers = [(s_name, s_cid, 1, f"changed symbol {s_name}")]
304
+ for c in raw_callers:
305
+ c_cand_name = c.get("caller") or c.get("symbol") or ""
306
+ if c_cand_name:
307
+ candidate_handlers.append((c_cand_name, c_cand_name, 2, f"caller {c_cand_name} of {s_name}"))
308
+
309
+ for rt_table in ("framework_routes", "raw_framework_routes", "routes"):
310
+ try:
311
+ table_check = con.execute(
312
+ f"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='{rt_table}'"
313
+ ).fetchone()
314
+ if not table_check or table_check[0] == 0:
315
+ continue
316
+
317
+ for h_name, h_cid, h_depth, h_reason in candidate_handlers:
318
+ if rt_table == "routes":
319
+ route_rows = con.execute(
320
+ "SELECT path, method, handler_name, file, NULL FROM routes "
321
+ "WHERE handler_name = ? OR symbol = ?",
322
+ (h_name, h_name),
323
+ ).fetchall()
324
+ else:
325
+ route_rows = con.execute(
326
+ f"SELECT route_path, http_method, handler_name, file_path, line FROM {rt_table} "
327
+ "WHERE handler_name = ? OR handler_canonical_id = ? OR handler_canonical_id LIKE ?",
328
+ (h_name, h_cid, f"%{h_name}"),
329
+ ).fetchall()
330
+
331
+ for rr in route_rows:
332
+ r_path = str(rr[0] or "")
333
+ r_method = str(rr[1] or "GET").upper()
334
+ r_file = str(rr[3] or "")
335
+ r_line = int(rr[4]) if rr[4] is not None else None
336
+ r_key = f"ROUTE:{r_path}:{r_method}"
337
+ if r_key not in seen_keys:
338
+ seen_keys.add(r_key)
339
+ affected_routes.append(
340
+ ImpactedEntity(
341
+ name=f"{r_method} {r_path}",
342
+ kind="ROUTE",
343
+ file=r_file,
344
+ line=r_line,
345
+ relationship="ROUTES_TO",
346
+ depth=h_depth,
347
+ confidence="FACT",
348
+ reason=f"Endpoint reached via {h_reason}",
349
+ evidence=f"Route {r_method} {r_path} -> {h_name} ({r_file})",
350
+ )
351
+ )
352
+
353
+ # Check if any frontend component fetches this route
354
+ try:
355
+ fetch_rows = con.execute(
356
+ "SELECT target_name, file_path, line, scope FROM local_bindings "
357
+ "WHERE expr_kind = 'REACT_FETCH_ROUTE' AND (target_name = ? OR target_name LIKE ?)",
358
+ (r_path, f"%{r_path.rstrip('/')}%"),
359
+ ).fetchall()
360
+ for fr in fetch_rows:
361
+ fe_name = str(fr[3] or Path(str(fr[1])).stem)
362
+ fe_file = str(fr[1])
363
+ fe_line = int(fr[2] or 1)
364
+ fe_key = f"FRONTEND_COMPONENT:{fe_file}:{fe_name}"
365
+ if fe_key not in seen_keys:
366
+ seen_keys.add(fe_key)
367
+ affected_frontend_components.append(
368
+ ImpactedEntity(
369
+ name=fe_name,
370
+ kind="FRONTEND_COMPONENT",
371
+ file=fe_file,
372
+ line=fe_line,
373
+ relationship="FETCHES_ROUTE",
374
+ depth=h_depth + 1,
375
+ confidence="FACT",
376
+ reason=f"Frontend component fetches affected route {r_method} {r_path}",
377
+ evidence=f"{fe_file}:{fe_line} -> {r_path}",
378
+ )
379
+ )
380
+ except sqlite3.OperationalError:
381
+ pass
382
+ break
383
+ except (sqlite3.OperationalError, sqlite3.DatabaseError):
384
+ pass
385
+
386
+ # 5. Tests targeting this symbol
387
+ tests = find_related_tests(con, s_name, max_results=30)
388
+ if s_cid != s_name:
389
+ tests.extend(find_related_tests(con, s_cid, max_results=30))
390
+
391
+ for t in tests:
392
+ if t.get("result") == "no_static_link_found":
393
+ continue
394
+ t_name = str(t.get("symbol") or t.get("test") or "").strip()
395
+ t_file = str(t.get("file") or "").strip()
396
+ if not t_name and not t_file:
397
+ continue
398
+
399
+ t_line_raw = t.get("line")
400
+ t_line = int(t_line_raw) if isinstance(t_line_raw, (int, str)) and str(t_line_raw).isdigit() else None
401
+ t_key = f"TEST:{t_file}:{t_name}"
402
+ if t_key not in seen_keys:
403
+ seen_keys.add(t_key)
404
+ affected_tests.append(
405
+ ImpactedEntity(
406
+ name=t_name,
407
+ kind="TEST",
408
+ file=t_file,
409
+ line=t_line,
410
+ relationship="TESTS",
411
+ depth=1,
412
+ confidence="FACT",
413
+ reason=f"Test targets modified symbol {s_name}",
414
+ evidence=f"{t_file}:{t_line} tests {s_name}",
415
+ )
416
+ )
417
+
418
+ # 6. Database readers and writers
419
+ # Query db_queries table (and database_queries for backward compatibility)
420
+ for db_table in ("db_queries", "database_queries"):
421
+ try:
422
+ table_check = con.execute(
423
+ f"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='{db_table}'"
424
+ ).fetchone()
425
+ if not table_check or table_check[0] == 0:
426
+ continue
427
+
428
+ if db_table == "db_queries":
429
+ db_rows = con.execute(
430
+ "SELECT table_name, operation, file_path, start_line FROM db_queries "
431
+ "WHERE caller_symbol_id = ? OR caller_symbol_id LIKE ?",
432
+ (s_cid, f"%{s_name}"),
433
+ ).fetchall()
434
+ else:
435
+ db_rows = con.execute(
436
+ "SELECT table_name, operation, file, line FROM database_queries "
437
+ "WHERE symbol = ? OR caller_symbol = ?",
438
+ (s_name, s_name),
439
+ ).fetchall()
440
+
441
+ for dr in db_rows:
442
+ tbl = str(dr[0] or "")
443
+ op = str(dr[1] or "SELECT").upper()
444
+ f_path = str(dr[2] or s_path)
445
+ l_num = int(dr[3]) if dr[3] is not None else None
446
+ is_mutating = op in _MUTATING_SQL_OPS
447
+ kind = "DB_WRITER" if is_mutating else "DB_READER"
448
+ rel = "WRITES_TABLE" if is_mutating else "READS_TABLE"
449
+ db_key = f"{kind}:{tbl}:{op}:{s_name}"
450
+ if db_key not in seen_keys:
451
+ seen_keys.add(db_key)
452
+ entity = ImpactedEntity(
453
+ name=f"{op} {tbl}",
454
+ kind=kind,
455
+ file=f_path,
456
+ line=l_num,
457
+ relationship=rel,
458
+ depth=1,
459
+ confidence="FACT",
460
+ reason=f"{s_name} executes {op} on {tbl}",
461
+ evidence=f"{s_name} -> {op} {tbl} ({f_path}:{l_num})",
462
+ )
463
+ if is_mutating:
464
+ db_writers.append(entity)
465
+ else:
466
+ db_readers.append(entity)
467
+ break
468
+ except (sqlite3.OperationalError, sqlite3.DatabaseError):
469
+ pass
470
+
471
+ # 7. Monorepo package impact
472
+ ws = detect_workspace(repository, con=con)
473
+ affected_packages: set[str] = set()
474
+ for p in changed_files:
475
+ pkg = ws.get_package_for_file(p)
476
+ if pkg:
477
+ affected_packages.add(pkg.package_id)
478
+ for dep in ws.get_dependent_packages(pkg.package_id):
479
+ affected_packages.add(dep.package_id)
480
+
481
+ total_count = (
482
+ len(direct_callers)
483
+ + len(transitive_callers)
484
+ + len(affected_callees)
485
+ + len(affected_frontend_components)
486
+ + len(affected_routes)
487
+ + len(affected_tests)
488
+ + len(db_readers)
489
+ + len(db_writers)
490
+ )
491
+
492
+ # 8. Blast radius score calculation: 0 - 100 scale
493
+ score = min(
494
+ 100.0,
495
+ (len(direct_callers) * 3.5)
496
+ + (len(transitive_callers) * 1.5)
497
+ + (len(affected_frontend_components) * 5.0)
498
+ + (len(affected_routes) * 8.0)
499
+ + (len(db_writers) * 6.0)
500
+ + (len(affected_packages) * 5.0),
501
+ )
502
+
503
+ # Risk level classification
504
+ if score >= 75.0:
505
+ risk_level = "CRITICAL"
506
+ elif score >= 50.0:
507
+ risk_level = "HIGH"
508
+ elif score >= 25.0:
509
+ risk_level = "MEDIUM"
510
+ else:
511
+ risk_level = "LOW"
512
+
513
+ # Breaking change flags
514
+ breaking_change_flags: list[str] = []
515
+ if affected_routes:
516
+ breaking_change_flags.append("PUBLIC_API_MODIFIED")
517
+ if db_writers:
518
+ breaking_change_flags.append("DB_MUTATION_IMPACT")
519
+ if len(affected_packages) > 1:
520
+ breaking_change_flags.append("CROSS_PACKAGE_IMPACT")
521
+ if changed_symbols_list and not affected_tests:
522
+ breaking_change_flags.append("UNCOVERED_BY_TESTS")
523
+
524
+ # Smart test command recommendation
525
+ rec_test_cmd = _generate_test_command(affected_tests)
526
+
527
+ return DeepImpactReport(
528
+ base_ref=base,
529
+ head_ref=head,
530
+ target_symbol=symbol,
531
+ changed_files=changed_files,
532
+ changed_symbols=changed_symbols_list,
533
+ direct_callers=direct_callers[:max_results],
534
+ transitive_callers=transitive_callers[:max_results],
535
+ affected_callees=affected_callees[:max_results],
536
+ affected_frontend_components=affected_frontend_components[:max_results],
537
+ affected_routes=affected_routes,
538
+ affected_tests=affected_tests[:max_results],
539
+ db_readers=db_readers,
540
+ db_writers=db_writers,
541
+ affected_packages=sorted(affected_packages),
542
+ total_impacted_count=total_count,
543
+ blast_radius_score=round(score, 1),
544
+ risk_level=risk_level,
545
+ breaking_change_flags=breaking_change_flags,
546
+ recommended_test_command=rec_test_cmd,
547
+ )