contextl 1.2.5 → 1.2.7

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/README.md CHANGED
@@ -84,7 +84,7 @@ Finds unused files and dead code by analyzing the dependency graph for files wit
84
84
 
85
85
  Takes the exact dependency graph built by the intelligence engine and physically writes it to disk as a directory of interconnected Markdown files. Automatically injects file metadata, JSDoc/Docstring explanations, and uses standard `[[wikilinks]]` to map out dependencies. Open the generated folder as an Obsidian vault for a stunning 3D interactive graph of your architecture!
86
86
 
87
- ---
87
+
88
88
 
89
89
  ## How the ranking works
90
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextl",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "contextl — finds the most relevant files in your codebase for any change request. MCP server for AI coding agents.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -60,6 +60,7 @@ class RankedFile:
60
60
  )
61
61
 
62
62
 
63
+
63
64
  def _tokenize(text: str) -> list[str]:
64
65
  """Lowercase, split on non-alphanumeric, remove stop words."""
65
66
  tokens = re.findall(r"[a-z0-9]+", text.lower())
@@ -106,25 +107,64 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
106
107
  return score, matched
107
108
 
108
109
 
109
- def _content_score(query_terms: list[str], file_content: str, idf_weights: dict[str, float]) -> tuple[float, list[str]]:
110
+ def _content_score(query_terms: list[str], file_path: str, file_content: str, idf_weights: dict[str, float]) -> tuple[float, list[str]]:
110
111
  """
111
112
  Score how often query terms appear in the file's source code.
112
113
  Uses TF-IDF weighting, capped to avoid huge files dominating.
114
+ Includes language-specific heuristics for Java and other OOP languages.
113
115
  """
114
- content_tokens = set(re.findall(r"[a-z0-9]+", file_content))
116
+ is_oop = file_path.endswith(".java") or file_path.endswith(".ts") or file_path.endswith(".tsx")
117
+ content_tokens = set(re.findall(r"[a-z0-9]+", file_content.lower()))
118
+
119
+ # 1. CamelCase Splitting
120
+ if is_oop:
121
+ camel_tokens = []
122
+ for word in re.findall(r"[a-zA-Z]+", file_content):
123
+ if any(c.isupper() for c in word):
124
+ sub = re.sub(r"([A-Z])", r" \1", word).lower()
125
+ camel_tokens.extend(re.findall(r"[a-z0-9]+", sub))
126
+ content_tokens.update([t for t in camel_tokens if len(t) > 1])
127
+
128
+ # 2. Extract Class Declarations & Annotations
129
+ declared_classes = set()
130
+ if is_oop or file_path.endswith(".py"):
131
+ decls = re.findall(r"\b(?:class|interface|enum)\s+([a-zA-Z0-9_]+)", file_content)
132
+ for d in decls:
133
+ declared_classes.add(d.lower())
134
+
135
+ annotations = set()
136
+ if is_oop or file_path.endswith(".py"):
137
+ anns = re.findall(r"@([a-zA-Z0-9_]+)", file_content)
138
+ for a in anns:
139
+ annotations.add(a.lower())
140
+
115
141
  matched = [t for t in query_terms if t in content_tokens]
116
142
 
117
143
  if not query_terms:
118
144
  return 0.0, []
119
145
 
120
- score = sum(idf_weights.get(t, 1.0) for t in matched)
146
+ score = 0.0
147
+ for t in matched:
148
+ base_weight = idf_weights.get(t, 1.0)
149
+ multiplier = 1.0
150
+
151
+ # 3. Apply Multipliers
152
+ if t in declared_classes:
153
+ multiplier = 10.0
154
+ elif t in annotations:
155
+ multiplier = 5.0
156
+
157
+ score += (base_weight * multiplier)
158
+
121
159
  max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
122
160
  if max_score > 0:
123
- score = score / max_score
161
+ base_normalized = min(1.0, sum(idf_weights.get(t, 1.0) for t in matched) / max_score)
162
+ bonus = score - sum(idf_weights.get(t, 1.0) for t in matched)
163
+ final_score = base_normalized + bonus
124
164
  else:
125
- score = 0.0
165
+ final_score = 0.0
126
166
 
127
- return score, matched
167
+ return final_score, matched
128
168
 
129
169
 
130
170
  def _apply_neighbor_bonus(
@@ -179,6 +219,7 @@ def query(
179
219
  return []
180
220
 
181
221
  # --- Pre-computation: Global TF-IDF ---
222
+ import pathlib
182
223
  total_files = len(repo_graph.nodes)
183
224
  document_frequency = {term: 0 for term in query_terms}
184
225
 
@@ -186,9 +227,18 @@ def query(
186
227
  for path, node in repo_graph.nodes.items():
187
228
  abs_path = node.path if hasattr(node, "absolute_path") else str(__import__("pathlib").Path(repo_graph.root) / path)
188
229
  try:
189
- content = open(abs_path, encoding="utf-8").read().lower()
230
+ content = open(abs_path, encoding="utf-8").read()
190
231
  file_contents[path] = content
191
- content_tokens = set(re.findall(r"[a-z0-9]+", content))
232
+ content_tokens = set(re.findall(r"[a-z0-9]+", content.lower()))
233
+
234
+ if path.endswith(".java") or path.endswith(".ts") or path.endswith(".tsx"):
235
+ camel_tokens = []
236
+ for word in re.findall(r"[a-zA-Z]+", content):
237
+ if any(c.isupper() for c in word):
238
+ sub = re.sub(r"([A-Z])", r" \1", word).lower()
239
+ camel_tokens.extend(re.findall(r"[a-z0-9]+", sub))
240
+ content_tokens.update([t for t in camel_tokens if len(t) > 1])
241
+
192
242
  path_tokens = set(_path_tokens(path))
193
243
  combined_tokens = content_tokens.union(path_tokens)
194
244
  for term in query_terms:
@@ -209,7 +259,7 @@ def query(
209
259
 
210
260
  for path, node in repo_graph.nodes.items():
211
261
  kscore, kterms = _keyword_score(query_terms, path, idf_weights)
212
- cscore, cterms = _content_score(query_terms, file_contents.get(path, ""), idf_weights)
262
+ cscore, cterms = _content_score(query_terms, path, file_contents.get(path, ""), idf_weights)
213
263
 
214
264
  keyword_scores[path] = kscore
215
265
  content_scores[path] = cscore