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 +0 -37
- package/package.json +1 -1
- package/python/query_engine.py +56 -50
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
package/python/query_engine.py
CHANGED
|
@@ -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],
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
150
|
+
|
|
151
|
+
# 3. Apply Multipliers
|
|
152
|
+
if t in declared_classes:
|
|
156
153
|
multiplier = 10.0
|
|
157
|
-
elif t in
|
|
154
|
+
elif t in annotations:
|
|
158
155
|
multiplier = 5.0
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
final_score = 0.0
|
|
166
166
|
|
|
167
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|