contextl 1.2.7 → 1.2.8
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.
- package/package.json +1 -1
- package/python/query_engine.py +115 -24
package/package.json
CHANGED
package/python/query_engine.py
CHANGED
|
@@ -32,7 +32,9 @@ STOP_WORDS = {
|
|
|
32
32
|
"where", "when", "which", "who", "make", "change", "update", "fix",
|
|
33
33
|
"add", "remove", "edit", "modify", "the", "my", "our", "i", "we",
|
|
34
34
|
"string", "int", "boolean", "class", "function", "return", "import",
|
|
35
|
-
"public", "private", "logic", "handler", "data", "value"
|
|
35
|
+
"public", "private", "logic", "handler", "data", "value", "yo", "bro",
|
|
36
|
+
"bruh", "pls", "please", "bug", "issue", "error", "help", "stuff",
|
|
37
|
+
"handling", "failing", "why", "need", "can", "could", "would", "should", "fr"
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
|
|
@@ -91,70 +93,138 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
|
|
|
91
93
|
score = 0.0
|
|
92
94
|
|
|
93
95
|
for term in query_terms:
|
|
96
|
+
# Exact match
|
|
94
97
|
if term in filename_toks:
|
|
95
|
-
score += idf_weights.get(term, 1.0) #
|
|
98
|
+
score += 2.0 * idf_weights.get(term, 1.0) # Massive signal
|
|
96
99
|
matched.append(term)
|
|
100
|
+
# Substring match on filename
|
|
101
|
+
elif len(term) > 3 and any(term in ft for ft in filename_toks):
|
|
102
|
+
score += 1.5 * idf_weights.get(term, 1.0)
|
|
103
|
+
matched.append(term)
|
|
104
|
+
# Exact match on path
|
|
97
105
|
elif term in path_toks:
|
|
98
|
-
score += 0.
|
|
106
|
+
score += 0.8 * idf_weights.get(term, 1.0) # Strong signal
|
|
107
|
+
if term not in matched:
|
|
108
|
+
matched.append(term)
|
|
109
|
+
# Substring match on path
|
|
110
|
+
elif len(term) > 3 and any(term in pt for pt in path_toks):
|
|
111
|
+
score += 0.4 * idf_weights.get(term, 1.0)
|
|
99
112
|
if term not in matched:
|
|
100
113
|
matched.append(term)
|
|
101
114
|
|
|
102
115
|
# Normalize by max possible score (sum of all IDFs)
|
|
103
116
|
max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
|
|
104
117
|
if max_score > 0:
|
|
105
|
-
score = score / max_score
|
|
118
|
+
score = min(1.0, score / max_score) # Cap at 1.0 since we have massive multipliers
|
|
106
119
|
|
|
107
120
|
return score, matched
|
|
108
121
|
|
|
109
122
|
|
|
110
|
-
def _content_score(query_terms: list[str], file_path: str, file_content: str, idf_weights: dict[str, float]) -> tuple[float, list[str]]:
|
|
123
|
+
def _content_score(query_terms: list[str], file_path: str, file_content: str, idf_weights: dict[str, float], dl: int, avgdl: float) -> tuple[float, list[str]]:
|
|
111
124
|
"""
|
|
112
125
|
Score how often query terms appear in the file's source code.
|
|
113
|
-
Uses
|
|
126
|
+
Uses BM25 Term Frequency weighting to neutralize document length bias.
|
|
114
127
|
Includes language-specific heuristics for Java and other OOP languages.
|
|
115
128
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
is_java = file_path.endswith(".java")
|
|
130
|
+
is_ts = file_path.endswith(".ts") or file_path.endswith(".tsx")
|
|
131
|
+
is_oop = is_java or is_ts
|
|
132
|
+
|
|
133
|
+
content_tokens_list = re.findall(r"[a-z0-9]+", file_content.lower())
|
|
118
134
|
|
|
119
|
-
# 1. CamelCase Splitting
|
|
120
|
-
if
|
|
135
|
+
# 1. CamelCase Splitting (Java only)
|
|
136
|
+
if is_java:
|
|
121
137
|
camel_tokens = []
|
|
122
138
|
for word in re.findall(r"[a-zA-Z]+", file_content):
|
|
123
139
|
if any(c.isupper() for c in word):
|
|
124
140
|
sub = re.sub(r"([A-Z])", r" \1", word).lower()
|
|
125
141
|
camel_tokens.extend(re.findall(r"[a-z0-9]+", sub))
|
|
126
|
-
|
|
142
|
+
content_tokens_list.extend([t for t in camel_tokens if len(t) > 1])
|
|
127
143
|
|
|
128
|
-
# 2. Extract Class Declarations &
|
|
144
|
+
# 2. Extract Class Declarations, Annotations & Method Names
|
|
129
145
|
declared_classes = set()
|
|
146
|
+
ts_exports = set()
|
|
147
|
+
java_methods = set()
|
|
130
148
|
if is_oop or file_path.endswith(".py"):
|
|
131
149
|
decls = re.findall(r"\b(?:class|interface|enum)\s+([a-zA-Z0-9_]+)", file_content)
|
|
132
150
|
for d in decls:
|
|
133
151
|
declared_classes.add(d.lower())
|
|
134
152
|
|
|
153
|
+
if is_java:
|
|
154
|
+
# Extract Java method names: public/private/protected ... methodName(
|
|
155
|
+
methods = re.findall(r"\b(?:public|private|protected)\s+(?:static\s+)?(?:[a-zA-Z0-9_<>\[\]]+)\s+([a-zA-Z0-9_]+)\s*\(", file_content)
|
|
156
|
+
for m in methods:
|
|
157
|
+
if m.lower() not in {"main", "get", "set", "is", "has"}:
|
|
158
|
+
java_methods.add(m.lower())
|
|
159
|
+
# Also add CamelCase parts of method names
|
|
160
|
+
sub = re.sub(r"([A-Z])", r" \1", m).lower()
|
|
161
|
+
for part in re.findall(r"[a-z0-9]+", sub):
|
|
162
|
+
if len(part) > 3:
|
|
163
|
+
java_methods.add(part)
|
|
164
|
+
|
|
165
|
+
if is_ts:
|
|
166
|
+
exports = re.findall(r"\bexport\s+(?:const|let|var|class|interface|function|default)\s+([a-zA-Z0-9_]+)", file_content)
|
|
167
|
+
for e in exports:
|
|
168
|
+
ts_exports.add(e.lower())
|
|
169
|
+
|
|
135
170
|
annotations = set()
|
|
136
171
|
if is_oop or file_path.endswith(".py"):
|
|
137
172
|
anns = re.findall(r"@([a-zA-Z0-9_]+)", file_content)
|
|
138
173
|
for a in anns:
|
|
139
174
|
annotations.add(a.lower())
|
|
140
175
|
|
|
141
|
-
|
|
176
|
+
term_counts = {}
|
|
177
|
+
for t in query_terms:
|
|
178
|
+
count = 0
|
|
179
|
+
for ct in content_tokens_list:
|
|
180
|
+
if t == ct:
|
|
181
|
+
count += 1
|
|
182
|
+
elif len(t) > 3 and t in ct:
|
|
183
|
+
count += 1
|
|
184
|
+
if count > 0:
|
|
185
|
+
term_counts[t] = count
|
|
186
|
+
|
|
187
|
+
matched = list(term_counts.keys())
|
|
142
188
|
|
|
143
189
|
if not query_terms:
|
|
144
190
|
return 0.0, []
|
|
145
191
|
|
|
192
|
+
# Multiplier strength scales inversely with query length.
|
|
193
|
+
# Short queries (1-2 terms, likely vibe) need a loud structural signal.
|
|
194
|
+
# Long queries (4+ terms, likely standard LLM) rely more on BM25 math.
|
|
195
|
+
n_terms = len(query_terms)
|
|
196
|
+
if n_terms <= 2:
|
|
197
|
+
class_mult = 10.0
|
|
198
|
+
ann_mult = 5.0
|
|
199
|
+
elif n_terms <= 4:
|
|
200
|
+
class_mult = 6.0
|
|
201
|
+
ann_mult = 3.0
|
|
202
|
+
else:
|
|
203
|
+
class_mult = 3.0
|
|
204
|
+
ann_mult = 2.0
|
|
205
|
+
|
|
146
206
|
score = 0.0
|
|
147
207
|
for t in matched:
|
|
148
208
|
base_weight = idf_weights.get(t, 1.0)
|
|
149
209
|
multiplier = 1.0
|
|
150
210
|
|
|
151
|
-
# 3. Apply Multipliers
|
|
152
|
-
if t in
|
|
153
|
-
multiplier =
|
|
211
|
+
# 3. Apply Multipliers — TS exports always get 8x regardless of query length
|
|
212
|
+
if t in ts_exports:
|
|
213
|
+
multiplier = 8.0
|
|
214
|
+
elif t in declared_classes:
|
|
215
|
+
multiplier = class_mult
|
|
216
|
+
elif t in java_methods:
|
|
217
|
+
multiplier = 4.0
|
|
154
218
|
elif t in annotations:
|
|
155
|
-
multiplier =
|
|
219
|
+
multiplier = ann_mult
|
|
156
220
|
|
|
157
|
-
|
|
221
|
+
# 4. BM25 Term Frequency
|
|
222
|
+
count = term_counts[t]
|
|
223
|
+
k1 = 1.5
|
|
224
|
+
b = 0.75
|
|
225
|
+
tf_bm25 = (count * (k1 + 1)) / (count + k1 * (1 - b + b * (dl / avgdl)))
|
|
226
|
+
|
|
227
|
+
score += (tf_bm25 * base_weight * multiplier)
|
|
158
228
|
|
|
159
229
|
max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
|
|
160
230
|
if max_score > 0:
|
|
@@ -170,7 +240,7 @@ def _content_score(query_terms: list[str], file_path: str, file_content: str, id
|
|
|
170
240
|
def _apply_neighbor_bonus(
|
|
171
241
|
scores: dict[str, float],
|
|
172
242
|
repo_graph: RepoGraph,
|
|
173
|
-
boost: float = 0.
|
|
243
|
+
boost: float = 0.35,
|
|
174
244
|
depth: int = 1,
|
|
175
245
|
) -> dict[str, float]:
|
|
176
246
|
"""
|
|
@@ -218,20 +288,29 @@ def query(
|
|
|
218
288
|
if not query_terms:
|
|
219
289
|
return []
|
|
220
290
|
|
|
221
|
-
# --- Pre-computation: Global TF-IDF ---
|
|
291
|
+
# --- Pre-computation: Global TF-IDF & BM25 ---
|
|
222
292
|
import pathlib
|
|
223
293
|
total_files = len(repo_graph.nodes)
|
|
224
294
|
document_frequency = {term: 0 for term in query_terms}
|
|
225
295
|
|
|
226
296
|
file_contents = {}
|
|
297
|
+
doc_lengths = {}
|
|
298
|
+
total_tokens_count = 0
|
|
299
|
+
|
|
227
300
|
for path, node in repo_graph.nodes.items():
|
|
228
301
|
abs_path = node.path if hasattr(node, "absolute_path") else str(__import__("pathlib").Path(repo_graph.root) / path)
|
|
229
302
|
try:
|
|
230
303
|
content = open(abs_path, encoding="utf-8").read()
|
|
231
304
|
file_contents[path] = content
|
|
232
|
-
content_tokens = set(re.findall(r"[a-z0-9]+", content.lower()))
|
|
233
305
|
|
|
234
|
-
|
|
306
|
+
raw_tokens = re.findall(r"[a-z0-9]+", content.lower())
|
|
307
|
+
dl = len(raw_tokens)
|
|
308
|
+
doc_lengths[path] = dl
|
|
309
|
+
total_tokens_count += dl
|
|
310
|
+
|
|
311
|
+
content_tokens = set(raw_tokens)
|
|
312
|
+
|
|
313
|
+
if path.endswith(".java"):
|
|
235
314
|
camel_tokens = []
|
|
236
315
|
for word in re.findall(r"[a-zA-Z]+", content):
|
|
237
316
|
if any(c.isupper() for c in word):
|
|
@@ -244,13 +323,24 @@ def query(
|
|
|
244
323
|
for term in query_terms:
|
|
245
324
|
if term in combined_tokens:
|
|
246
325
|
document_frequency[term] += 1
|
|
326
|
+
elif len(term) > 3:
|
|
327
|
+
for ct in combined_tokens:
|
|
328
|
+
if term in ct:
|
|
329
|
+
document_frequency[term] += 1
|
|
330
|
+
break
|
|
247
331
|
except Exception:
|
|
248
332
|
file_contents[path] = ""
|
|
333
|
+
doc_lengths[path] = 0
|
|
334
|
+
|
|
335
|
+
avgdl = total_tokens_count / total_files if total_files > 0 else 1.0
|
|
249
336
|
|
|
250
337
|
idf_weights = {}
|
|
251
338
|
for term in query_terms:
|
|
252
339
|
df = document_frequency[term]
|
|
253
|
-
|
|
340
|
+
if df == 0:
|
|
341
|
+
idf_weights[term] = 0.0
|
|
342
|
+
else:
|
|
343
|
+
idf_weights[term] = math.log(total_files / (1 + df)) if total_files > 0 else 1.0
|
|
254
344
|
|
|
255
345
|
# --- Pass 1: keyword + content scores per file ---
|
|
256
346
|
keyword_scores: dict[str, float] = {}
|
|
@@ -259,7 +349,8 @@ def query(
|
|
|
259
349
|
|
|
260
350
|
for path, node in repo_graph.nodes.items():
|
|
261
351
|
kscore, kterms = _keyword_score(query_terms, path, idf_weights)
|
|
262
|
-
|
|
352
|
+
dl = doc_lengths.get(path, 0)
|
|
353
|
+
cscore, cterms = _content_score(query_terms, path, file_contents.get(path, ""), idf_weights, dl, avgdl)
|
|
263
354
|
|
|
264
355
|
keyword_scores[path] = kscore
|
|
265
356
|
content_scores[path] = cscore
|