contextl 1.2.6 → 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,44 +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
- ---
88
-
89
- ## AI System Prompt (Query Optimizer)
90
-
91
- ContextL is a deterministic keyword and graph engine, not a semantic AI. For maximum accuracy, inject this system prompt into your AI agent's rules (e.g., `.cursorrules` or custom instructions). It forces the AI to translate your natural language questions into highly optimized, lexical keyword strings before hitting the `query_repo` tool.
92
-
93
- ```text
94
- System Instruction: You are a translation layer designed to convert human requests into highly optimized queries for the contextl codebase search engine. The contextl engine is NOT a semantic AI; it is an advanced, dependency-aware fuzzy keyword searcher. It relies on exact token matching and import-graph ranking.
95
-
96
- Your goal is to extract the core technical keywords from the user's request and discard all conversational filler or vague concepts.
97
-
98
- Rules for generating the query string:
99
-
100
- 1. Remove Conversational Filler: Drop words like "where is", "how does", "find the", "show me".
101
- 2. Translate Concepts to Code: If the user asks for a concept, translate it into the exact keywords, classes, or library syntax a developer would type.
102
- Example: "routing" ➡️ createRouter Route routeTree
103
- Example: "entry point" ➡️ main SpringBootApplication bootstrap web.xml init
104
- Example: "database schema" ➡️ Entity Column Table model schema
105
- 3. Include Known Framework Terms: If you know the language or framework of the codebase, append its standard terminology.
106
- 4. Target File Types/Structures: If looking for configurations, use terms like config properties yaml xml json.
107
- 5. Keep it space-separated: Output a single string of space-separated keywords without punctuation.
108
87
 
109
- Examples:
110
-
111
- User: "Where are the main react components and routing defined?"
112
- Optimized Query: createRouter Route routeTree components layout
113
-
114
- User: "Find the application entry point and main method."
115
- Optimized Query: public static void main bootstrap init ApplicationContext configuration
116
-
117
- User: "How is the user authentication handled?"
118
- Optimized Query: login auth authenticate jwt token session passport
119
-
120
- Input: [Insert human request here]
121
- Output: [Return ONLY the optimized keyword string]
122
- ```
123
-
124
- ---
125
88
 
126
89
  ## How the ranking works
127
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextl",
3
- "version": "1.2.6",
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,37 +60,6 @@ class RankedFile:
60
60
  )
61
61
 
62
62
 
63
- @dataclass
64
- class FileFeatures:
65
- tokens: set[str]
66
- annotations: set[str]
67
- declarations: set[str]
68
-
69
-
70
- def _extract_file_features(content: str, extension: str) -> FileFeatures:
71
- """Extract structural heuristics and lowercase tokens from a file."""
72
- # 1. Declarations: class X, interface Y, enum Z
73
- decl_pattern = r"\b(?:class|interface|enum)\s+([A-Za-z0-9_]+)"
74
- declarations = {m.group(1).lower() for m in re.finditer(decl_pattern, content)}
75
-
76
- # 2. Annotations: @Controller, @Injectable
77
- annotations = {m.group(1).lower() for m in re.finditer(r"@([A-Za-z0-9_]+)", content)}
78
-
79
- # 3. Base tokens
80
- tokens = set(re.findall(r"[a-z0-9]+", content.lower()))
81
-
82
- # 4. CamelCase / PascalCase Splitting for structural languages
83
- if extension in {".java", ".ts", ".tsx"}:
84
- for word in re.findall(r"[A-Za-z]+", content):
85
- if not word.islower() and not word.isupper() and len(word) > 2:
86
- # Split CamelCase
87
- sub = re.sub(r"([a-z])([A-Z])", r"\1 \2", word).lower()
88
- for part in re.findall(r"[a-z0-9]+", sub):
89
- if len(part) > 1:
90
- tokens.add(part)
91
-
92
- return FileFeatures(tokens=tokens, annotations=annotations, declarations=declarations)
93
-
94
63
 
95
64
  def _tokenize(text: str) -> list[str]:
96
65
  """Lowercase, split on non-alphanumeric, remove stop words."""
@@ -138,33 +107,64 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
138
107
  return score, matched
139
108
 
140
109
 
141
- def _content_score(query_terms: list[str], features: "FileFeatures", 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]]:
142
111
  """
143
112
  Score how often query terms appear in the file's source code.
144
- Applies massive multipliers for architectural declarations and annotations.
113
+ Uses TF-IDF weighting, capped to avoid huge files dominating.
114
+ Includes language-specific heuristics for Java and other OOP languages.
145
115
  """
146
- if not features or not query_terms:
147
- return 0.0, []
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())
148
140
 
149
- matched = [t for t in query_terms if t in features.tokens]
141
+ matched = [t for t in query_terms if t in content_tokens]
142
+
143
+ if not query_terms:
144
+ return 0.0, []
150
145
 
151
146
  score = 0.0
152
147
  for t in matched:
153
148
  base_weight = idf_weights.get(t, 1.0)
154
149
  multiplier = 1.0
155
- if t in features.declarations:
150
+
151
+ # 3. Apply Multipliers
152
+ if t in declared_classes:
156
153
  multiplier = 10.0
157
- elif t in features.annotations:
154
+ elif t in annotations:
158
155
  multiplier = 5.0
159
- score += base_weight * multiplier
156
+
157
+ score += (base_weight * multiplier)
160
158
 
161
159
  max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
162
160
  if max_score > 0:
163
- 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
164
164
  else:
165
- score = 0.0
165
+ final_score = 0.0
166
166
 
167
- return score, matched
167
+ return final_score, matched
168
168
 
169
169
 
170
170
  def _apply_neighbor_bonus(
@@ -223,23 +223,29 @@ def query(
223
223
  total_files = len(repo_graph.nodes)
224
224
  document_frequency = {term: 0 for term in query_terms}
225
225
 
226
- file_features_cache = {}
226
+ file_contents = {}
227
227
  for path, node in repo_graph.nodes.items():
228
228
  abs_path = node.path if hasattr(node, "absolute_path") else str(__import__("pathlib").Path(repo_graph.root) / path)
229
- extension = pathlib.Path(path).suffix.lower()
230
-
231
229
  try:
232
230
  content = open(abs_path, encoding="utf-8").read()
233
- features = _extract_file_features(content, extension)
234
- file_features_cache[path] = features
231
+ file_contents[path] = content
232
+ content_tokens = set(re.findall(r"[a-z0-9]+", content.lower()))
235
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
+
236
242
  path_tokens = set(_path_tokens(path))
237
- combined_tokens = features.tokens.union(path_tokens)
243
+ combined_tokens = content_tokens.union(path_tokens)
238
244
  for term in query_terms:
239
245
  if term in combined_tokens:
240
246
  document_frequency[term] += 1
241
247
  except Exception:
242
- file_features_cache[path] = None
248
+ file_contents[path] = ""
243
249
 
244
250
  idf_weights = {}
245
251
  for term in query_terms:
@@ -253,7 +259,7 @@ def query(
253
259
 
254
260
  for path, node in repo_graph.nodes.items():
255
261
  kscore, kterms = _keyword_score(query_terms, path, idf_weights)
256
- cscore, cterms = _content_score(query_terms, file_features_cache.get(path), idf_weights)
262
+ cscore, cterms = _content_score(query_terms, path, file_contents.get(path, ""), idf_weights)
257
263
 
258
264
  keyword_scores[path] = kscore
259
265
  content_scores[path] = cscore