contextl 1.2.20 → 1.2.21
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
|
@@ -127,6 +127,9 @@ def analyze_impact(
|
|
|
127
127
|
Raises:
|
|
128
128
|
ValueError: If target_file is not found in the graph.
|
|
129
129
|
"""
|
|
130
|
+
if not target_file:
|
|
131
|
+
raise ValueError("target_file cannot be empty")
|
|
132
|
+
|
|
130
133
|
if target_file not in repo_graph.nodes:
|
|
131
134
|
raise ValueError(
|
|
132
135
|
f"File not found in repository graph: {target_file}. "
|
|
@@ -191,6 +194,8 @@ def analyze_impact(
|
|
|
191
194
|
target_node = repo_graph.nodes.get(target_file)
|
|
192
195
|
in_degree = target_node.in_degree if target_node else 0
|
|
193
196
|
report.risk_score = (in_degree * 0.5) + (len(report.transitively_affected) * 0.2)
|
|
197
|
+
if report.cycle_detected:
|
|
198
|
+
report.risk_score += len(report.cycle_members) * 0.8
|
|
194
199
|
|
|
195
200
|
return report
|
|
196
201
|
|
package/python/import_parser.py
CHANGED
|
@@ -567,7 +567,7 @@ def _find_file_in_repo(
|
|
|
567
567
|
# Fallback for Python cross-language imports (e.g. from tests to .ts/.js core logic)
|
|
568
568
|
for ext in [".ts", ".tsx", ".js", ".jsx", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".c"]:
|
|
569
569
|
alt_suffix = "/" + candidate_str + ext
|
|
570
|
-
alt_matches = [p for p in file_index if p.endswith(alt_suffix)]
|
|
570
|
+
alt_matches = [p for p in file_index if p.endswith(alt_suffix) or p == alt_suffix.lstrip("/")]
|
|
571
571
|
if len(alt_matches) == 1:
|
|
572
572
|
return alt_matches[0]
|
|
573
573
|
|
package/python/mcp_server.py
CHANGED
|
@@ -58,7 +58,7 @@ from import_parser import extract_skeleton
|
|
|
58
58
|
@dataclass
|
|
59
59
|
class _CacheEntry:
|
|
60
60
|
repo_path: str # resolved absolute path
|
|
61
|
-
mtime_hash: float # max mtime across all source files at build time
|
|
61
|
+
mtime_hash: tuple[float, int] # max mtime across all source files at build time, and total file count
|
|
62
62
|
scan: object # ScanResult
|
|
63
63
|
repo_graph: object # RepoGraph
|
|
64
64
|
last_checked_time: float = 0.0
|
|
@@ -68,9 +68,10 @@ _cache_lock = threading.Lock()
|
|
|
68
68
|
_cache: dict[str, _CacheEntry] = {}
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def _compute_mtime_hash(repo_path: str) -> float:
|
|
72
|
-
"""Walk repo_path and return
|
|
71
|
+
def _compute_mtime_hash(repo_path: str) -> tuple[float, int]:
|
|
72
|
+
"""Walk repo_path and return (maximum file modification time, file count)."""
|
|
73
73
|
max_mtime = 0.0
|
|
74
|
+
file_count = 0
|
|
74
75
|
for dirpath, _, filenames in os.walk(repo_path):
|
|
75
76
|
# Skip hidden dirs and common noise dirs
|
|
76
77
|
dirpath_obj = Path(dirpath)
|
|
@@ -82,9 +83,10 @@ def _compute_mtime_hash(repo_path: str) -> float:
|
|
|
82
83
|
mtime = os.path.getmtime(os.path.join(dirpath, fname))
|
|
83
84
|
if mtime > max_mtime:
|
|
84
85
|
max_mtime = mtime
|
|
86
|
+
file_count += 1
|
|
85
87
|
except OSError:
|
|
86
88
|
pass
|
|
87
|
-
return max_mtime
|
|
89
|
+
return (max_mtime, file_count)
|
|
88
90
|
|
|
89
91
|
|
|
90
92
|
def _get_graph(repo_path: str):
|
|
@@ -52,11 +52,15 @@ def _extract_file_context(file_path: str, extension: str, root_dir: str) -> tupl
|
|
|
52
52
|
return explanation, outline, snippet
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def export_obsidian_vault(
|
|
55
|
+
def export_obsidian_vault(repo_path: str, output_dir: str) -> str:
|
|
56
56
|
"""
|
|
57
57
|
Generates a markdown file for every node in the graph in a hierarchical folder structure,
|
|
58
58
|
with Obsidian wikilinks connecting them based on imports.
|
|
59
59
|
"""
|
|
60
|
+
scan = scan_repo(repo_path)
|
|
61
|
+
parse = parse_imports(scan)
|
|
62
|
+
repo_graph = build_graph(scan, parse)
|
|
63
|
+
|
|
60
64
|
out_path = Path(output_dir).resolve()
|
|
61
65
|
|
|
62
66
|
if out_path.exists():
|
package/python/query_engine.py
CHANGED
|
@@ -98,7 +98,7 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
|
|
|
98
98
|
score += 2.0 * idf_weights.get(term, 1.0) # Massive signal
|
|
99
99
|
matched.append(term)
|
|
100
100
|
# Substring match on filename
|
|
101
|
-
elif len(term) >=
|
|
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):
|
|
102
102
|
score += 1.5 * idf_weights.get(term, 1.0)
|
|
103
103
|
matched.append(term)
|
|
104
104
|
# Exact match on path
|
|
@@ -107,7 +107,7 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
|
|
|
107
107
|
if term not in matched:
|
|
108
108
|
matched.append(term)
|
|
109
109
|
# Substring match on path
|
|
110
|
-
elif len(term) >=
|
|
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):
|
|
111
111
|
score += 0.4 * idf_weights.get(term, 1.0)
|
|
112
112
|
if term not in matched:
|
|
113
113
|
matched.append(term)
|
|
@@ -186,7 +186,7 @@ def _content_score(query_terms: list[str], file_path: str, file_content: str, id
|
|
|
186
186
|
for ct in content_tokens_list:
|
|
187
187
|
if t == ct:
|
|
188
188
|
count += 1
|
|
189
|
-
elif len(t) >=
|
|
189
|
+
elif len(t) >= 2 and (t in ct or (len(ct) >= 2 and ct in t)):
|
|
190
190
|
count += 1
|
|
191
191
|
if count > 0:
|
|
192
192
|
term_counts[t] = count
|
|
@@ -322,9 +322,9 @@ def query(
|
|
|
322
322
|
for term in query_terms:
|
|
323
323
|
if term in combined_tokens:
|
|
324
324
|
document_frequency[term] += 1
|
|
325
|
-
elif len(term) >=
|
|
325
|
+
elif len(term) >= 2:
|
|
326
326
|
for ct in combined_tokens:
|
|
327
|
-
if term in ct or (len(ct) >=
|
|
327
|
+
if term in ct or (len(ct) >= 2 and ct in term):
|
|
328
328
|
document_frequency[term] += 1
|
|
329
329
|
break
|
|
330
330
|
|
|
@@ -381,6 +381,13 @@ def query(
|
|
|
381
381
|
matched_terms=matched_terms.get(path, []),
|
|
382
382
|
))
|
|
383
383
|
|
|
384
|
+
# Down-rank test files unless the user specifically searched for tests
|
|
385
|
+
is_test_query = "test" in query_str.lower() or "spec" in query_str.lower()
|
|
386
|
+
from impact_analysis import _is_test_file
|
|
387
|
+
for r in results:
|
|
388
|
+
if _is_test_file(r.path) and not is_test_query:
|
|
389
|
+
r.total_score *= 0.5
|
|
390
|
+
|
|
384
391
|
# Sort by total score descending
|
|
385
392
|
results.sort(key=lambda r: r.total_score, reverse=True)
|
|
386
393
|
|