blasttrace 1.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.
Potentially problematic release.
This version of blasttrace might be problematic. Click here for more details.
- blasttrace/__init__.py +7 -0
- blasttrace/classifier.py +415 -0
- blasttrace/license.py +293 -0
- blasttrace/parsers/__init__.py +4 -0
- blasttrace/parsers/api_route_parser.py +318 -0
- blasttrace/parsers/contract_linker.py +490 -0
- blasttrace/parsers/python_ast_parser.py +340 -0
- blasttrace/parsers/sql_schema_parser.py +376 -0
- blasttrace/parsers/test_mapper.py +182 -0
- blasttrace/parsers/ts_react_parser.py +299 -0
- blasttrace/remediation.py +220 -0
- blasttrace/server.py +824 -0
- blasttrace/visualizer.py +165 -0
- blasttrace-1.0.2.dist-info/METADATA +125 -0
- blasttrace-1.0.2.dist-info/RECORD +18 -0
- blasttrace-1.0.2.dist-info/WHEEL +5 -0
- blasttrace-1.0.2.dist-info/entry_points.txt +3 -0
- blasttrace-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace TypeScript & React Frontend Parser
|
|
3
|
+
Parses TypeScript/JavaScript functions, React components, interfaces/types, ES imports,
|
|
4
|
+
API consumer calls (fetch, axios, useSWR, useQuery), and response property accesses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_url_to_route_path(raw_url: str) -> str:
|
|
13
|
+
cleaned = raw_url.strip().strip("\"'`")
|
|
14
|
+
cleaned = re.sub(r"^https?://[^/]+", "", cleaned)
|
|
15
|
+
cleaned = cleaned.split("?")[0]
|
|
16
|
+
cleaned = re.sub(r"\$\{[^}]+\}", ":param", cleaned)
|
|
17
|
+
cleaned = re.sub(r"\{[^}]+\}", ":param", cleaned)
|
|
18
|
+
if not cleaned.startswith("/"):
|
|
19
|
+
cleaned = "/" + cleaned
|
|
20
|
+
if len(cleaned) > 1 and cleaned.endswith("/"):
|
|
21
|
+
cleaned = cleaned.rstrip("/")
|
|
22
|
+
return cleaned
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_ts_params(param_str: str) -> List[Dict[str, Any]]:
|
|
26
|
+
params: List[Dict[str, Any]] = []
|
|
27
|
+
raw = param_str.strip()
|
|
28
|
+
if not raw:
|
|
29
|
+
return params
|
|
30
|
+
# Handle destructured props like `{ userId, amount = 0 }: CheckoutProps`
|
|
31
|
+
if raw.startswith("{"):
|
|
32
|
+
destruct_match = re.match(r"\{([^}]*)\}(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?", raw)
|
|
33
|
+
if destruct_match:
|
|
34
|
+
inner = destruct_match.group(1)
|
|
35
|
+
prop_type = (destruct_match.group(2) or "").strip()
|
|
36
|
+
for part in inner.split(","):
|
|
37
|
+
p = part.strip()
|
|
38
|
+
if not p:
|
|
39
|
+
continue
|
|
40
|
+
has_def = "=" in p
|
|
41
|
+
p_name = p.split("=")[0].split(":")[0].strip()
|
|
42
|
+
def_val = p.split("=", 1)[1].strip() if has_def else None
|
|
43
|
+
params.append({
|
|
44
|
+
"name": p_name,
|
|
45
|
+
"annotation": prop_type,
|
|
46
|
+
"required": not has_def,
|
|
47
|
+
"default": def_val,
|
|
48
|
+
})
|
|
49
|
+
return params
|
|
50
|
+
|
|
51
|
+
for part in raw.split(","):
|
|
52
|
+
p = part.strip()
|
|
53
|
+
if not p:
|
|
54
|
+
continue
|
|
55
|
+
has_def = "=" in p
|
|
56
|
+
left = p.split("=", 1)[0].strip()
|
|
57
|
+
def_val = p.split("=", 1)[1].strip() if has_def else None
|
|
58
|
+
optional_q = "?" in left.split(":")[0]
|
|
59
|
+
if ":" in left:
|
|
60
|
+
p_name, p_ann = left.split(":", 1)
|
|
61
|
+
p_name = p_name.strip().rstrip("?")
|
|
62
|
+
p_ann = p_ann.strip()
|
|
63
|
+
else:
|
|
64
|
+
p_name = left.rstrip("?")
|
|
65
|
+
p_ann = ""
|
|
66
|
+
params.append({
|
|
67
|
+
"name": p_name,
|
|
68
|
+
"annotation": p_ann,
|
|
69
|
+
"required": not (has_def or optional_q),
|
|
70
|
+
"default": def_val,
|
|
71
|
+
})
|
|
72
|
+
return params
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_ts_react_content(content: str, rel_file: str) -> Dict[str, Any]:
|
|
76
|
+
symbols: Dict[str, Dict[str, Any]] = {}
|
|
77
|
+
interfaces: Dict[str, Dict[str, Any]] = {}
|
|
78
|
+
imports: List[Dict[str, str]] = []
|
|
79
|
+
api_calls: List[Dict[str, Any]] = []
|
|
80
|
+
property_accesses: List[str] = []
|
|
81
|
+
nodes: List[Dict[str, Any]] = []
|
|
82
|
+
edges: List[Dict[str, str]] = []
|
|
83
|
+
|
|
84
|
+
module_id = f"fe:{rel_file}"
|
|
85
|
+
nodes.append({
|
|
86
|
+
"id": module_id,
|
|
87
|
+
"type": "frontend_module",
|
|
88
|
+
"name": rel_file,
|
|
89
|
+
"file": rel_file,
|
|
90
|
+
"layer": "frontend",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# 1. ES Imports
|
|
94
|
+
import_pattern = re.compile(
|
|
95
|
+
r"import\s+(?:type\s+)?(?:\{([^}]+)\}|([A-Za-z0-9_$]+))\s+from\s+[`\"']([^`\"']+)[`\"']",
|
|
96
|
+
re.MULTILINE,
|
|
97
|
+
)
|
|
98
|
+
for match in import_pattern.finditer(content):
|
|
99
|
+
named_group = match.group(1)
|
|
100
|
+
default_imp = match.group(2)
|
|
101
|
+
src_mod = match.group(3)
|
|
102
|
+
if named_group:
|
|
103
|
+
for item in named_group.split(","):
|
|
104
|
+
sym = item.strip().split(" as ")[0].strip()
|
|
105
|
+
if sym:
|
|
106
|
+
imports.append({"module": src_mod, "symbol": sym})
|
|
107
|
+
elif default_imp:
|
|
108
|
+
imports.append({"module": src_mod, "symbol": default_imp.strip()})
|
|
109
|
+
|
|
110
|
+
# 2. Interfaces & Type Aliases
|
|
111
|
+
iface_pattern = re.compile(r"(?:export\s+)?interface\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}", re.DOTALL)
|
|
112
|
+
for match in iface_pattern.finditer(content):
|
|
113
|
+
iname = match.group(1).strip()
|
|
114
|
+
ibody = match.group(2)
|
|
115
|
+
fields: Dict[str, Dict[str, Any]] = {}
|
|
116
|
+
for line in ibody.splitlines():
|
|
117
|
+
line_clean = line.strip().rstrip(";,")
|
|
118
|
+
if not line_clean or line_clean.startswith("//"):
|
|
119
|
+
continue
|
|
120
|
+
if ":" in line_clean:
|
|
121
|
+
fname, ftype = line_clean.split(":", 1)
|
|
122
|
+
fname_clean = fname.strip().rstrip("?")
|
|
123
|
+
fields[fname_clean] = {
|
|
124
|
+
"name": fname_clean,
|
|
125
|
+
"type": ftype.strip(),
|
|
126
|
+
"required": not fname.strip().endswith("?"),
|
|
127
|
+
}
|
|
128
|
+
iface_id = f"fe:{rel_file}:{iname}"
|
|
129
|
+
interfaces[iname] = {"name": iname, "fields": fields, "file": rel_file}
|
|
130
|
+
nodes.append({
|
|
131
|
+
"id": iface_id,
|
|
132
|
+
"type": "ts_interface",
|
|
133
|
+
"name": iname,
|
|
134
|
+
"fields": list(fields.keys()),
|
|
135
|
+
"file": rel_file,
|
|
136
|
+
"layer": "frontend",
|
|
137
|
+
})
|
|
138
|
+
edges.append({"source": module_id, "target": iface_id, "relation": "defines_interface"})
|
|
139
|
+
|
|
140
|
+
# 3. Functions, Arrow Functions, and React Components
|
|
141
|
+
fn_pattern = re.compile(
|
|
142
|
+
r"(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?",
|
|
143
|
+
)
|
|
144
|
+
for match in fn_pattern.finditer(content):
|
|
145
|
+
fname = match.group(1).strip()
|
|
146
|
+
raw_params = match.group(2)
|
|
147
|
+
ret_type = (match.group(3) or "").strip()
|
|
148
|
+
params = _parse_ts_params(raw_params)
|
|
149
|
+
is_component = fname[0].isupper()
|
|
150
|
+
sym_id = f"fe:{rel_file}:{fname}"
|
|
151
|
+
symbols[fname] = {
|
|
152
|
+
"name": fname,
|
|
153
|
+
"kind": "react_component" if is_component else "ts_function",
|
|
154
|
+
"params": params,
|
|
155
|
+
"return_type": ret_type,
|
|
156
|
+
"signature_string": f"function {fname}({raw_params})" + (f": {ret_type}" if ret_type else ""),
|
|
157
|
+
"file": rel_file,
|
|
158
|
+
}
|
|
159
|
+
nodes.append({
|
|
160
|
+
"id": sym_id,
|
|
161
|
+
"type": "react_component" if is_component else "ts_function",
|
|
162
|
+
"name": fname,
|
|
163
|
+
"params": params,
|
|
164
|
+
"return_type": ret_type,
|
|
165
|
+
"file": rel_file,
|
|
166
|
+
"layer": "frontend",
|
|
167
|
+
})
|
|
168
|
+
edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
|
|
169
|
+
|
|
170
|
+
arrow_pattern = re.compile(
|
|
171
|
+
r"(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*(?::\s*[^=]+)?=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([A-Za-z0-9_<>[\]|&]+))?\s*=>",
|
|
172
|
+
)
|
|
173
|
+
for match in arrow_pattern.finditer(content):
|
|
174
|
+
fname = match.group(1).strip()
|
|
175
|
+
if fname in symbols:
|
|
176
|
+
continue
|
|
177
|
+
raw_params = match.group(2)
|
|
178
|
+
ret_type = (match.group(3) or "").strip()
|
|
179
|
+
params = _parse_ts_params(raw_params)
|
|
180
|
+
is_component = fname[0].isupper()
|
|
181
|
+
sym_id = f"fe:{rel_file}:{fname}"
|
|
182
|
+
symbols[fname] = {
|
|
183
|
+
"name": fname,
|
|
184
|
+
"kind": "react_component" if is_component else "ts_function",
|
|
185
|
+
"params": params,
|
|
186
|
+
"return_type": ret_type,
|
|
187
|
+
"signature_string": f"const {fname} = ({raw_params}) =>",
|
|
188
|
+
"file": rel_file,
|
|
189
|
+
}
|
|
190
|
+
nodes.append({
|
|
191
|
+
"id": sym_id,
|
|
192
|
+
"type": "react_component" if is_component else "ts_function",
|
|
193
|
+
"name": fname,
|
|
194
|
+
"params": params,
|
|
195
|
+
"return_type": ret_type,
|
|
196
|
+
"file": rel_file,
|
|
197
|
+
"layer": "frontend",
|
|
198
|
+
})
|
|
199
|
+
edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
|
|
200
|
+
|
|
201
|
+
# 4. Property accesses on response/state objects (e.g. data.balance, user.balance, profile.email)
|
|
202
|
+
prop_pattern = re.compile(r"\b(?:data|user|res|response|payload|item|account|profile)\.([a-zA-Z_][a-zA-Z0-9_]*)\b")
|
|
203
|
+
for p_match in prop_pattern.finditer(content):
|
|
204
|
+
prop_name = p_match.group(1)
|
|
205
|
+
if prop_name not in ("json", "status", "ok", "headers", "then", "catch", "map", "filter", "length"):
|
|
206
|
+
if prop_name not in property_accesses:
|
|
207
|
+
property_accesses.append(prop_name)
|
|
208
|
+
|
|
209
|
+
# 5. API calls: fetch('/api/...'), axios.get('/api/...'), useSWR('/api/...')
|
|
210
|
+
fetch_pattern = re.compile(
|
|
211
|
+
r"\bfetch\s*\(\s*[`\"']([^`\"']+)[`\"'](?:\s*,\s*\{([^}]*)\})?",
|
|
212
|
+
re.DOTALL,
|
|
213
|
+
)
|
|
214
|
+
for match in fetch_pattern.finditer(content):
|
|
215
|
+
raw_url = match.group(1)
|
|
216
|
+
opts = match.group(2) or ""
|
|
217
|
+
m_match = re.search(r"method\s*:\s*[`\"']([A-Za-z]+)[`\"']", opts, re.IGNORECASE)
|
|
218
|
+
method = m_match.group(1).upper() if m_match else "GET"
|
|
219
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
220
|
+
api_calls.append({
|
|
221
|
+
"client": "fetch",
|
|
222
|
+
"method": method,
|
|
223
|
+
"raw_url": raw_url,
|
|
224
|
+
"normalized_path": norm_path,
|
|
225
|
+
"accessed_fields": property_accesses,
|
|
226
|
+
"file": rel_file,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
axios_pattern = re.compile(
|
|
230
|
+
r"\baxios\.(get|post|put|delete|patch)\s*(?:<[^>]+>)?\s*\(\s*[`\"']([^`\"']+)[`\"']",
|
|
231
|
+
re.IGNORECASE,
|
|
232
|
+
)
|
|
233
|
+
for match in axios_pattern.finditer(content):
|
|
234
|
+
method = match.group(1).upper()
|
|
235
|
+
raw_url = match.group(2)
|
|
236
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
237
|
+
api_calls.append({
|
|
238
|
+
"client": "axios",
|
|
239
|
+
"method": method,
|
|
240
|
+
"raw_url": raw_url,
|
|
241
|
+
"normalized_path": norm_path,
|
|
242
|
+
"accessed_fields": property_accesses,
|
|
243
|
+
"file": rel_file,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
swr_pattern = re.compile(r"\b(?:useSWR|useQuery)\s*(?:<[^>]+>)?\s*\(\s*[`\"'](/[^`\"']+)[`\"']")
|
|
247
|
+
for match in swr_pattern.finditer(content):
|
|
248
|
+
raw_url = match.group(1)
|
|
249
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
250
|
+
api_calls.append({
|
|
251
|
+
"client": "hook",
|
|
252
|
+
"method": "GET",
|
|
253
|
+
"raw_url": raw_url,
|
|
254
|
+
"normalized_path": norm_path,
|
|
255
|
+
"accessed_fields": property_accesses,
|
|
256
|
+
"file": rel_file,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"file": rel_file,
|
|
261
|
+
"symbols": symbols,
|
|
262
|
+
"interfaces": interfaces,
|
|
263
|
+
"imports": imports,
|
|
264
|
+
"api_calls": api_calls,
|
|
265
|
+
"property_accesses": property_accesses,
|
|
266
|
+
"nodes": nodes,
|
|
267
|
+
"edges": edges,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def parse_ts_react_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
|
|
272
|
+
if not os.path.exists(file_path):
|
|
273
|
+
return {
|
|
274
|
+
"error": f"Frontend file not found: {file_path}",
|
|
275
|
+
"symbols": {},
|
|
276
|
+
"interfaces": {},
|
|
277
|
+
"imports": [],
|
|
278
|
+
"api_calls": [],
|
|
279
|
+
"property_accesses": [],
|
|
280
|
+
"nodes": [],
|
|
281
|
+
"edges": [],
|
|
282
|
+
}
|
|
283
|
+
rel_file = os.path.relpath(file_path, repo_root).replace("\\", "/") if repo_root else os.path.basename(file_path)
|
|
284
|
+
try:
|
|
285
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
286
|
+
content = f.read()
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
return {
|
|
289
|
+
"error": str(exc),
|
|
290
|
+
"symbols": {},
|
|
291
|
+
"interfaces": {},
|
|
292
|
+
"imports": [],
|
|
293
|
+
"api_calls": [],
|
|
294
|
+
"property_accesses": [],
|
|
295
|
+
"nodes": [],
|
|
296
|
+
"edges": [],
|
|
297
|
+
}
|
|
298
|
+
return parse_ts_react_content(content, rel_file)
|
|
299
|
+
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Surgical Multi-File Sync Plan & Patch Generator
|
|
3
|
+
Produces ordered multi-file synchronization checklists and copy-paste code patches
|
|
4
|
+
across Database, Backend, API, Frontend, and Tests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_LAYER_ORDER = {
|
|
11
|
+
"database": 1,
|
|
12
|
+
"backend": 2,
|
|
13
|
+
"api": 3,
|
|
14
|
+
"frontend": 4,
|
|
15
|
+
"tests": 5,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_surgical_sync_plan(
|
|
20
|
+
topology: Dict[str, Any],
|
|
21
|
+
classification_result: Dict[str, Any],
|
|
22
|
+
downstream_chains: List[List[str]],
|
|
23
|
+
) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Generates an ordered checklist of files/functions to synchronize and copy-paste
|
|
26
|
+
remediation patches when a breaking change is detected.
|
|
27
|
+
"""
|
|
28
|
+
gate_status = classification_result.get("gate_status", "PASS")
|
|
29
|
+
classification = classification_result.get("classification", "SAFE_INTERNAL_REFACTOR")
|
|
30
|
+
target_symbol = classification_result.get("target_symbol", "")
|
|
31
|
+
target_file = classification_result.get("target_file", "")
|
|
32
|
+
proposed_edit = classification_result.get("proposed_edit", "")
|
|
33
|
+
|
|
34
|
+
nodes_by_id = topology.get("nodes_by_id", {})
|
|
35
|
+
impacted_nodes: List[Dict[str, Any]] = []
|
|
36
|
+
seen_ids = set()
|
|
37
|
+
|
|
38
|
+
for chain in downstream_chains:
|
|
39
|
+
for idx, nid in enumerate(chain):
|
|
40
|
+
if idx == 0:
|
|
41
|
+
continue
|
|
42
|
+
if nid not in seen_ids:
|
|
43
|
+
seen_ids.add(nid)
|
|
44
|
+
nmeta = nodes_by_id.get(nid, {"id": nid, "name": nid, "layer": "backend", "file": ""})
|
|
45
|
+
impacted_nodes.append(nmeta)
|
|
46
|
+
|
|
47
|
+
impacted_nodes.sort(key=lambda n: (_LAYER_ORDER.get(n.get("layer", "backend"), 9), n.get("file", "")))
|
|
48
|
+
|
|
49
|
+
checklist: List[Dict[str, Any]] = []
|
|
50
|
+
patches: List[Dict[str, str]] = []
|
|
51
|
+
|
|
52
|
+
# 1. Origin backward-compatibility patch
|
|
53
|
+
if classification == "BREAKING_FUNCTION_SIGNATURE":
|
|
54
|
+
patches.append({
|
|
55
|
+
"layer": "backend",
|
|
56
|
+
"file": target_file or "backend_module.py",
|
|
57
|
+
"symbol": target_symbol,
|
|
58
|
+
"strategy": "Provide backward-compatible default parameter value so existing callers do not break",
|
|
59
|
+
"patch": (
|
|
60
|
+
f"# SURGICAL SAFE-COMPATIBILITY PATCH ({target_file})\n"
|
|
61
|
+
f"# Keep newly added parameters optional with a default value during rollout:\n"
|
|
62
|
+
f"{_suggest_safe_signature(target_symbol, proposed_edit)}"
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
elif classification == "BREAKING_DATABASE_SCHEMA":
|
|
66
|
+
col_short = target_symbol.split(".")[-1]
|
|
67
|
+
patches.append({
|
|
68
|
+
"layer": "database",
|
|
69
|
+
"file": target_file or "schema.sql",
|
|
70
|
+
"symbol": target_symbol,
|
|
71
|
+
"strategy": "Use Expand-and-Contract two-phase migration instead of immediate column drop/rename",
|
|
72
|
+
"patch": (
|
|
73
|
+
f"-- PHASE 1 (EXPAND): Add new column with DEFAULT and backfill before dropping '{col_short}'\n"
|
|
74
|
+
f"ALTER TABLE {target_symbol.split('.')[0] if '.' in target_symbol else 'target_table'} "
|
|
75
|
+
f"ADD COLUMN IF NOT EXISTS {col_short}_v2 NUMERIC(12, 2) DEFAULT 0.00;\n"
|
|
76
|
+
f"-- Keep '{col_short}' readable until all ORM, API, and Frontend consumers are synced."
|
|
77
|
+
),
|
|
78
|
+
})
|
|
79
|
+
elif classification == "BREAKING_API_CONTRACT":
|
|
80
|
+
patches.append({
|
|
81
|
+
"layer": "api",
|
|
82
|
+
"file": target_file or "api/routes.py",
|
|
83
|
+
"symbol": target_symbol,
|
|
84
|
+
"strategy": "Preserve legacy response field alias alongside the new contract field",
|
|
85
|
+
"patch": (
|
|
86
|
+
f"# SURGICAL API DUAL-EMIT PATCH ({target_file})\n"
|
|
87
|
+
f"# Emit both legacy and updated keys in the JSON payload until frontend deployment completes:\n"
|
|
88
|
+
f"return {{\n"
|
|
89
|
+
f' "status": "ok",\n'
|
|
90
|
+
f' # Retain legacy contract key for active frontend consumers\n'
|
|
91
|
+
f"}}"
|
|
92
|
+
),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# 2. Ordered downstream multi-file sync checklist & per-layer patches
|
|
96
|
+
for step_idx, nmeta in enumerate(impacted_nodes, start=1):
|
|
97
|
+
layer = nmeta.get("layer", "backend")
|
|
98
|
+
rel_file = nmeta.get("file", "")
|
|
99
|
+
name = nmeta.get("name", nmeta.get("id", ""))
|
|
100
|
+
ntype = nmeta.get("type", "")
|
|
101
|
+
|
|
102
|
+
action_desc = _describe_sync_action(layer, ntype, name, target_symbol, classification)
|
|
103
|
+
checklist.append({
|
|
104
|
+
"step": step_idx,
|
|
105
|
+
"layer": layer,
|
|
106
|
+
"file": rel_file,
|
|
107
|
+
"symbol": name,
|
|
108
|
+
"node_type": ntype,
|
|
109
|
+
"required_action": action_desc,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if layer == "frontend" and rel_file:
|
|
113
|
+
prop_name = nmeta.get("property") or target_symbol.split(".")[-1]
|
|
114
|
+
patches.append({
|
|
115
|
+
"layer": "frontend",
|
|
116
|
+
"file": rel_file,
|
|
117
|
+
"symbol": name,
|
|
118
|
+
"strategy": f"Add defensive fallback on response property '{prop_name}' in {rel_file}",
|
|
119
|
+
"patch": (
|
|
120
|
+
f"// SURGICAL FRONTEND CONSUMER PATCH ({rel_file})\n"
|
|
121
|
+
f"const safe_{prop_name} = data?.{prop_name} ?? data?.{prop_name}_v2 ?? 0;"
|
|
122
|
+
),
|
|
123
|
+
})
|
|
124
|
+
elif layer == "backend" and ntype in ("orm_field", "class_field") and rel_file:
|
|
125
|
+
fld = nmeta.get("field") or target_symbol.split(".")[-1]
|
|
126
|
+
patches.append({
|
|
127
|
+
"layer": "backend",
|
|
128
|
+
"file": rel_file,
|
|
129
|
+
"symbol": name,
|
|
130
|
+
"strategy": f"Synchronize ORM model attribute '{name}' with updated schema definition",
|
|
131
|
+
"patch": (
|
|
132
|
+
f"# SURGICAL ORM SYNC PATCH ({rel_file})\n"
|
|
133
|
+
f"# Ensure '{fld}' maps cleanly or provides a @property compatibility shim:\n"
|
|
134
|
+
f"@property\n"
|
|
135
|
+
f"def {fld}_compat(self):\n"
|
|
136
|
+
f" return getattr(self, '{fld}', None)"
|
|
137
|
+
),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
impacted_test_files = sorted({
|
|
141
|
+
n.get("file", "")
|
|
142
|
+
for n in impacted_nodes
|
|
143
|
+
if n.get("layer") == "tests" and n.get("file")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"gate_status": gate_status,
|
|
148
|
+
"classification": classification,
|
|
149
|
+
"total_impacted_nodes": len(impacted_nodes),
|
|
150
|
+
"ordered_sync_checklist": checklist,
|
|
151
|
+
"surgical_patches": patches,
|
|
152
|
+
"recommended_test_command": (
|
|
153
|
+
f"pytest {' '.join(impacted_test_files)}"
|
|
154
|
+
if impacted_test_files
|
|
155
|
+
else "pytest"
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _suggest_safe_signature(symbol_name: str, proposed_edit: str) -> str:
|
|
161
|
+
clean = proposed_edit.strip()
|
|
162
|
+
if clean.startswith(("def ", "async def ")) and ")" in clean:
|
|
163
|
+
head, tail = clean.rsplit(")", 1)
|
|
164
|
+
if "=" not in head.split(",")[-1]:
|
|
165
|
+
return f"{head} = None){tail}"
|
|
166
|
+
return clean
|
|
167
|
+
return f"def {symbol_name}(*args, **kwargs): ..."
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _describe_sync_action(layer: str, ntype: str, name: str, target_symbol: str, classification: str) -> str:
|
|
171
|
+
if layer == "database":
|
|
172
|
+
return f"Update database column/constraint reference '{name}' to match '{target_symbol}'."
|
|
173
|
+
if layer == "backend":
|
|
174
|
+
if ntype in ("orm_model", "orm_field", "class_field"):
|
|
175
|
+
return f"Sync ORM/model field '{name}' with schema change on '{target_symbol}'."
|
|
176
|
+
return f"Update call site or field access inside '{name}' to match new contract of '{target_symbol}'."
|
|
177
|
+
if layer == "api":
|
|
178
|
+
return f"Update API endpoint/serializer '{name}' payload contract and OpenAPI schema."
|
|
179
|
+
if layer == "frontend":
|
|
180
|
+
return f"Update frontend UI component/consumer '{name}' response property binding and TypeScript interface."
|
|
181
|
+
if layer == "tests":
|
|
182
|
+
return f"Re-run and update assertions in covering test '{name}'."
|
|
183
|
+
return f"Synchronize '{name}' with modified contract '{target_symbol}'."
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def generate_cycle_refactoring_plan(cycles: List[List[str]]) -> Dict[str, Any]:
|
|
187
|
+
"""
|
|
188
|
+
Produces surgical refactoring recommendations to break circular import/dependency loops.
|
|
189
|
+
"""
|
|
190
|
+
if not cycles:
|
|
191
|
+
return {
|
|
192
|
+
"status": "Clean",
|
|
193
|
+
"message": "Zero circular code dependencies detected.",
|
|
194
|
+
"refactoring_patches": [],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
patches: List[Dict[str, str]] = []
|
|
198
|
+
for idx, cycle in enumerate(cycles):
|
|
199
|
+
mod_a = cycle[0]
|
|
200
|
+
mod_b = cycle[1] if len(cycle) > 1 else cycle[0]
|
|
201
|
+
patches.append({
|
|
202
|
+
"cycle_index": idx + 1,
|
|
203
|
+
"cycle_members": cycle,
|
|
204
|
+
"strategic_goal": f"Break circular dependency between '{mod_a}' and '{mod_b}'",
|
|
205
|
+
"recommended_pattern": "Extract shared types/interfaces into a dedicated contracts module or move runtime import inside function scope (TYPE_CHECKING guard).",
|
|
206
|
+
"surgical_patch": (
|
|
207
|
+
f"# SURGICAL CYCLE-BREAKER PATCH (in {mod_a})\n"
|
|
208
|
+
f"from typing import TYPE_CHECKING\n"
|
|
209
|
+
f"if TYPE_CHECKING:\n"
|
|
210
|
+
f" # Keep static type-checking without runtime circular import\n"
|
|
211
|
+
f" import {mod_b.split(':')[-1].replace('/', '.').replace('.py', '')}\n"
|
|
212
|
+
),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"status": "Circular Dependencies Detected",
|
|
217
|
+
"cycles_count": len(cycles),
|
|
218
|
+
"refactoring_patches": patches,
|
|
219
|
+
}
|
|
220
|
+
|