blasttrace 1.2.0__py2.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.

Potentially problematic release.


This version of blasttrace might be problematic. Click here for more details.

blasttrace/server.py ADDED
@@ -0,0 +1,935 @@
1
+ """
2
+ BlastTrace FastMCP Server & Standalone CLI
3
+ Pre-Edit Full-Stack Breaking-Change & Contract Blast-Radius Engine.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from typing import List, Dict, Any, Optional
11
+
12
+ # Ensure project root and build directories are on sys.path
13
+ PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ ROOT_DIR = os.path.dirname(PACKAGE_DIR)
15
+ for candidate in (ROOT_DIR, os.path.join(ROOT_DIR, "build")):
16
+ if candidate not in sys.path:
17
+ sys.path.insert(0, candidate)
18
+
19
+ from blasttrace.license import (
20
+ activate_license,
21
+ deactivate_license,
22
+ verify_license,
23
+ get_license_status,
24
+ get_runtime_token,
25
+ generate_tester_license_key,
26
+ )
27
+ from blasttrace.parsers.contract_linker import (
28
+ build_fullstack_topology,
29
+ resolve_entity_id,
30
+ )
31
+ from blasttrace.parsers.sql_schema_parser import parse_schema_file
32
+ from blasttrace.parsers.python_ast_parser import parse_python_file
33
+ from blasttrace.parsers.ts_react_parser import parse_ts_react_file
34
+ from blasttrace.parsers.api_route_parser import parse_api_file
35
+ from blasttrace.classifier import (
36
+ classify_proposed_edit,
37
+ format_evidence_chains,
38
+ )
39
+ from blasttrace.visualizer import (
40
+ generate_mermaid_blast_radius,
41
+ generate_mermaid_contract_chain,
42
+ generate_mermaid_cycles,
43
+ )
44
+ from blasttrace.remediation import (
45
+ generate_surgical_sync_plan,
46
+ generate_cycle_refactoring_plan,
47
+ )
48
+
49
+ from collections import deque
50
+
51
+ class _DeterministicReachabilityEngine:
52
+ """
53
+ BlastTrace Native Core — Pure-environment deterministic dependency reachability engine.
54
+ Ensures seamless cross-platform execution on Windows, macOS, and Linux without external build tools.
55
+ """
56
+ def __init__(self):
57
+ self._topology_links: Dict[str, List[str]] = {}
58
+
59
+ def add_edge(self, u: str, v: str) -> None:
60
+ if u not in self._topology_links:
61
+ self._topology_links[u] = []
62
+ if v not in self._topology_links[u]:
63
+ self._topology_links[u].append(v)
64
+ if v not in self._topology_links:
65
+ self._topology_links[v] = []
66
+
67
+ def find_shortest_path(self, start: str, target: str) -> List[str]:
68
+ if start not in self._topology_links or target not in self._topology_links:
69
+ return []
70
+ origin_hop: Dict[str, str] = {}
71
+ seen = {start}
72
+ horizon = deque([start])
73
+ reached = False
74
+ while horizon:
75
+ curr = horizon.popleft()
76
+ if curr == target:
77
+ reached = True
78
+ break
79
+ for nxt in self._topology_links.get(curr, []):
80
+ if nxt not in seen:
81
+ seen.add(nxt)
82
+ origin_hop[nxt] = curr
83
+ horizon.append(nxt)
84
+ if not reached:
85
+ return []
86
+ trace_seq = []
87
+ curr = target
88
+ while curr != start:
89
+ trace_seq.append(curr)
90
+ curr = origin_hop[curr]
91
+ trace_seq.append(start)
92
+ trace_seq.reverse()
93
+ return trace_seq
94
+
95
+ def trace_blast_radius_paths(self, start: str, max_hops: int = 4) -> List[List[str]]:
96
+ all_chains: List[List[str]] = []
97
+ if start not in self._topology_links or max_hops <= 0:
98
+ return all_chains
99
+ origin_hop: Dict[str, str] = {}
100
+ hop_depth: Dict[str, int] = {start: 0}
101
+ horizon = deque([start])
102
+ while horizon:
103
+ curr = horizon.popleft()
104
+ d = hop_depth[curr]
105
+ if d >= max_hops:
106
+ continue
107
+ for nxt in self._topology_links.get(curr, []):
108
+ if nxt not in hop_depth:
109
+ hop_depth[nxt] = d + 1
110
+ origin_hop[nxt] = curr
111
+ horizon.append(nxt)
112
+ chain = []
113
+ walk = nxt
114
+ while walk != start:
115
+ chain.append(walk)
116
+ walk = origin_hop[walk]
117
+ chain.append(start)
118
+ chain.reverse()
119
+ all_chains.append(chain)
120
+ return all_chains
121
+
122
+ def detect_circular_dependencies(self, runtime_token: str = "") -> List[List[str]]:
123
+ idx_map: Dict[str, int] = {}
124
+ boundary_map: Dict[str, int] = {}
125
+ active_frame: Dict[str, bool] = {}
126
+ frame_st: List[str] = []
127
+ seq_counter = [0]
128
+ detected_rings: List[List[str]] = []
129
+
130
+ def _eval_recursive_ring(u: str):
131
+ seq_counter[0] += 1
132
+ idx_map[u] = boundary_map[u] = seq_counter[0]
133
+ frame_st.append(u)
134
+ active_frame[u] = True
135
+
136
+ for v in self._topology_links.get(u, []):
137
+ if v not in idx_map:
138
+ _eval_recursive_ring(v)
139
+ boundary_map[u] = min(boundary_map[u], boundary_map[v])
140
+ elif active_frame.get(v, False):
141
+ boundary_map[u] = min(boundary_map[u], idx_map[v])
142
+
143
+ if boundary_map[u] == idx_map[u]:
144
+ ring_group = []
145
+ while True:
146
+ v = frame_st.pop()
147
+ active_frame[v] = False
148
+ ring_group.append(v)
149
+ if u == v:
150
+ break
151
+ if len(ring_group) > 1 or (u in self._topology_links.get(u, [])):
152
+ detected_rings.append(ring_group)
153
+
154
+ for node in list(self._topology_links.keys()):
155
+ if node not in idx_map:
156
+ _eval_recursive_ring(node)
157
+
158
+ return detected_rings
159
+
160
+
161
+ try:
162
+ import blasttrace_cpp_core
163
+ CppBlastEngine = blasttrace_cpp_core.CppBlastEngine
164
+ except ImportError:
165
+ CppBlastEngine = _DeterministicReachabilityEngine
166
+
167
+ from fastmcp import FastMCP
168
+
169
+ mcp = FastMCP(name="BlastTrace")
170
+ IS_PRO_TIER = False
171
+
172
+
173
+ def _populate_cpp_engine(topology: Dict[str, Any]) -> Any:
174
+ """Instantiates and populates the BlastTrace Native Core from parsed full-stack topology."""
175
+ engine = CppBlastEngine()
176
+ for edge in topology.get("edges", []):
177
+ engine.add_edge(edge["source"], edge["target"])
178
+ return engine
179
+
180
+
181
+ def _pro_gate_error(tool_label: str) -> str:
182
+ return json.dumps(
183
+ {
184
+ "error": "Pro License Required",
185
+ "engine": "BlastTrace Native Core",
186
+ "tier": "COMMUNITY",
187
+ "message": f"{tool_label} requires an active BlastTrace Pro license. Run 'blasttrace activate <KEY>' to unlock.",
188
+ },
189
+ indent=2,
190
+ )
191
+
192
+
193
+ # ============================================================================
194
+ # FREE COMMUNITY TIER TOOLS (2 Tools)
195
+ # ============================================================================
196
+
197
+
198
+ @mcp.tool()
199
+ def index_repository_topology(repo_path: str) -> str:
200
+ """
201
+ Scans the repository across Database, Backend, API, Frontend, and Tests,
202
+ populates the BlastTrace Native Core, and returns structural summary counts.
203
+ """
204
+ topology = build_fullstack_topology(repo_path)
205
+ if "error" in topology:
206
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
207
+
208
+ _populate_cpp_engine(topology)
209
+ layer_counts: Dict[str, int] = {"database": 0, "backend": 0, "api": 0, "frontend": 0, "tests": 0}
210
+ for n in topology["nodes"]:
211
+ layer = n.get("layer", "backend")
212
+ layer_counts[layer] = layer_counts.get(layer, 0) + 1
213
+
214
+ return json.dumps(
215
+ {
216
+ "status": "Indexed",
217
+ "engine": "BlastTrace Native Core",
218
+ "tier": "PRO" if verify_license() else "COMMUNITY",
219
+ "repo_root": topology["repo_root"],
220
+ "node_count": len(topology["nodes"]),
221
+ "edge_count": len(topology["edges"]),
222
+ "layer_breakdown": layer_counts,
223
+ "tables_indexed": list(topology["db_tables"].keys()),
224
+ "api_routes_indexed": [f"{r['method']} {r['path']}" for r in topology["api_routes"]],
225
+ "test_suites_indexed": list(topology["test_suites"].keys()),
226
+ },
227
+ indent=2,
228
+ )
229
+
230
+
231
+ @mcp.tool()
232
+ def analyze_symbol_reachability(repo_path: str, target_symbol: str, max_hops: int = 4) -> str:
233
+ """
234
+ Traces direct and transitive downstream callers/importers of a function, class, or file
235
+ within the repository using the Deterministic Dependency Reachability Engine.
236
+ """
237
+ topology = build_fullstack_topology(repo_path)
238
+ if "error" in topology:
239
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
240
+
241
+ engine = _populate_cpp_engine(topology)
242
+ matched_ids = resolve_entity_id(topology, target_symbol)
243
+
244
+ all_chains: List[List[str]] = []
245
+ impacted_symbols: List[str] = []
246
+ for start_id in matched_ids:
247
+ chains = engine.trace_blast_radius_paths(start_id, max_hops)
248
+ for ch in chains:
249
+ all_chains.append(ch)
250
+ for nid in ch[1:]:
251
+ if nid not in impacted_symbols:
252
+ impacted_symbols.append(nid)
253
+
254
+ return json.dumps(
255
+ {
256
+ "status": "Reachability Evaluated",
257
+ "engine": "BlastTrace Native Core",
258
+ "tier": "PRO" if verify_license() else "COMMUNITY",
259
+ "target_symbol": target_symbol,
260
+ "resolved_node_ids": matched_ids,
261
+ "max_hops": max_hops,
262
+ "impacted_count": len(impacted_symbols),
263
+ "impacted_nodes": impacted_symbols,
264
+ "reachability_chains": format_evidence_chains(topology, all_chains),
265
+ },
266
+ indent=2,
267
+ )
268
+
269
+
270
+ # ============================================================================
271
+ # PRO TIER TOOLS (8 Tools — Gated by verify_license() + Native C++ Seal)
272
+ # ============================================================================
273
+
274
+
275
+ @mcp.tool()
276
+ def verify_pre_edit_safety_gate(
277
+ repo_path: str,
278
+ file_path: str,
279
+ symbol_name: str,
280
+ proposed_signature_or_diff: str,
281
+ ) -> str:
282
+ """
283
+ [PRO TIER] Flagship Pre-Edit Breaking-Change Gate: Compares existing symbol/schema/route
284
+ contract against the proposed edit, classifies severity (PASS / WARN / BLOCK), traces
285
+ all downstream consumers across DB/Backend/API/Frontend/Tests, and returns a Mermaid
286
+ blast-radius diagram and surgical multi-file synchronization plan.
287
+ """
288
+ if not verify_license():
289
+ return _pro_gate_error("Pre-Edit Breaking-Change Safety Gate")
290
+
291
+ topology = build_fullstack_topology(repo_path)
292
+ if "error" in topology:
293
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
294
+
295
+ engine = _populate_cpp_engine(topology)
296
+ matched_ids = resolve_entity_id(topology, symbol_name)
297
+ if not matched_ids and file_path:
298
+ matched_ids = resolve_entity_id(topology, f"{os.path.basename(file_path)}:{symbol_name}")
299
+
300
+ downstream_chains: List[List[str]] = []
301
+ for start_id in matched_ids:
302
+ chains = engine.trace_blast_radius_paths(start_id, 6)
303
+ downstream_chains.extend(chains)
304
+
305
+ classification_res = classify_proposed_edit(
306
+ topology,
307
+ file_path,
308
+ symbol_name,
309
+ proposed_signature_or_diff,
310
+ downstream_chains,
311
+ )
312
+ gate_status = classification_res["gate_status"]
313
+ sync_plan = generate_surgical_sync_plan(topology, classification_res, downstream_chains)
314
+ mermaid_diag = generate_mermaid_blast_radius(
315
+ downstream_chains,
316
+ title=f"Pre-Edit Blast Radius: {symbol_name}",
317
+ gate_status=gate_status,
318
+ topology=topology,
319
+ )
320
+
321
+ return json.dumps(
322
+ {
323
+ "engine": "BlastTrace Native Core",
324
+ "tier": "PRO",
325
+ "gate_status": gate_status,
326
+ "classification": classification_res["classification"],
327
+ "target_file": classification_res["target_file"],
328
+ "target_symbol": symbol_name,
329
+ "reasons": classification_res["reasons"],
330
+ "evidence_chains": classification_res["evidence_chains"],
331
+ "mermaid_diagram": mermaid_diag,
332
+ "surgical_sync_plan": sync_plan,
333
+ },
334
+ indent=2,
335
+ )
336
+
337
+
338
+ @mcp.tool()
339
+ def trace_fullstack_contract_chain(repo_path: str, start_entity: str, target_entity: str = "") -> str:
340
+ """
341
+ [PRO TIER] Traces end-to-end contract paths from a Database Column/Table -> ORM Model ->
342
+ Backend Function -> API Route -> Frontend UI Component -> Covering Test Suite.
343
+ """
344
+ if not verify_license():
345
+ return _pro_gate_error("Full-Stack Cross-Boundary Contract Tracing")
346
+
347
+ topology = build_fullstack_topology(repo_path)
348
+ if "error" in topology:
349
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
350
+
351
+ engine = _populate_cpp_engine(topology)
352
+ start_ids = resolve_entity_id(topology, start_entity)
353
+ target_ids = resolve_entity_id(topology, target_entity) if target_entity else []
354
+
355
+ chosen_path: List[str] = []
356
+ all_chains: List[List[str]] = []
357
+
358
+ if start_ids and target_ids:
359
+ for s_id in start_ids:
360
+ for t_id in target_ids:
361
+ path = engine.find_shortest_path(s_id, t_id)
362
+ if path:
363
+ all_chains.append(path)
364
+ if not chosen_path or len(path) > len(chosen_path):
365
+ chosen_path = path
366
+ elif start_ids:
367
+ for s_id in start_ids:
368
+ chains = engine.trace_blast_radius_paths(s_id, 6)
369
+ for ch in chains:
370
+ all_chains.append(ch)
371
+ if len(ch) > len(chosen_path):
372
+ chosen_path = ch
373
+
374
+ evidence = format_evidence_chains(topology, [chosen_path] if chosen_path else all_chains)
375
+ return json.dumps(
376
+ {
377
+ "status": "Contract Chain Identified" if (chosen_path or all_chains) else "No Contract Link Found",
378
+ "engine": "BlastTrace Native Core",
379
+ "tier": "PRO",
380
+ "start_entity": start_entity,
381
+ "target_entity": target_entity or "all_downstream_consumers",
382
+ "path_sequence": chosen_path,
383
+ "hop_count": max(0, len(chosen_path) - 1),
384
+ "evidence_chains": evidence,
385
+ "mermaid_diagram": generate_mermaid_contract_chain(
386
+ chosen_path if chosen_path else (all_chains[0] if all_chains else []),
387
+ title=f"Full-Stack Contract Chain: {start_entity}",
388
+ topology=topology,
389
+ ),
390
+ },
391
+ indent=2,
392
+ )
393
+
394
+
395
+ @mcp.tool()
396
+ def analyze_db_schema_blast_radius(repo_path: str, schema_or_migration_file: str, table_or_column: str) -> str:
397
+ """
398
+ [PRO TIER] Parses SQL/Prisma/ORM changes and identifies every backend query, ORM model,
399
+ API endpoint, frontend UI component, and test suite that will break if the column or table
400
+ is modified or dropped.
401
+ """
402
+ if not verify_license():
403
+ return _pro_gate_error("Database Schema & Migration Blast-Radius Analyzer")
404
+
405
+ topology = build_fullstack_topology(repo_path)
406
+ if "error" in topology:
407
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
408
+
409
+ engine = _populate_cpp_engine(topology)
410
+ matched_ids = resolve_entity_id(topology, f"db:{table_or_column.lower()}")
411
+ if not matched_ids:
412
+ matched_ids = resolve_entity_id(topology, table_or_column)
413
+
414
+ chains: List[List[str]] = []
415
+ for sid in matched_ids:
416
+ chains.extend(engine.trace_blast_radius_paths(sid, 6))
417
+
418
+ impacted_by_layer: Dict[str, List[str]] = {
419
+ "backend_orm_and_functions": [],
420
+ "api_endpoints": [],
421
+ "frontend_components": [],
422
+ "tests": [],
423
+ }
424
+ nodes_by_id = topology["nodes_by_id"]
425
+ for ch in chains:
426
+ for nid in ch[1:]:
427
+ nmeta = nodes_by_id.get(nid, {})
428
+ layer = nmeta.get("layer", "")
429
+ label = f"{nmeta.get('file', '')}:{nmeta.get('name', nid)}" if nmeta.get("file") else nid
430
+ if layer == "backend" and label not in impacted_by_layer["backend_orm_and_functions"]:
431
+ impacted_by_layer["backend_orm_and_functions"].append(label)
432
+ elif layer == "api" and label not in impacted_by_layer["api_endpoints"]:
433
+ impacted_by_layer["api_endpoints"].append(label)
434
+ elif layer == "frontend" and label not in impacted_by_layer["frontend_components"]:
435
+ impacted_by_layer["frontend_components"].append(label)
436
+ elif layer == "tests" and label not in impacted_by_layer["tests"]:
437
+ impacted_by_layer["tests"].append(label)
438
+
439
+ cls_res = classify_proposed_edit(
440
+ topology,
441
+ schema_or_migration_file,
442
+ table_or_column,
443
+ f"DROP COLUMN {table_or_column}",
444
+ chains,
445
+ )
446
+ sync_plan = generate_surgical_sync_plan(topology, cls_res, chains)
447
+
448
+ return json.dumps(
449
+ {
450
+ "status": "Schema Blast Radius Computed",
451
+ "engine": "BlastTrace Native Core",
452
+ "tier": "PRO",
453
+ "schema_file": schema_or_migration_file,
454
+ "target_table_or_column": table_or_column,
455
+ "gate_status": cls_res["gate_status"],
456
+ "classification": cls_res["classification"],
457
+ "impacted_layers": impacted_by_layer,
458
+ "evidence_chains": format_evidence_chains(topology, chains),
459
+ "mermaid_diagram": generate_mermaid_blast_radius(
460
+ chains,
461
+ title=f"DB Schema Blast Radius: {table_or_column}",
462
+ gate_status=cls_res["gate_status"],
463
+ topology=topology,
464
+ ),
465
+ "surgical_sync_plan": sync_plan,
466
+ },
467
+ indent=2,
468
+ )
469
+
470
+
471
+ @mcp.tool()
472
+ def analyze_api_contract_impact(repo_path: str, route_path_or_file: str, changed_field: str = "") -> str:
473
+ """
474
+ [PRO TIER] Traces an API endpoint (/api/v1/...) to all backend handlers and all frontend
475
+ fetch/axios/useQuery consumers, flagging broken payload fields.
476
+ """
477
+ if not verify_license():
478
+ return _pro_gate_error("API Contract Breaking-Change Scanner")
479
+
480
+ topology = build_fullstack_topology(repo_path)
481
+ if "error" in topology:
482
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
483
+
484
+ engine = _populate_cpp_engine(topology)
485
+ query_ids = resolve_entity_id(topology, route_path_or_file)
486
+ if changed_field:
487
+ field_ids = resolve_entity_id(topology, f"{route_path_or_file}.{changed_field}")
488
+ if field_ids:
489
+ query_ids = field_ids + [qid for qid in query_ids if qid not in field_ids]
490
+
491
+ chains: List[List[str]] = []
492
+ for qid in query_ids:
493
+ chains.extend(engine.trace_blast_radius_paths(qid, 5))
494
+
495
+ frontend_consumers: List[str] = []
496
+ covering_tests: List[str] = []
497
+ nodes_by_id = topology["nodes_by_id"]
498
+ for ch in chains:
499
+ for nid in ch[1:]:
500
+ nmeta = nodes_by_id.get(nid, {})
501
+ if nmeta.get("layer") == "frontend":
502
+ entry = f"{nmeta.get('file', '')} ({nmeta.get('name', nid)})"
503
+ if entry not in frontend_consumers:
504
+ frontend_consumers.append(entry)
505
+ elif nmeta.get("layer") == "tests":
506
+ entry = f"{nmeta.get('file', '')} ({nmeta.get('name', nid)})"
507
+ if entry not in covering_tests:
508
+ covering_tests.append(entry)
509
+
510
+ cls_res = classify_proposed_edit(
511
+ topology,
512
+ route_path_or_file,
513
+ route_path_or_file,
514
+ f"removed {changed_field}" if changed_field else "breaking route modification",
515
+ chains,
516
+ )
517
+
518
+ return json.dumps(
519
+ {
520
+ "status": "API Contract Impact Evaluated",
521
+ "engine": "BlastTrace Native Core",
522
+ "tier": "PRO",
523
+ "route_or_file": route_path_or_file,
524
+ "changed_field": changed_field,
525
+ "gate_status": cls_res["gate_status"],
526
+ "classification": "BREAKING_API_CONTRACT" if chains else cls_res["classification"],
527
+ "frontend_consumers": frontend_consumers,
528
+ "covering_tests": covering_tests,
529
+ "evidence_chains": format_evidence_chains(topology, chains),
530
+ "mermaid_diagram": generate_mermaid_blast_radius(
531
+ chains,
532
+ title=f"API Contract Impact: {route_path_or_file}" + (f" [{changed_field}]" if changed_field else ""),
533
+ gate_status=cls_res["gate_status"],
534
+ topology=topology,
535
+ ),
536
+ "surgical_sync_plan": generate_surgical_sync_plan(topology, cls_res, chains),
537
+ },
538
+ indent=2,
539
+ )
540
+
541
+
542
+ @mcp.tool()
543
+ def analyze_python_module_impact(repo_path: str, file_path: str, target_symbol: str) -> str:
544
+ """
545
+ [PRO TIER] Deep Python AST signature, type-hint, decorator, and cross-module call-chain
546
+ impact analysis.
547
+ """
548
+ if not verify_license():
549
+ return _pro_gate_error("Python AST Module & Signature Impact Analyzer")
550
+
551
+ topology = build_fullstack_topology(repo_path)
552
+ if "error" in topology:
553
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
554
+
555
+ abs_target = file_path if os.path.isabs(file_path) else os.path.join(topology["repo_root"], file_path)
556
+ parsed_py = parse_python_file(abs_target, topology["repo_root"])
557
+
558
+ engine = _populate_cpp_engine(topology)
559
+ matched_ids = resolve_entity_id(topology, target_symbol)
560
+ chains: List[List[str]] = []
561
+ for mid in matched_ids:
562
+ chains.extend(engine.trace_blast_radius_paths(mid, 5))
563
+
564
+ sym_details = parsed_py.get("functions", {}).get(target_symbol) or parsed_py.get("classes", {}).get(target_symbol)
565
+
566
+ return json.dumps(
567
+ {
568
+ "status": "Python AST Impact Analyzed",
569
+ "engine": "BlastTrace Native Core",
570
+ "tier": "PRO",
571
+ "file": parsed_py.get("file", file_path),
572
+ "target_symbol": target_symbol,
573
+ "symbol_metadata": sym_details,
574
+ "downstream_chains": format_evidence_chains(topology, chains),
575
+ "mermaid_diagram": generate_mermaid_blast_radius(
576
+ chains,
577
+ title=f"Python Symbol Impact: {target_symbol}",
578
+ gate_status="WARN" if chains else "PASS",
579
+ topology=topology,
580
+ ),
581
+ },
582
+ indent=2,
583
+ )
584
+
585
+
586
+ @mcp.tool()
587
+ def analyze_typescript_frontend_impact(repo_path: str, file_path: str, target_symbol: str) -> str:
588
+ """
589
+ [PRO TIER] Deep TypeScript/React component prop, interface, hook, and API consumer
590
+ blast-radius analysis.
591
+ """
592
+ if not verify_license():
593
+ return _pro_gate_error("TypeScript & React Frontend Impact Analyzer")
594
+
595
+ topology = build_fullstack_topology(repo_path)
596
+ if "error" in topology:
597
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
598
+
599
+ abs_target = file_path if os.path.isabs(file_path) else os.path.join(topology["repo_root"], file_path)
600
+ parsed_fe = parse_ts_react_file(abs_target, topology["repo_root"])
601
+
602
+ engine = _populate_cpp_engine(topology)
603
+ matched_ids = resolve_entity_id(topology, target_symbol)
604
+ chains: List[List[str]] = []
605
+ for mid in matched_ids:
606
+ chains.extend(engine.trace_blast_radius_paths(mid, 5))
607
+
608
+ sym_details = parsed_fe.get("symbols", {}).get(target_symbol) or parsed_fe.get("interfaces", {}).get(target_symbol)
609
+
610
+ return json.dumps(
611
+ {
612
+ "status": "Frontend Component Impact Analyzed",
613
+ "engine": "BlastTrace Native Core",
614
+ "tier": "PRO",
615
+ "file": parsed_fe.get("file", file_path),
616
+ "target_symbol": target_symbol,
617
+ "component_or_interface": sym_details,
618
+ "api_dependencies": parsed_fe.get("api_calls", []),
619
+ "accessed_response_fields": parsed_fe.get("property_accesses", []),
620
+ "downstream_chains": format_evidence_chains(topology, chains),
621
+ "mermaid_diagram": generate_mermaid_blast_radius(
622
+ chains,
623
+ title=f"Frontend Blast Radius: {target_symbol}",
624
+ gate_status="WARN" if chains else "PASS",
625
+ topology=topology,
626
+ ),
627
+ },
628
+ indent=2,
629
+ )
630
+
631
+
632
+ @mcp.tool()
633
+ def select_impacted_tests(repo_path: str, changed_files_or_symbols: List[str]) -> str:
634
+ """
635
+ [PRO TIER] Deterministically selects the exact unit, integration, and E2E test files
636
+ and test cases that cover the modified symbols, schema columns, or API endpoints.
637
+ """
638
+ if not verify_license():
639
+ return _pro_gate_error("Change-Aware Test Selector")
640
+
641
+ topology = build_fullstack_topology(repo_path)
642
+ if "error" in topology:
643
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
644
+
645
+ engine = _populate_cpp_engine(topology)
646
+ nodes_by_id = topology["nodes_by_id"]
647
+
648
+ selected_test_files: List[str] = []
649
+ selected_test_cases: List[str] = []
650
+ evidence_chains: List[List[str]] = []
651
+
652
+ for item in changed_files_or_symbols:
653
+ matched_ids = resolve_entity_id(topology, item)
654
+ for mid in matched_ids:
655
+ chains = engine.trace_blast_radius_paths(mid, 6)
656
+ for ch in chains:
657
+ for nid in ch:
658
+ nmeta = nodes_by_id.get(nid, {})
659
+ if nmeta.get("layer") == "tests":
660
+ evidence_chains.append(ch)
661
+ tfile = nmeta.get("file", "")
662
+ if tfile and tfile not in selected_test_files:
663
+ selected_test_files.append(tfile)
664
+ if nmeta.get("type") == "test_case":
665
+ tc_name = nmeta.get("name", nid)
666
+ if tc_name not in selected_test_cases:
667
+ selected_test_cases.append(tc_name)
668
+
669
+ return json.dumps(
670
+ {
671
+ "status": "Impacted Tests Selected",
672
+ "engine": "BlastTrace Native Core",
673
+ "tier": "PRO",
674
+ "changed_inputs": changed_files_or_symbols,
675
+ "selected_test_files": selected_test_files,
676
+ "selected_test_cases": selected_test_cases,
677
+ "pytest_command": f"pytest {' '.join(selected_test_files)}" if selected_test_files else "pytest",
678
+ "evidence_chains": format_evidence_chains(topology, evidence_chains),
679
+ },
680
+ indent=2,
681
+ )
682
+
683
+
684
+ @mcp.tool()
685
+ def detect_circular_code_dependencies(repo_path: str) -> str:
686
+ """
687
+ [PRO TIER] Uses the BlastTrace Native Core Recursive Loop Analysis engine to detect
688
+ circular import and call loops across modules/packages, outputting a Mermaid cycle graph
689
+ and surgical refactoring patch.
690
+ """
691
+ if not verify_license():
692
+ return _pro_gate_error("Circular Dependency Interceptor")
693
+
694
+ topology = build_fullstack_topology(repo_path)
695
+ if "error" in topology:
696
+ return json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2)
697
+
698
+ engine = CppBlastEngine()
699
+ for mod_edge in topology.get("module_import_edges", []):
700
+ engine.add_edge(mod_edge["source"], mod_edge["target"])
701
+
702
+ runtime_token = get_runtime_token()
703
+ cycles = engine.detect_circular_dependencies(runtime_token)
704
+
705
+ return json.dumps(
706
+ {
707
+ "status": "Circular Dependencies Detected" if cycles else "Clean",
708
+ "engine": "BlastTrace Native Core",
709
+ "tier": "PRO",
710
+ "cycles_detected": len(cycles),
711
+ "dependency_loops": cycles,
712
+ "mermaid_diagram": generate_mermaid_cycles(cycles),
713
+ "surgical_refactoring": generate_cycle_refactoring_plan(cycles),
714
+ },
715
+ indent=2,
716
+ )
717
+
718
+
719
+ # ============================================================================
720
+ # STANDALONE TERMINAL CLI & GIT PRE-COMMIT GATE
721
+ # ============================================================================
722
+
723
+
724
+ def _format_terminal_reachability(data: dict) -> str:
725
+ impacted_count = data.get("impacted_count", 0)
726
+ target = data.get("target_symbol", "")
727
+ tier = data.get("tier", "COMMUNITY")
728
+ chains = data.get("reachability_chains", [])
729
+
730
+ layer_counts = {
731
+ "Database": 0,
732
+ "Backend": 0,
733
+ "Go Microservice": 0,
734
+ "API": 0,
735
+ "Web Storefront (React)": 0,
736
+ "Vue / Nuxt Storefront": 0,
737
+ "Flutter POS / Client": 0,
738
+ "iOS Client (Swift)": 0,
739
+ "Android Client (Kotlin)": 0,
740
+ "Tests": 0,
741
+ }
742
+ test_files = set()
743
+ for nid in data.get("impacted_nodes", []):
744
+ if nid.startswith("db:"):
745
+ layer_counts["Database"] += 1
746
+ elif nid.startswith("py:"):
747
+ layer_counts["Backend"] += 1
748
+ elif nid.startswith("go:"):
749
+ layer_counts["Go Microservice"] += 1
750
+ elif nid.startswith("api:") or nid.startswith("api_field:"):
751
+ layer_counts["API"] += 1
752
+ elif nid.startswith("fe:"):
753
+ layer_counts["Web Storefront (React)"] += 1
754
+ elif nid.startswith("vue:"):
755
+ layer_counts["Vue / Nuxt Storefront"] += 1
756
+ elif nid.startswith("dart:"):
757
+ layer_counts["Flutter POS / Client"] += 1
758
+ elif nid.startswith("swift:"):
759
+ layer_counts["iOS Client (Swift)"] += 1
760
+ elif nid.startswith("kotlin:"):
761
+ layer_counts["Android Client (Kotlin)"] += 1
762
+ elif nid.startswith("test:"):
763
+ layer_counts["Tests"] += 1
764
+ test_files.add(nid.replace("test:", "").split("::")[0])
765
+
766
+ lines = [
767
+ "━" * 60,
768
+ " BlastTrace Contract Reachability Summary",
769
+ "━" * 60,
770
+ f" Target Symbol : {target}",
771
+ f" License Tier : {tier}",
772
+ f" Total Blast Radius : {impacted_count} downstream node(s)",
773
+ "",
774
+ " Downstream Breakdown by Layer:"
775
+ ]
776
+ for layer, count in layer_counts.items():
777
+ if count > 0:
778
+ lines.append(f" • {layer:<22} : {count} impacted")
779
+
780
+ if chains:
781
+ lines.append("")
782
+ lines.append(" Immediate Downstream Paths (Top 5):")
783
+ for ch in chains[:5]:
784
+ lines.append(f" -> {ch}")
785
+ if len(chains) > 5:
786
+ lines.append(f" ... and {len(chains) - 5} more path(s) (pass --json for full trace)")
787
+
788
+ if test_files:
789
+ lines.append("")
790
+ lines.append(" Targeted Test Command:")
791
+ lines.append(f" pytest {' '.join(sorted(test_files)[:4])}")
792
+
793
+ lines.append("━" * 60)
794
+ return "\n".join(lines)
795
+
796
+
797
+ def _run_git_diff_gate(repo_path: str, is_pre_commit: bool = False) -> int:
798
+ """Inspects modified files in git working tree / staged index and evaluates blast radius."""
799
+ cmd = ["git", "diff", "--cached", "--name-only"] if is_pre_commit else ["git", "diff", "--name-only"]
800
+ try:
801
+ out = subprocess.check_output(cmd, cwd=repo_path, stderr=subprocess.DEVNULL, text=True)
802
+ changed_files = [line.strip() for line in out.splitlines() if line.strip()]
803
+ except Exception:
804
+ changed_files = []
805
+
806
+ topology = build_fullstack_topology(repo_path)
807
+ if "error" in topology:
808
+ print(json.dumps({"engine": "BlastTrace Native Core", "error": topology["error"]}, indent=2))
809
+ return 1
810
+
811
+ engine = _populate_cpp_engine(topology)
812
+ impacts: List[str] = []
813
+ for fpath in changed_files:
814
+ for mid in resolve_entity_id(topology, fpath):
815
+ chains = engine.trace_blast_radius_paths(mid, 5)
816
+ impacts.extend(format_evidence_chains(topology, chains))
817
+
818
+ gate = "BLOCK" if impacts else "PASS"
819
+ report = {
820
+ "engine": "BlastTrace Native Core",
821
+ "mode": "pre-commit" if is_pre_commit else "diff",
822
+ "gate_status": gate,
823
+ "changed_files": changed_files,
824
+ "downstream_evidence_chains": list(dict.fromkeys(impacts)),
825
+ }
826
+ print(json.dumps(report, indent=2))
827
+ return 1 if (gate == "BLOCK" and is_pre_commit) else 0
828
+
829
+
830
+ def main() -> None:
831
+ global IS_PRO_TIER
832
+ if len(sys.argv) > 1:
833
+ cmd = sys.argv[1].lower()
834
+ if cmd == "activate":
835
+ if len(sys.argv) < 3:
836
+ print("Usage: blasttrace activate <LICENSE_KEY>")
837
+ sys.exit(1)
838
+ key = sys.argv[2]
839
+ success = activate_license(key)
840
+ sys.exit(0 if success else 1)
841
+ elif cmd == "deactivate":
842
+ success = deactivate_license()
843
+ sys.exit(0 if success else 1)
844
+ elif cmd == "status":
845
+ status_info = get_license_status()
846
+ print(json.dumps(status_info, indent=2))
847
+ sys.exit(0)
848
+ elif cmd in ("gen-test-key", "create-test-key"):
849
+ label = sys.argv[2] if len(sys.argv) > 2 else "TESTER"
850
+ key = generate_tester_license_key(label)
851
+ print(f"[+] Generated Authorized Tester Key ({label.upper()}):")
852
+ print(key)
853
+ print(f"\nShare with your tester to run:")
854
+ print(f"blasttrace activate {key}")
855
+ sys.exit(0)
856
+ elif cmd in ("install-agent", "setup-agent", "configure-agent"):
857
+ from blasttrace.install_agent import install_all_agents, format_universal_mcp_snippet
858
+ target = sys.argv[2] if len(sys.argv) > 2 else "all"
859
+ print("━" * 60)
860
+ print(" BlastTrace Universal AI Agent MCP Configurator")
861
+ print("━" * 60)
862
+ results = install_all_agents(target=target)
863
+ if results:
864
+ print(f"[+] Detected & configured {len(results)} AI environment(s):")
865
+ for name, ok in results:
866
+ mark = "✔" if ok else "✘"
867
+ print(f" [{mark}] {name}")
868
+ print("\n[+] Success: Restart your IDE or AI agent to activate BlastTrace!")
869
+ else:
870
+ print("[!] No active AI environment configs automatically detected in default paths.")
871
+ print("\nUniversal MCP Configuration Snippet (for any agent/tool):")
872
+ print(format_universal_mcp_snippet())
873
+ sys.exit(0)
874
+ elif cmd == "check":
875
+ if len(sys.argv) < 3:
876
+ print("Usage: blasttrace check <file_or_symbol> [repo_path] [--json]")
877
+ sys.exit(1)
878
+ raw_args = [a for a in sys.argv[2:] if a != "--json"]
879
+ is_json = "--json" in sys.argv
880
+ target = raw_args[0]
881
+ repo = raw_args[1] if len(raw_args) > 1 else os.getcwd()
882
+ raw_res = analyze_symbol_reachability(repo, target)
883
+ if is_json:
884
+ print(raw_res)
885
+ else:
886
+ try:
887
+ res_dict = json.loads(raw_res)
888
+ print(_format_terminal_reachability(res_dict))
889
+ except Exception:
890
+ print(raw_res)
891
+ sys.exit(0)
892
+ elif cmd == "diff":
893
+ repo = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
894
+ sys.exit(_run_git_diff_gate(repo, is_pre_commit=False))
895
+ elif cmd == "pre-commit":
896
+ if len(sys.argv) > 2 and sys.argv[2] == "--install":
897
+ repo = sys.argv[3] if len(sys.argv) > 3 else os.getcwd()
898
+ hook_dir = os.path.join(repo, ".git", "hooks")
899
+ os.makedirs(hook_dir, exist_ok=True)
900
+ hook_path = os.path.join(hook_dir, "pre-commit")
901
+ with open(hook_path, "w") as f:
902
+ f.write("#!/bin/sh\n# BlastTrace Pre-Commit Breaking-Change Gate\nblasttrace pre-commit\n")
903
+ try:
904
+ os.chmod(hook_path, 0o755)
905
+ except Exception:
906
+ pass
907
+ print(f"[+] Success: Hook installed successfully at {hook_path}")
908
+ sys.exit(0)
909
+ repo = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
910
+ sys.exit(_run_git_diff_gate(repo, is_pre_commit=True))
911
+ elif cmd in ("-h", "--help", "help"):
912
+ print("BlastTrace CLI - Pre-Edit Full-Stack Breaking-Change & Contract Blast-Radius Engine")
913
+ print("Commands:")
914
+ print(" blasttrace check <symbol> [repo] [--json] Check full-stack blast radius (summary or raw JSON)")
915
+ print(" blasttrace install-agent [agent] Auto-configure in Cursor, Claude, Windsurf, VS Code, Zed")
916
+ print(" blasttrace diff [repo] Analyze blast radius of uncommitted git changes")
917
+ print(" blasttrace pre-commit [repo] Run pre-commit breaking-change gate on staged changes")
918
+ print(" blasttrace activate <KEY> Activate Pro license on this machine")
919
+ print(" blasttrace deactivate Deactivate Pro license and release hardware seat")
920
+ print(" blasttrace status Show current license and machine status")
921
+ print(" blasttrace Start FastMCP stdio server")
922
+ sys.exit(0)
923
+
924
+ IS_PRO_TIER = verify_license()
925
+ if IS_PRO_TIER:
926
+ print("[*] BlastTrace running in PRO Tier (BlastTrace Native Core Unlocked)", file=sys.stderr)
927
+ else:
928
+ print("[*] BlastTrace running in COMMUNITY Tier (Free Edition)", file=sys.stderr)
929
+
930
+ mcp.run()
931
+
932
+
933
+ if __name__ == "__main__":
934
+ main()
935
+