contextl 1.2.24 → 1.2.26

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.24",
3
+ "version": "1.2.26",
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",
@@ -19,6 +19,18 @@ import sys
19
19
  from pathlib import Path, PurePosixPath
20
20
  from dataclasses import dataclass, field
21
21
 
22
+ PYTHON_STDLIB_AND_COMMON_PACKAGES = {
23
+ "os", "sys", "re", "math", "json", "datetime", "typing", "collections",
24
+ "itertools", "functools", "pathlib", "logging", "argparse", "subprocess",
25
+ "unittest", "pytest", "numpy", "pandas", "requests", "urllib", "asyncio",
26
+ "time", "random", "hashlib", "base64", "csv", "xml", "html", "http",
27
+ "concurrent", "multiprocessing", "threading", "sqlite3", "flask", "django",
28
+ "fastapi", "sqlalchemy", "pydantic", "dataclasses", "uuid", "socket",
29
+ "struct", "tempfile", "shutil", "glob", "copy", "warnings", "traceback",
30
+ "contextlib", "io", "sysconfig", "importlib", "pkgutil", "runpy",
31
+ "celery", "redis", "psycopg2", "boto3", "jinja2", "pytest_mock",
32
+ }
33
+
22
34
  from scanner import ScannedFile, ScanResult
23
35
 
24
36
 
@@ -142,7 +154,7 @@ def _ts_imports_js_ts(root: "Node") -> list[str]:
142
154
  return child.text.decode("utf-8", errors="replace")
143
155
  # Fallback: strip quotes manually
144
156
  raw = node.text.decode("utf-8", errors="replace")
145
- return raw.strip("'\"`")
157
+ return raw.strip('"\'`')
146
158
  return None
147
159
 
148
160
  def visit(node: "Node"):
@@ -883,6 +895,10 @@ def parse_imports(scan_result: ScanResult) -> ParseResult:
883
895
  else:
884
896
  if scanned_file.extension == ".rs" and locals().get("is_likely_external", False):
885
897
  continue
898
+ if scanned_file.extension == ".py":
899
+ base_pkg = raw.split(".")[0]
900
+ if base_pkg in PYTHON_STDLIB_AND_COMMON_PACKAGES:
901
+ continue
886
902
  result.unresolved.append((scanned_file.path, raw))
887
903
 
888
904
  return result
@@ -1495,11 +1511,18 @@ def extract_skeleton(file_path: str) -> dict:
1495
1511
  try:
1496
1512
  path = Path(file_path)
1497
1513
  ext = path.suffix.lower()
1514
+
1515
+ lang_map = {
1516
+ ".py": "python", ".ts": "typescript", ".tsx": "tsx",
1517
+ ".js": "javascript", ".jsx": "jsx", ".java": "java",
1518
+ ".rs": "rust", ".go": "go", ".cpp": "cpp",
1519
+ }
1520
+ derived_language = lang_map.get(ext, ext.lstrip("."))
1498
1521
 
1499
1522
  if not _TS_AVAILABLE:
