contextl 1.2.23 → 1.2.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextl",
3
- "version": "1.2.23",
3
+ "version": "1.2.24",
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",
@@ -31,7 +31,7 @@
31
31
  "files": [
32
32
  "bin/",
33
33
  "python/",
34
- "README.md"
34
+ "README.md", "requirements.txt"
35
35
  ],
36
36
 
37
37
  "engines": {
@@ -553,24 +553,35 @@ def _find_file_in_repo(
553
553
  return None
554
554
 
555
555
  if extension == ".py":
556
- py_ext = candidate_str + ".py"
557
- if py_ext in file_index:
558
- return py_ext
559
- py_idx = candidate_str + "/__init__.py"
560
- if py_idx in file_index:
561
- return py_idx
562
- suffix = "/" + py_ext
563
- for path in file_index:
564
- if path.endswith(suffix) or path == py_ext:
565
- return path
566
-
567
- # Fallback for Python cross-language imports (e.g. from tests to .ts/.js core logic)
568
- for ext in [".ts", ".tsx", ".js", ".jsx", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".c"]:
569
- alt_suffix = "/" + candidate_str + ext
570
- alt_matches = [p for p in file_index if p.endswith(alt_suffix) or p == alt_suffix.lstrip("/")]
571
- if len(alt_matches) == 1:
572
- return alt_matches[0]
573
-
556
+ def _resolve_python(cand: str) -> str | None:
557
+ py_ext = cand + ".py"
558
+ if py_ext in file_index:
559
+ return py_ext
560
+ py_idx = cand + "/__init__.py"
561
+ if py_idx in file_index:
562
+ return py_idx
563
+ suffix = "/" + py_ext
564
+ for path in file_index:
565
+ if path.endswith(suffix) or path == py_ext:
566
+ return path
567
+
568
+ # Fallback for Python cross-language imports (e.g. from tests to .ts/.js core logic)
569
+ for ext in [".ts", ".tsx", ".js", ".jsx", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".c"]:
570
+ alt_suffix = "/" + cand + ext
571
+ alt_matches = [p for p in file_index if p.endswith(alt_suffix) or p == alt_suffix.lstrip("/")]
572
+ if len(alt_matches) == 1:
573
+ return alt_matches[0]
574
+ return None
575
+
576
+ res = _resolve_python(candidate_str)
577
+ if res is not None:
578
+ return res
579
+
580
+ # Try stripping the last component (in case it's a symbol-level import like `from a.b import C`)
581
+ if "/" in candidate_str:
582
+ stripped = candidate_str.rsplit("/", 1)[0]
583
+ return _resolve_python(stripped)
584
+
574
585
  return None
575
586
  for ext in [".tsx", ".ts", ".jsx", ".js"]:
576
587
  with_ext = candidate_str + ext
@@ -106,7 +106,10 @@ def _get_graph(repo_path: str):
106
106
  now = time.time()
107
107
  entry = _cache.get(resolved)
108
108
 
109
- # Fast path: bypass filesystem walk if checked within last 5 seconds
109
+ # Fast path: bypass filesystem walk if checked within last 5 seconds.
110
+ # This intentionally creates a 5s "blind window" where new/deleted/modified
111
+ # files are not instantly detected, drastically reducing disk I/O on large repos
112
+ # when the MCP client rapid-fires multiple queries concurrently.
110
113
  if entry and (now - entry.last_checked_time < 5.0):
111
114
  return entry.scan, entry.repo_graph
112
115
 
@@ -17,6 +17,7 @@ No LLM. No embeddings. Pure text + graph.
17
17
  import math
18
18
  import re
19
19
  from dataclasses import dataclass, field
20
+ from impact_analysis import _is_test_file
20
21
 
21
22
  from scanner import scan_repo
22
23
  from import_parser import parse_imports
@@ -93,12 +94,16 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
93
94
  score = 0.0
94
95
 
95
96
  for term in query_terms:
96
- # Exact match
97
+ # Exact match on filename
97
98
  if term in filename_toks:
98
99
  score += 2.0 * idf_weights.get(term, 1.0) # Massive signal
99
100
  matched.append(term)
100
- # Substring match on filename
101
- elif len(term) >= 2 and any(term in ft for ft in filename_toks) or any(len(ft) >= 2 and ft in term for ft in filename_toks):
101
+ # Substring or abbreviation match on filename
102
+ elif len(term) >= 2 and any(
103
+ term in ft or ft in term or ft.startswith(term) or term.startswith(ft) or
104
+ all(c in iter(ft) for c in term)
105
+ for ft in filename_toks
106
+ ):
102
107
  score += 1.5 * idf_weights.get(term, 1.0)
103
108
  matched.append(term)
104
109
  # Exact match on path
@@ -106,8 +111,12 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
106
111
  score += 0.8 * idf_weights.get(term, 1.0) # Strong signal
107
112
  if term not in matched:
108
113
  matched.append(term)
109
- # Substring match on path
110
- elif len(term) >= 2 and any(term in pt for pt in path_toks) or any(len(pt) >= 2 and pt in term for pt in path_toks):
114
+ # Substring or abbreviation match on path
115
+ elif len(term) >= 2 and any(
116
+ term in pt or pt in term or pt.startswith(term) or term.startswith(pt) or
117
+ all(c in iter(pt) for c in term)
118
+ for pt in path_toks
119
+ ):
111
120
  score += 0.4 * idf_weights.get(term, 1.0)
112
121
  if term not in matched:
113
122
  matched.append(term)
@@ -247,7 +256,7 @@ def _content_score(query_terms: list[str], file_path: str, file_content: str, id
247
256
  def _apply_neighbor_bonus(
248
257
  scores: dict[str, float],
249
258
  repo_graph: RepoGraph,
250
- boost: float = 0.35,
259
+ boost: float = 0.2,
251
260
  depth: int = 1,
252
261
  ) -> dict[str, float]:
253
262
  """
@@ -383,7 +392,6 @@ def query(
383
392
 
384
393
  # Down-rank test files unless the user specifically searched for tests
385
394
  is_test_query = "test" in q.lower() or "spec" in q.lower()
386
- from impact_analysis import _is_test_file
387
395
  for r in results:
388
396
  if _is_test_file(r.path) and not is_test_query:
389
397
  r.total_score *= 0.5
@@ -0,0 +1,10 @@
1
+ networkx
2
+ mcp>=1.0.0
3
+ tree-sitter>=0.22.0
4
+ tree-sitter-python>=0.22.0
5
+ tree-sitter-javascript>=0.22.0
6
+ tree-sitter-typescript>=0.22.0
7
+ tree-sitter-java>=0.22.0
8
+ tree-sitter-rust>=0.22.0
9
+ tree-sitter-go>=0.22.0
10
+ tree-sitter-cpp>=0.22.0