contextl 1.2.16 → 1.2.18
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/bin/contextl.js +1 -1
- package/package.json +1 -1
- package/python/git_review.py +10 -7
- package/python/graph_builder.py +19 -1
- package/python/import_parser.py +85 -0
- package/python/mcp_server.py +31 -29
- package/python/obsidian_export.py +17 -21
- package/python/query_engine.py +28 -39
- package/python/scanner.py +3 -2
package/bin/contextl.js
CHANGED
|
@@ -106,7 +106,7 @@ function pipInstall(python, packageName) {
|
|
|
106
106
|
function ensureDeps(python) {
|
|
107
107
|
const required = [
|
|
108
108
|
{ importName: "networkx", pipName: "networkx" },
|
|
109
|
-
{ importName: "mcp", pipName: "mcp" },
|
|
109
|
+
{ importName: "mcp", pipName: "mcp>=1.0.0" },
|
|
110
110
|
{ importName: "tree_sitter", pipName: "tree-sitter>=0.22.0" },
|
|
111
111
|
{ importName: "tree_sitter_python", pipName: "tree-sitter-python>=0.22.0" },
|
|
112
112
|
{ importName: "tree_sitter_javascript", pipName: "tree-sitter-javascript>=0.22.0" },
|
package/package.json
CHANGED
package/python/git_review.py
CHANGED
|
@@ -58,13 +58,16 @@ def _run_git(args: list[str], cwd: str) -> list[str]:
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _is_git_repo(repo_path: str) -> bool:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
64
|
+
cwd=repo_path,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
)
|
|
68
|
+
return result.returncode == 0
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
return False
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
def get_changed_files(
|
package/python/graph_builder.py
CHANGED
|
@@ -34,6 +34,10 @@ class FileNode:
|
|
|
34
34
|
out_degree: int = 0 # files this imports
|
|
35
35
|
centrality: float = 0.0 # PageRank score
|
|
36
36
|
|
|
37
|
+
# Text data (computed once)
|
|
38
|
+
content: str = ""
|
|
39
|
+
doc_length: int = 0
|
|
40
|
+
|
|
37
41
|
|
|
38
42
|
@dataclass
|
|
39
43
|
class RepoGraph:
|
|
@@ -126,16 +130,30 @@ def build_graph(scan_result, parse_result: ParseResult) -> RepoGraph:
|
|
|
126
130
|
"""
|
|
127
131
|
G = nx.DiGraph()
|
|
128
132
|
|
|
133
|
+
import re
|
|
134
|
+
|
|
129
135
|
# Add all scanned files as nodes
|
|
130
136
|
file_nodes: dict[str, FileNode] = {}
|
|
131
137
|
for f in scan_result.files:
|
|
138
|
+
abs_path = str(Path(scan_result.root) / f.path)
|
|
139
|
+
try:
|
|
140
|
+
with open(abs_path, "r", encoding="utf-8") as f_obj:
|
|
141
|
+
content = f_obj.read()
|
|
142
|
+
dl = len(re.findall(r"[a-z0-9]+", content.lower()))
|
|
143
|
+
except Exception:
|
|
144
|
+
content = ""
|
|
145
|
+
dl = 0
|
|
146
|
+
|
|
132
147
|
node = FileNode(
|
|
133
148
|
path=f.path,
|
|
134
149
|
extension=f.extension,
|
|
135
150
|
size_bytes=f.size_bytes,
|
|
151
|
+
content=content,
|
|
152
|
+
doc_length=dl,
|
|
136
153
|
)
|
|
137
154
|
file_nodes[f.path] = node
|
|
138
|
-
|
|
155
|
+
# Add basic node properties to nx graph
|
|
156
|
+
G.add_node(f.path, path=node.path, extension=node.extension, size_bytes=node.size_bytes)
|
|
139
157
|
|
|
140
158
|
# Add edges from import relationships
|
|
141
159
|
for rel in parse_result.relationships:
|
package/python/import_parser.py
CHANGED
|
@@ -1367,6 +1367,85 @@ def _skeleton_rust(root: "Node") -> dict:
|
|
|
1367
1367
|
return {"classes": classes, "functions": functions, "exports": exports, "docstrings": docstrings}
|
|
1368
1368
|
|
|
1369
1369
|
|
|
1370
|
+
|
|
1371
|
+
def _skeleton_go(root: "Node") -> dict:
|
|
1372
|
+
classes = []
|
|
1373
|
+
functions = []
|
|
1374
|
+
exports = []
|
|
1375
|
+
docstrings = {}
|
|
1376
|
+
|
|
1377
|
+
def visit_fn(node: "Node") -> dict:
|
|
1378
|
+
name = ""
|
|
1379
|
+
params = ""
|
|
1380
|
+
ret_type = ""
|
|
1381
|
+
is_method = (node.type == "method_declaration")
|
|
1382
|
+
receiver = ""
|
|
1383
|
+
|
|
1384
|
+
for child in node.children:
|
|
1385
|
+
if child.type == "identifier" or child.type == "field_identifier":
|
|
1386
|
+
if not name:
|
|
1387
|
+
name = _node_text(child)
|
|
1388
|
+
elif child.type == "parameter_list":
|
|
1389
|
+
if is_method and not receiver:
|
|
1390
|
+
receiver = _node_text(child)
|
|
1391
|
+
else:
|
|
1392
|
+
params = _node_text(child)
|
|
1393
|
+
elif child.type == "type_identifier":
|
|
1394
|
+
ret_type = _node_text(child)
|
|
1395
|
+
|
|
1396
|
+
return {"name": name, "params": params, "return_type": ret_type, "visibility": "pub" if name and name[0].isupper() else "private", "is_async": False, "decorators": [], "receiver": receiver}
|
|
1397
|
+
|
|
1398
|
+
def visit(node: "Node"):
|
|
1399
|
+
if node.type in ("function_declaration", "method_declaration"):
|
|
1400
|
+
functions.append(visit_fn(node))
|
|
1401
|
+
elif node.type == "type_spec":
|
|
1402
|
+
name = ""
|
|
1403
|
+
for child in node.children:
|
|
1404
|
+
if child.type == "type_identifier":
|
|
1405
|
+
name = _node_text(child)
|
|
1406
|
+
elif child.type == "struct_type" or child.type == "interface_type":
|
|
1407
|
+
if name:
|
|
1408
|
+
classes.append({"name": name, "fields": [], "methods": [], "extends": "", "implements": [], "properties": []})
|
|
1409
|
+
for child in node.children:
|
|
1410
|
+
visit(child)
|
|
1411
|
+
|
|
1412
|
+
visit(root)
|
|
1413
|
+
return {"classes": classes, "functions": functions, "exports": exports, "docstrings": docstrings}
|
|
1414
|
+
|
|
1415
|
+
def _skeleton_cpp(root: "Node") -> dict:
|
|
1416
|
+
classes = []
|
|
1417
|
+
functions = []
|
|
1418
|
+
exports = []
|
|
1419
|
+
docstrings = {}
|
|
1420
|
+
|
|
1421
|
+
def visit_fn(node: "Node") -> dict:
|
|
1422
|
+
name = ""
|
|
1423
|
+
params = ""
|
|
1424
|
+
for child in node.children:
|
|
1425
|
+
if child.type == "function_declarator":
|
|
1426
|
+
for sub in child.children:
|
|
1427
|
+
if sub.type == "identifier" or sub.type == "field_identifier":
|
|
1428
|
+
name = _node_text(sub)
|
|
1429
|
+
elif sub.type == "parameter_list":
|
|
1430
|
+
params = _node_text(sub)
|
|
1431
|
+
return {"name": name, "params": params, "return_type": "", "visibility": "pub", "is_async": False, "decorators": []}
|
|
1432
|
+
|
|
1433
|
+
def visit(node: "Node"):
|
|
1434
|
+
if node.type == "function_definition":
|
|
1435
|
+
functions.append(visit_fn(node))
|
|
1436
|
+
elif node.type in ("class_specifier", "struct_specifier"):
|
|
1437
|
+
name = ""
|
|
1438
|
+
for child in node.children:
|
|
1439
|
+
if child.type == "type_identifier":
|
|
1440
|
+
name = _node_text(child)
|
|
1441
|
+
if name:
|
|
1442
|
+
classes.append({"name": name, "fields": [], "methods": [], "extends": "", "implements": [], "properties": []})
|
|
1443
|
+
for child in node.children:
|
|
1444
|
+
visit(child)
|
|
1445
|
+
|
|
1446
|
+
visit(root)
|
|
1447
|
+
return {"classes": classes, "functions": functions, "exports": exports, "docstrings": docstrings}
|
|
1448
|
+
|
|
1370
1449
|
_SKELETON_HANDLERS = {
|
|
1371
1450
|
".py": _skeleton_python,
|
|
1372
1451
|
".ts": _skeleton_ts_js,
|
|
@@ -1375,6 +1454,12 @@ _SKELETON_HANDLERS = {
|
|
|
1375
1454
|
".jsx": _skeleton_ts_js,
|
|
1376
1455
|
".java": _skeleton_java,
|
|
1377
1456
|
".rs": _skeleton_rust,
|
|
1457
|
+
".go": _skeleton_go,
|
|
1458
|
+
".cpp": _skeleton_cpp,
|
|
1459
|
+
".h": _skeleton_cpp,
|
|
1460
|
+
".hpp": _skeleton_cpp,
|
|
1461
|
+
".cc": _skeleton_cpp,
|
|
1462
|
+
".c": _skeleton_cpp,
|
|
1378
1463
|
}
|
|
1379
1464
|
|
|
1380
1465
|
|
package/python/mcp_server.py
CHANGED
|
@@ -14,7 +14,7 @@ Tools exposed:
|
|
|
14
14
|
query_repo — rank files by relevance to a natural-language query
|
|
15
15
|
scan_repo — list all source files the engine can see in a repo
|
|
16
16
|
analyze_impact — find every file affected by changing a target file
|
|
17
|
-
|
|
17
|
+
find_dead_files — find unimported files with in-degree 0
|
|
18
18
|
export_obsidian_vault — export repo graph to an Obsidian markdown vault
|
|
19
19
|
|
|
20
20
|
Performance:
|
|
@@ -61,10 +61,11 @@ class _CacheEntry:
|
|
|
61
61
|
mtime_hash: float # max mtime across all source files at build time
|
|
62
62
|
scan: object # ScanResult
|
|
63
63
|
repo_graph: object # RepoGraph
|
|
64
|
+
last_checked_time: float = 0.0
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
_cache_lock = threading.Lock()
|
|
67
|
-
_cache:
|
|
68
|
+
_cache: dict[str, _CacheEntry] = {}
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def _compute_mtime_hash(repo_path: str) -> float:
|
|
@@ -95,16 +96,24 @@ def _get_graph(repo_path: str):
|
|
|
95
96
|
On subsequent calls with unchanged files: returns instantly from cache.
|
|
96
97
|
"""
|
|
97
98
|
global _cache
|
|
99
|
+
import time
|
|
98
100
|
|
|
99
101
|
resolved = str(Path(repo_path).resolve())
|
|
100
|
-
mtime_hash = _compute_mtime_hash(resolved)
|
|
101
102
|
|
|
102
103
|
with _cache_lock:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
now = time.time()
|
|
105
|
+
entry = _cache.get(resolved)
|
|
106
|
+
|
|
107
|
+
# Fast path: bypass filesystem walk if checked within last 5 seconds
|
|
108
|
+
if entry and (now - entry.last_checked_time < 5.0):
|
|
109
|
+
return entry.scan, entry.repo_graph
|
|
110
|
+
|
|
111
|
+
mtime_hash = _compute_mtime_hash(resolved)
|
|
112
|
+
|
|
113
|
+
if entry and entry.mtime_hash == mtime_hash:
|
|
114
|
+
entry.last_checked_time = now
|
|
106
115
|
print("[contextl] graph cache hit", file=sys.stderr, flush=True)
|
|
107
|
-
return
|
|
116
|
+
return entry.scan, entry.repo_graph
|
|
108
117
|
|
|
109
118
|
# Cache miss — build the pipeline
|
|
110
119
|
print("[contextl] building graph...", file=sys.stderr, flush=True)
|
|
@@ -112,11 +121,12 @@ def _get_graph(repo_path: str):
|
|
|
112
121
|
parse = parse_imports(scan)
|
|
113
122
|
repo_graph = build_graph(scan, parse)
|
|
114
123
|
|
|
115
|
-
_cache = _CacheEntry(
|
|
124
|
+
_cache[resolved] = _CacheEntry(
|
|
116
125
|
repo_path=resolved,
|
|
117
126
|
mtime_hash=mtime_hash,
|
|
118
127
|
scan=scan,
|
|
119
128
|
repo_graph=repo_graph,
|
|
129
|
+
last_checked_time=now,
|
|
120
130
|
)
|
|
121
131
|
print(f"[contextl] graph built: {scan.total_files} files", file=sys.stderr, flush=True)
|
|
122
132
|
return scan, repo_graph
|
|
@@ -228,7 +238,7 @@ async def list_tools() -> list[types.Tool]:
|
|
|
228
238
|
},
|
|
229
239
|
),
|
|
230
240
|
types.Tool(
|
|
231
|
-
name="
|
|
241
|
+
name="find_dead_files",
|
|
232
242
|
description=(
|
|
233
243
|
"Find files in the repository that are never imported by any other file "
|
|
234
244
|
"(in-degree of 0 in the dependency graph). Automatically filters out "
|
|
@@ -340,8 +350,8 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
340
350
|
if name == "analyze_impact":
|
|
341
351
|
return await _handle_impact(arguments)
|
|
342
352
|
|
|
343
|
-
if name == "
|
|
344
|
-
return await
|
|
353
|
+
if name == "find_dead_files":
|
|
354
|
+
return await _handle_dead_files(arguments)
|
|
345
355
|
|
|
346
356
|
if name == "export_obsidian_vault":
|
|
347
357
|
return await _handle_export_obsidian(arguments)
|
|
@@ -363,10 +373,8 @@ async def _handle_query(args: dict) -> list[types.TextContent]:
|
|
|
363
373
|
query_str = args.get("query", "")
|
|
364
374
|
top_n = min(int(args.get("top_n", 5)), 20)
|
|
365
375
|
|
|
366
|
-
# Run the engine (synchronous — wrap in thread to stay async-safe)
|
|
367
|
-
loop = asyncio.get_event_loop()
|
|
368
376
|
try:
|
|
369
|
-
result = await
|
|
377
|
+
result = await asyncio.to_thread(_run_query, repo_path, query_str, top_n)
|
|
370
378
|
except Exception as e:
|
|
371
379
|
result = {"error": str(e), "query": query_str, "repo": repo_path, "results": []}
|
|
372
380
|
|
|
@@ -399,9 +407,8 @@ def _run_query(repo_path: str, query_str: str, top_n: int) -> dict:
|
|
|
399
407
|
async def _handle_scan(args: dict) -> list[types.TextContent]:
|
|
400
408
|
repo_path = args.get("repo_path", "")
|
|
401
409
|
|
|
402
|
-
loop = asyncio.get_event_loop()
|
|
403
410
|
try:
|
|
404
|
-
result = await
|
|
411
|
+
result = await asyncio.to_thread(_run_scan, repo_path)
|
|
405
412
|
except Exception as e:
|
|
406
413
|
result = {"error": str(e), "repo": repo_path}
|
|
407
414
|
|
|
@@ -426,9 +433,8 @@ async def _handle_impact(args: dict) -> list[types.TextContent]:
|
|
|
426
433
|
target_file = args.get("target_file", "")
|
|
427
434
|
max_depth = min(int(args.get("max_depth", 5)), 10)
|
|
428
435
|
|
|
429
|
-
loop = asyncio.get_event_loop()
|
|
430
436
|
try:
|
|
431
|
-
result = await
|
|
437
|
+
result = await asyncio.to_thread(_run_impact, repo_path, target_file, max_depth)
|
|
432
438
|
except Exception as e:
|
|
433
439
|
result = {"error": str(e), "repo": repo_path, "target_file": target_file}
|
|
434
440
|
|
|
@@ -461,19 +467,18 @@ def _run_impact(repo_path: str, target_file: str, max_depth: int) -> dict:
|
|
|
461
467
|
}
|
|
462
468
|
|
|
463
469
|
|
|
464
|
-
async def
|
|
470
|
+
async def _handle_dead_files(args: dict) -> list[types.TextContent]:
|
|
465
471
|
repo_path = args.get("repo_path", "")
|
|
466
472
|
|
|
467
|
-
loop = asyncio.get_event_loop()
|
|
468
473
|
try:
|
|
469
|
-
result = await
|
|
474
|
+
result = await asyncio.to_thread(_run_dead_files, repo_path)
|
|
470
475
|
except Exception as e:
|
|
471
476
|
result = {"error": str(e), "repo": repo_path}
|
|
472
477
|
|
|
473
478
|
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
474
479
|
|
|
475
480
|
|
|
476
|
-
def
|
|
481
|
+
def _run_dead_files(repo_path: str) -> dict:
|
|
477
482
|
"""Uses cached graph — pipeline runs only on first call or after file changes."""
|
|
478
483
|
scan, repo_graph = _get_graph(repo_path)
|
|
479
484
|
standalone_files = find_standalone_files(repo_graph)
|
|
@@ -490,9 +495,8 @@ async def _handle_export_obsidian(args: dict) -> list[types.TextContent]:
|
|
|
490
495
|
repo_path = args.get("repo_path", "")
|
|
491
496
|
output_dir = args.get("output_dir", "")
|
|
492
497
|
|
|
493
|
-
loop = asyncio.get_event_loop()
|
|
494
498
|
try:
|
|
495
|
-
result = await
|
|
499
|
+
result = await asyncio.to_thread(_run_export_obsidian, repo_path, output_dir)
|
|
496
500
|
except Exception as e:
|
|
497
501
|
result = {"error": str(e), "repo": repo_path, "output_dir": output_dir}
|
|
498
502
|
|
|
@@ -517,10 +521,9 @@ async def _handle_review_changes(args: dict) -> list[types.TextContent]:
|
|
|
517
521
|
include_staged = bool(args.get("staged", True))
|
|
518
522
|
include_unstaged = bool(args.get("unstaged", True))
|
|
519
523
|
|
|
520
|
-
loop = asyncio.get_event_loop()
|
|
521
524
|
try:
|
|
522
|
-
result = await
|
|
523
|
-
|
|
525
|
+
result = await asyncio.to_thread(
|
|
526
|
+
_run_review_changes, repo_path, include_staged, include_unstaged
|
|
524
527
|
)
|
|
525
528
|
except Exception as e:
|
|
526
529
|
result = {"error": str(e), "repo": repo_path}
|
|
@@ -554,9 +557,8 @@ def _run_review_changes(repo_path: str, include_staged: bool, include_unstaged:
|
|
|
554
557
|
async def _handle_get_skeleton(args: dict) -> list[types.TextContent]:
|
|
555
558
|
file_path = args.get("file_path", "")
|
|
556
559
|
|
|
557
|
-
loop = asyncio.get_event_loop()
|
|
558
560
|
try:
|
|
559
|
-
result = await
|
|
561
|
+
result = await asyncio.to_thread(extract_skeleton, file_path)
|
|
560
562
|
except Exception as e:
|
|
561
563
|
result = {"error": str(e), "file": file_path}
|
|
562
564
|
|
|
@@ -13,21 +13,18 @@ import re
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from scanner import scan_repo
|
|
16
|
-
from import_parser import parse_imports
|
|
16
|
+
from import_parser import parse_imports, extract_skeleton
|
|
17
17
|
from graph_builder import build_graph, RepoGraph
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
PYTHON_DOCSTRING_PATTERN = re.compile(r'^\s*["\']{3}(.*?)["\']{3}', re.DOTALL)
|
|
21
|
-
JS_DOC_PATTERN = re.compile(r'^\s*/\*\*(.*?)\*/', re.DOTALL)
|
|
22
|
-
OUTLINE_PATTERN = re.compile(r'^\s*(?:export\s+)?(?:class|def|function|interface|type)\s+([a-zA-Z0-9_]+)', re.MULTILINE)
|
|
23
|
-
|
|
24
19
|
def _extract_file_context(file_path: str, extension: str, root_dir: str) -> tuple[str, list[str], str]:
|
|
25
20
|
"""Returns (explanation, outline_symbols, snippet)."""
|
|
26
21
|
explanation = "*No explanation provided in source code.*"
|
|
27
22
|
outline = []
|
|
28
23
|
snippet = ""
|
|
24
|
+
abs_path = str(Path(root_dir) / file_path)
|
|
25
|
+
|
|
29
26
|
try:
|
|
30
|
-
content =
|
|
27
|
+
content = Path(abs_path).read_text(encoding="utf-8")
|
|
31
28
|
|
|
32
29
|
# 1. Snippet (first 20 lines)
|
|
33
30
|
lines = content.splitlines()
|
|
@@ -35,21 +32,20 @@ def _extract_file_context(file_path: str, extension: str, root_dir: str) -> tupl
|
|
|
35
32
|
if len(lines) > 20:
|
|
36
33
|
snippet += "\n..."
|
|
37
34
|
|
|
38
|
-
# 2. Explanation
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
# 2 & 3. Explanation and Outline from Tree-Sitter
|
|
36
|
+
skeleton = extract_skeleton(abs_path)
|
|
37
|
+
|
|
38
|
+
if "error" not in skeleton:
|
|
39
|
+
# Grab the first docstring if it exists
|
|
40
|
+
if skeleton["docstrings"]:
|
|
41
|
+
explanation = list(skeleton["docstrings"].values())[0]
|
|
42
|
+
|
|
43
|
+
# Collect classes and functions
|
|
44
|
+
for cls in skeleton["classes"]:
|
|
45
|
+
outline.append(cls["name"])
|
|
46
|
+
for fn in skeleton["functions"]:
|
|
47
|
+
outline.append(fn["name"])
|
|
48
48
|
|
|
49
|
-
# 3. Outline
|
|
50
|
-
for match in OUTLINE_PATTERN.finditer(content):
|
|
51
|
-
outline.append(match.group(1))
|
|
52
|
-
|
|
53
49
|
except Exception:
|
|
54
50
|
pass
|
|
55
51
|
|
package/python/query_engine.py
CHANGED
|
@@ -296,48 +296,37 @@ def query(
|
|
|
296
296
|
return []
|
|
297
297
|
|
|
298
298
|
# --- Pre-computation: Global TF-IDF & BM25 ---
|
|
299
|
-
import pathlib
|
|
300
299
|
total_files = len(repo_graph.nodes)
|
|
301
300
|
document_frequency = {term: 0 for term in query_terms}
|
|
302
301
|
|
|
303
|
-
file_contents = {}
|
|
304
|
-
doc_lengths = {}
|
|
305
302
|
total_tokens_count = 0
|
|
306
303
|
|
|
307
304
|
for path, node in repo_graph.nodes.items():
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
305
|
+
content = node.content
|
|
306
|
+
dl = node.doc_length
|
|
307
|
+
total_tokens_count += dl
|
|
308
|
+
|
|
309
|
+
raw_tokens = re.findall(r"[a-z0-9]+", content.lower())
|
|
310
|
+
content_tokens = set(raw_tokens)
|
|
311
|
+
|
|
312
|
+
if path.endswith(".java"):
|
|
313
|
+
camel_tokens = []
|
|
314
|
+
for word in re.findall(r"[a-zA-Z]+", content):
|
|
315
|
+
if any(c.isupper() for c in word):
|
|
316
|
+
sub = re.sub(r"([A-Z])", r" \1", word).lower()
|
|
317
|
+
camel_tokens.extend(re.findall(r"[a-z0-9]+", sub))
|
|
318
|
+
content_tokens.update([t for t in camel_tokens if len(t) > 1])
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
for term in query_terms:
|
|
331
|
-
if term in combined_tokens:
|
|
332
|
-
document_frequency[term] += 1
|
|
333
|
-
elif len(term) > 3:
|
|
334
|
-
for ct in combined_tokens:
|
|
335
|
-
if term in ct:
|
|
336
|
-
document_frequency[term] += 1
|
|
337
|
-
break
|
|
338
|
-
except Exception:
|
|
339
|
-
file_contents[path] = ""
|
|
340
|
-
doc_lengths[path] = 0
|
|
320
|
+
path_tokens = set(_path_tokens(path))
|
|
321
|
+
combined_tokens = content_tokens.union(path_tokens)
|
|
322
|
+
for term in query_terms:
|
|
323
|
+
if term in combined_tokens:
|
|
324
|
+
document_frequency[term] += 1
|
|
325
|
+
elif len(term) > 3:
|
|
326
|
+
for ct in combined_tokens:
|
|
327
|
+
if term in ct:
|
|
328
|
+
document_frequency[term] += 1
|
|
329
|
+
break
|
|
341
330
|
|
|
342
331
|
avgdl = total_tokens_count / total_files if total_files > 0 else 1.0
|
|
343
332
|
|
|
@@ -357,8 +346,8 @@ def query(
|
|
|
357
346
|
|
|
358
347
|
for path, node in repo_graph.nodes.items():
|
|
359
348
|
kscore, kterms = _keyword_score(query_terms, path, idf_weights)
|
|
360
|
-
dl =
|
|
361
|
-
cscore, cterms = _content_score(query_terms, path,
|
|
349
|
+
dl = node.doc_length
|
|
350
|
+
cscore, cterms = _content_score(query_terms, path, node.content, idf_weights, dl, avgdl)
|
|
362
351
|
|
|
363
352
|
keyword_scores[path] = kscore
|
|
364
353
|
content_scores[path] = cscore
|
|
@@ -373,10 +362,10 @@ def query(
|
|
|
373
362
|
|
|
374
363
|
base_scores[path] = combined
|
|
375
364
|
|
|
376
|
-
# --- Pass
|
|
365
|
+
# --- Pass 2: neighbor bonus ---
|
|
377
366
|
boosted_scores = _apply_neighbor_bonus(base_scores, repo_graph)
|
|
378
367
|
|
|
379
|
-
# --- Pass
|
|
368
|
+
# --- Pass 3: centrality tiebreaker ---
|
|
380
369
|
results = []
|
|
381
370
|
for path, node in repo_graph.nodes.items():
|
|
382
371
|
neighbor_bonus = boosted_scores[path] - base_scores[path]
|
package/python/scanner.py
CHANGED
|
@@ -3,7 +3,7 @@ Repository Intelligence Engine
|
|
|
3
3
|
Step 1: Repository Scanner
|
|
4
4
|
|
|
5
5
|
Walks a repository and discovers all relevant source files.
|
|
6
|
-
Filters by supported extensions
|
|
6
|
+
Filters by supported extensions across 9 language families.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
@@ -17,7 +17,8 @@ SUPPORTED_EXTENSIONS = {".tsx", ".ts", ".jsx", ".js", ".py", ".java", ".go", ".r
|
|
|
17
17
|
ignore_dirs = {
|
|
18
18
|
".git", "node_modules", "__pycache__", "venv", ".venv",
|
|
19
19
|
"env", ".env", ".next", ".nuxt", "out", "build", "dist",
|
|
20
|
-
"coverage", ".nyc_output", ".pytest_cache", "npm", "pypi"
|
|
20
|
+
"coverage", ".nyc_output", ".pytest_cache", "npm", "pypi",
|
|
21
|
+
"vendor", "target", ".cargo", "Pods", "third_party"
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
def _should_ignore(path: str) -> bool:
|