Repository navigation
Expand file tree
/
Copy pathFQDN_NORMALIZATION_FIX.py
More file actions
329 lines (276 loc) · 12.9 KB
/
Copy pathFQDN_NORMALIZATION_FIX.py
File metadata and controls
329 lines (276 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""
FQDN Normalization Fix - Enhanced Component ID Resolution
NOTE: This file is a standalone reference/patch proposal for
codewiki/src/be/cluster_modules.py. It is kept at the repository root
temporarily for review purposes; its logic should be integrated into
codewiki/src/be/cluster_modules.py (or this file removed) once merged.
This file contains the proposed fix for cluster_modules.py to handle:
1. LLM-added "deps." prefixes
2. Fuzzy substring matching for nested paths
3. Enhanced debugging and logging
4. Partial path matching for complex Java packages
Replace the normalization loop in cluster_modules.py:212-233 with this code.
NOTE: This is a scratch/reference module, not imported anywhere in the
codebase. It documents a proposed patch for cluster_modules.py and should be
merged into that file (or moved to docs/ or a PR description) rather than
kept as standalone code at the repository root.
"""
from typing import Dict, List
import logging
logger = logging.getLogger(__name__)
def normalize_component_ids_enhanced(
module_tree: Dict,
components: Dict,
short_to_fqdn: Dict[str, str]
) -> tuple[int, int]:
"""
Enhanced component ID normalization with multiple fallback strategies.
Args:
module_tree: Module tree with component IDs from LLM
components: Dictionary of actual components (FQDN → Node)
short_to_fqdn: Mapping of short IDs to FQDNs
Returns:
Tuple of (total_normalized, total_failed)
"""
total_normalized = 0
total_failed = 0
for module_name, module_data in module_tree.items():
original_components = module_data.get('components', [])
normalized_components = []
for comp_id in original_components:
# Strategy 1: Exact FQDN match
if comp_id in components:
normalized_components.append(comp_id)
logger.debug(f" ✅ Exact match: '{comp_id}'")
continue
# Strategy 2: Short ID mapping
if comp_id in short_to_fqdn:
fqdn = short_to_fqdn[comp_id]
normalized_components.append(fqdn)
total_normalized += 1
logger.debug(f" ✅ Mapped '{comp_id}' → '{fqdn}'")
continue
# Strategy 3: Strip "deps." prefix and retry
stripped_id = comp_id
if comp_id.startswith("deps."):
stripped_id = comp_id[5:] # Remove "deps." prefix
logger.debug(f" 🔧 Stripping 'deps.' prefix: '{comp_id}' → '{stripped_id}'")
# Try exact match with stripped ID
if stripped_id in components:
normalized_components.append(stripped_id)
total_normalized += 1
logger.info(
f" ✅ Found after stripping 'deps.' prefix:\n"
f" LLM returned: '{comp_id}'\n"
f" Actual FQDN: '{stripped_id}'"
)
continue
# Try short_id mapping with stripped ID
if stripped_id in short_to_fqdn:
fqdn = short_to_fqdn[stripped_id]
normalized_components.append(fqdn)
total_normalized += 1
logger.info(
f" ✅ Mapped after stripping prefix:\n"
f" LLM returned: '{comp_id}'\n"
f" Stripped to: '{stripped_id}'\n"
f" Mapped to: '{fqdn}'"
)
continue
# Strategy 4: Fuzzy substring matching on component name
# Extract the likely component name (last segment)
component_name = comp_id.split('.')[-1]
# Search for FQDNs ending with this component name
fuzzy_matches = [
fqdn for fqdn in components.keys()
if fqdn.split('.')[-1] == component_name
]
if len(fuzzy_matches) == 1:
# Unique match found
normalized_components.append(fuzzy_matches[0])
total_normalized += 1
logger.info(
f" ✅ Fuzzy matched by component name:\n"
f" LLM returned: '{comp_id}'\n"
f" Component: '{component_name}'\n"
f" Matched FQDN: '{fuzzy_matches[0]}'"
)
continue
elif len(fuzzy_matches) > 1:
# Multiple matches - try to find best match by path similarity
best_match = _find_best_path_match(comp_id, fuzzy_matches)
if best_match:
normalized_components.append(best_match)
total_normalized += 1
logger.info(
f" ✅ Best fuzzy match from {len(fuzzy_matches)} candidates:\n"
f" LLM returned: '{comp_id}'\n"
f" Matched FQDN: '{best_match}'"
)
continue
else:
logger.warning(
f" ⚠️ Multiple ambiguous fuzzy matches for '{comp_id}':\n"
f" Component: '{component_name}'\n"
f" Candidates:\n" +
'\n'.join(f" - {m}" for m in fuzzy_matches[:5]) +
(f"\n ... and {len(fuzzy_matches) - 5} more" if len(fuzzy_matches) > 5 else "")
)
# Strategy 5: Try matching by suffix path (last N segments)
# This handles cases where LLM includes partial path
# Example: "deps.openframe-oss-lib.src.main.java.Class"
# should match "openframe-oss-lib.different.path.java.Class"
suffix_matches: List[str] = []
if '.' in comp_id:
# Try matching last 2-4 segments
segments = comp_id.split('.')
suffix_matches = []
for n in range(2, min(5, len(segments) + 1)):
suffix = '.'.join(segments[-n:])
suffix_matches = [
fqdn for fqdn in components.keys()
if fqdn.endswith(suffix)
]
if len(suffix_matches) == 1:
normalized_components.append(suffix_matches[0])
total_normalized += 1
logger.info(
f" ✅ Matched by path suffix:\n"
f" LLM returned: '{comp_id}'\n"
f" Suffix: '{suffix}'\n"
f" Matched FQDN: '{suffix_matches[0]}'"
)
break
else:
# No suffix match worked
pass
if suffix_matches and len(suffix_matches) == 1:
continue
# All strategies failed - keep original (will fail validation)
normalized_components.append(comp_id)
total_failed += 1
# Enhanced failure logging
similar_short_ids = [
k for k in short_to_fqdn.keys()
if component_name.lower() in k.lower()
][:5]
possible_fqdns = [
fqdn for fqdn in components.keys()
if component_name.lower() in fqdn.lower()
][:5]
logger.warning(
f" ❌ Failed to normalize '{comp_id}' in module '{module_name}'\n"
f" ├─ Original ID: '{comp_id}'\n"
f" ├─ Stripped ID: '{stripped_id}'\n"
f" ├─ Component name: '{component_name}'\n"
f" ├─ Fuzzy matches: {len(fuzzy_matches)}\n"
f" ├─ Similar short IDs: {similar_short_ids if similar_short_ids else 'None'}\n"
f" ├─ Possible FQDNs: {possible_fqdns if possible_fqdns else 'None'}\n"
f" └─ Reason: Component not found in any normalization strategy\n"
f" This could indicate:\n"
f" • Component filtered during parsing\n"
f" • LLM hallucination (non-existent component)\n"
f" • FQDN format mismatch requiring new normalization strategy"
)
module_data['components'] = normalized_components
return total_normalized, total_failed
def _find_best_path_match(llm_id: str, candidates: List[str]) -> str | None:
"""
Find the best matching FQDN from candidates based on path similarity.
Args:
llm_id: Component ID returned by LLM
candidates: List of potential FQDN matches
Returns:
Best matching FQDN or None if ambiguous
"""
llm_segments = llm_id.split('.')
# Score each candidate by number of matching path segments
scores = []
for candidate in candidates:
candidate_segments = candidate.split('.')
# Count matching segments in order
matches = 0
for i, llm_seg in enumerate(llm_segments):
if llm_seg in candidate_segments:
# Bonus points if segments are in same relative position
try:
candidate_idx = candidate_segments.index(llm_seg)
position_diff = abs(i - candidate_idx)
matches += 1 + (1.0 / (1 + position_diff)) # Weight by position similarity
except ValueError:
pass
scores.append((candidate, matches))
# Sort by score descending
scores.sort(key=lambda x: x[1], reverse=True)
# Return best match if it's clearly better than alternatives
if len(scores) > 1 and scores[0][1] > scores[1][1]:
logger.debug(f" Best match score: {scores[0][1]:.2f} vs next: {scores[1][1]:.2f}")
return scores[0][0]
return None
def build_short_id_to_fqdn_map_enhanced(components: Dict) -> Dict[str, str]:
"""
Enhanced version of build_short_id_to_fqdn_map with partial path support.
Maps both:
- Short component names (e.g., "UserService")
- Partial paths (e.g., "auth.UserService", "services.auth.UserService")
This helps match LLM outputs that include partial directory paths.
"""
from collections import defaultdict
mapping = {}
collisions = defaultdict(list)
for fqdn, node in components.items():
# Strategy 1: Use node's short_id if available
short_id = node.short_id
if not short_id:
# Fallback: extract from FQDN
if '::' in fqdn:
short_id = fqdn.split('::')[-1]
else:
short_id = fqdn.split('.')[-1]
# Map short_id → FQDN
if short_id in mapping:
collisions[short_id].append(fqdn)
else:
mapping[short_id] = fqdn
# Strategy 2: Map partial paths (last 2-4 segments)
# This helps match LLM outputs like "services.auth.UserService"
# when the full FQDN is "main-repo.src.services.auth.UserService"
segments = fqdn.split('.')
if len(segments) > 2:
# Map progressively longer suffixes
for i in range(2, min(5, len(segments) + 1)): # Last 2-4 segments
partial_id = '.'.join(segments[-i:])
# Only map if unique
if partial_id not in mapping:
mapping[partial_id] = fqdn
elif mapping[partial_id] != fqdn:
# Collision detected
if partial_id not in collisions:
collisions[partial_id].append(mapping[partial_id])
collisions[partial_id].append(fqdn)
# Log collisions for debugging
if collisions:
logger.warning(f" ⚠️ Found {len(collisions)} short ID collisions:")
for short_id, fqdns in list(collisions.items())[:5]:
logger.warning(f" '{short_id}' → {fqdns[:3]}")
logger.info(f" ✅ Built short_id → FQDN mapping with {len(mapping)} entries")
logger.debug(f" ({len(collisions)} collisions detected)")
return mapping
# Example usage in cluster_modules.py:
"""
Replace lines 200-241 with:
# Build reverse mapping: short_id → FQDN (enhanced version)
logger.info(f"🔄 Normalizing component IDs in clustering response")
short_to_fqdn = build_short_id_to_fqdn_map_enhanced(components)
# Normalize all component IDs in module_tree (enhanced version)
total_normalized, total_failed = normalize_component_ids_enhanced(
module_tree,
components,
short_to_fqdn
)
if total_normalized > 0:
logger.info(f" ✅ Normalized {total_normalized} short IDs to FQDNs")
if total_failed > 0:
logger.warning(f" ⚠️ Failed to normalize {total_failed} component IDs")
logger.info("")
"""