contextl 1.2.5 → 1.2.6

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
@@ -86,6 +86,43 @@ Takes the exact dependency graph built by the intelligence engine and physically
86
86
 
87
87
  ---
88
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
+
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
+
89
126
  ## How the ranking works
90
127
 
91
128
  1. **Keyword match** — does the filename contain query terms?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextl",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
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,38 @@ 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
  def _tokenize(text: str) -> list[str]:
64
96
  """Lowercase, split on non-alphanumeric, remove stop words."""
65
97
  tokens = re.findall(r"[a-z0-9]+", text.lower())
@@ -106,18 +138,26 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
106
138
  return score, matched
107
139
 
108
140
 
109
- def _content_score(query_terms: list[str], file_content: str, idf_weights: dict[str, float]) -> tuple[float, list[str]]:
141
+ def _content_score(query_terms: list[str], features: "FileFeatures", idf_weights: dict[str, float]) -> tuple[float, list[str]]:
110
142
  """
111
143
  Score how often query terms appear in the file's source code.
112
- Uses TF-IDF weighting, capped to avoid huge files dominating.
144
+ Applies massive multipliers for architectural declarations and annotations.
113
145
  """
114
- content_tokens = set(re.findall(r"[a-z0-9]+", file_content))
115
- matched = [t for t in query_terms if t in content_tokens]
116
-
117
- if not query_terms:
146
+ if not features or not query_terms:
118
147
  return 0.0, []
119
148
 
120
- score = sum(idf_weights.get(t, 1.0) for t in matched)
149
+ matched = [t for t in query_terms if t in features.tokens]
150
+
151
+ score = 0.0
152
+ for t in matched:
153
+ base_weight = idf_weights.get(t, 1.0)
154
+ multiplier = 1.0
155
+ if t in features.declarations:
156
+ multiplier = 10.0
157
+ elif t in features.annotations:
158
+ multiplier = 5.0
159
+ score += base_weight * multiplier
160
+
121
161
  max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
122
162
  if max_score > 0:
123
163
  score = score / max_score
@@ -179,23 +219,27 @@ 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
 
185
- file_contents = {}
226
+ file_features_cache = {}
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)
229
+ extension = pathlib.Path(path).suffix.lower()
230
+
188
231
  try:
189
- content = open(abs_path, encoding="utf-8").read().lower()
190
- file_contents[path] = content
191
- content_tokens = set(re.findall(r"[a-z0-9]+", content))
232
+ content = open(abs_path, encoding="utf-8").read()
233
+ features = _extract_file_features(content, extension)
234
+ file_features_cache[path] = features
235
+
192
236
  path_tokens = set(_path_tokens(path))
193
- combined_tokens = content_tokens.union(path_tokens)
237
+ combined_tokens = features.tokens.union(path_tokens)
194
238
  for term in query_terms:
195
239
  if term in combined_tokens:
196
240
  document_frequency[term] += 1
197
241
  except Exception:
198
- file_contents[path] = ""
242
+ file_features_cache[path] = None
199
243
 
200
244
  idf_weights = {}
201
245
  for term in query_terms:
@@ -209,7 +253,7 @@ def query(
209
253
 
210
254
  for path, node in repo_graph.nodes.items():
211
255
  kscore, kterms = _keyword_score(query_terms, path, idf_weights)
212
- cscore, cterms = _content_score(query_terms, file_contents.get(path, ""), idf_weights)
256
+ cscore, cterms = _content_score(query_terms, file_features_cache.get(path), idf_weights)
213
257
 
214
258
  keyword_scores[path] = kscore
215
259
  content_scores[path] = cscore