1500
1523
  return {
1501
1524
  "file": str(path),
1502
- "language": ext.lstrip("."),
1525
+ "language": derived_language,
1503
1526
  "error": "tree-sitter not installed — run: pip install tree-sitter tree-sitter-python tree-sitter-typescript ...",
1504
1527
  "classes": [],
1505
1528
  "functions": [],
@@ -1511,7 +1534,7 @@ def extract_skeleton(file_path: str) -> dict:
1511
1534
  if parser is None:
1512
1535
  return {
1513
1536
  "file": str(path),
1514
- "language": ext.lstrip("."),
1537
+ "language": derived_language,
1515
1538
  "error": f"Unsupported file type: {ext}",
1516
1539
  "classes": [],
1517
1540
  "functions": [],
@@ -1526,7 +1549,7 @@ def extract_skeleton(file_path: str) -> dict:
1526
1549
  if handler is None:
1527
1550
  return {
1528
1551
  "file": str(path),
1529
- "language": ext.lstrip("."),
1552
+ "language": derived_language,
1530
1553
  "error": f"No skeleton handler for {ext}",
1531
1554
  "classes": [],
1532
1555
  "functions": [],
@@ -1534,12 +1557,6 @@ def extract_skeleton(file_path: str) -> dict:
1534
1557
  "docstrings": {},
1535
1558
  }
1536
1559
 
1537
- lang_map = {
1538
- ".py": "python", ".ts": "typescript", ".tsx": "tsx",
1539
- ".js": "javascript", ".jsx": "jsx", ".java": "java",
1540
- ".rs": "rust", ".go": "go", ".cpp": "cpp",
1541
- }
1542
-
1543
1560
  if ext == ".py":
1544
1561
  body = handler(tree.root_node, source_bytes)
1545
1562
  else:
@@ -1547,14 +1564,22 @@ def extract_skeleton(file_path: str) -> dict:
1547
1564
 
1548
1565
  return {
1549
1566
  "file": str(path),
1550
- "language": lang_map.get(ext, ext.lstrip(".")),
1567
+ "language": derived_language,
1551
1568
  **body,
1552
1569
  }
1553
1570
 
1554
1571
  except Exception as e:
1572
+ # Fallback if path is not yet defined
1573
+ lang = "unknown"
1574
+ if 'derived_language' in locals():
1575
+ lang = derived_language
1576
+ elif 'file_path' in locals():
1577
+ ext = Path(file_path).suffix.lower()
1578
+ lang = lang_map.get(ext, ext.lstrip(".")) if 'lang_map' in locals() else ext.lstrip(".")
1579
+
1555
1580
  return {
1556
1581
  "file": str(file_path),
1557
- "language": "unknown",
1582
+ "language": lang,
1558
1583
  "error": str(e),
1559
1584
  "classes": [],
1560
1585
  "functions": [],
@@ -96,7 +96,7 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
96
96
  for term in query_terms:
97
97
  # Exact match on filename
98
98
  if term in filename_toks:
99
- score += 2.0 * idf_weights.get(term, 1.0) # Massive signal
99
+ score += 4.0 * idf_weights.get(term, 1.0) # Massive signal
100
100
  matched.append(term)
101
101
  # Substring or abbreviation match on filename
102
102
  elif len(term) >= 2 and any(
@@ -104,11 +104,11 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
104
104
  all(c in iter(ft) for c in term)
105
105
  for ft in filename_toks
106
106
  ):
107
- score += 1.5 * idf_weights.get(term, 1.0)
107
+ score += 3.0 * idf_weights.get(term, 1.0)
108
108
  matched.append(term)
109
109
  # Exact match on path
110
110
  elif term in path_toks:
111
- score += 0.8 * idf_weights.get(term, 1.0) # Strong signal
111
+ score += 2.0 * idf_weights.get(term, 1.0) # Strong signal
112
112
  if term not in matched:
113
113
  matched.append(term)
114
114
  # Substring or abbreviation match on path
@@ -117,14 +117,16 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
117
117
  all(c in iter(pt) for c in term)
118
118
  for pt in path_toks
119
119
  ):
120
- score += 0.4 * idf_weights.get(term, 1.0)
120
+ score += 1.5 * idf_weights.get(term, 1.0)
121
121
  if term not in matched:
122
122
  matched.append(term)
123
123
 
124
124
  # Normalize by max possible score (sum of all IDFs)
125
125
  max_score = sum(idf_weights.get(t, 1.0) for t in query_terms)
126
126
  if max_score > 0:
127
- score = min(1.0, score / max_score) # Cap at 1.0 since we have massive multipliers
127
+ base_normalized = min(1.0, sum(idf_weights.get(t, 1.0) for t in matched) / max_score)
128
+ bonus = score - sum(idf_weights.get(t, 1.0) for t in matched)
129
+ score = base_normalized + max(0, bonus)
128
130
 
129
131
  return score, matched
130
132
 
@@ -362,8 +364,9 @@ def query(
362
364
  content_scores[path] = cscore
363
365
  matched_terms[path] = list(set(kterms + cterms))
364
366
 
365
- # Base combination: 50/50 keyword vs content
366
- combined = (kscore * 0.5) + (cscore * 0.5)
367
+ # Base combination: heavily weight filename/path matches (70%) over content matches (30%)
368
+ # This prevents transitive consumers of a file from out-ranking the file itself
369
+ combined = (kscore * 0.7) + (cscore * 0.3)
367
370
 
368
371
  # Tiebreaker: Slightly penalize deep paths so src/x.py wins over build/npm/python/x.py
369
372
  depth_penalty = 0.0001 * path.count("/")