Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`.

**Test Verification Baseline:** **1207 Automated Tests** (843 Pytest Backend + 314 Vitest Frontend + 50 Playwright E2E).
**Test Verification Baseline:** **1216 Automated Tests** (852 Pytest Backend + 314 Vitest Frontend + 50 Playwright E2E).

---

Expand Down Expand Up @@ -1178,6 +1178,17 @@ and leaves the prior indexed state intact without data loss._
- `test_summarizer_client_sends_extra_headers` - _Verifies that SummarizerService builds its real OpenAI client from SQLite settings and refreshes on rotation._
- `test_embeddings_api_client_sends_extra_headers` - _Verifies that API embedding initialization configures OpenAI with persisted settings and extra headers._

#### `tests/test_local_source_relative_paths.py` (9 tests)
- `test_relative_maps_stored_paths`
- `test_nested_roots_use_the_longest_root_and_file_roots_use_the_file_name`
- `test_clean_stored_path_keeps_the_git_form`
- `test_find_symbol_returns_relative_paths`
- `test_get_file_outline_returns_the_relative_path`
- `test_find_routes_and_api_callers_return_relative_paths`
- `test_trace_path_returns_relative_paths`
- `test_diagnose_issue_parts_return_relative_paths`
- `test_resolved_stack_frames_return_relative_paths`

#### `tests/test_local_storage_indexing.py` (4 tests)
- `test_incremental_indexing_on_save`
- `test_incremental_indexing_code_file`
Expand Down
7 changes: 5 additions & 2 deletions app/mcp/handlers/route_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from app.services.database import get_db_connection
from app.services.git_manager import format_git_permalink
from app.services.chunking import match_route_and_call
from app.services.source_paths import LocalSourceRoots

logger = logging.getLogger("contextcortex")

Expand Down Expand Up @@ -64,11 +65,12 @@ async def handle_find_routes(

sql += " ORDER BY r.repo ASC, r.path_pattern ASC, r.http_method ASC, r.start_line ASC"
rows = conn.execute(sql, params).fetchall()
local_roots = LocalSourceRoots.load(conn)

all_routes = []
seen_routes = set()
for r in rows:
rel_path = (r["filepath"].split("://", 1)[1] if "://" in r["filepath"] else r["filepath"]).replace("\\", "/")
rel_path = local_roots.relative(r["filepath"], r["repo"])
if _is_test_route_file(rel_path):
continue
dedup_key = (r["repo"], r["http_method"], r["path_pattern"], rel_path, r["start_line"])
Expand Down Expand Up @@ -146,6 +148,7 @@ async def handle_find_api_callers(

sql += " ORDER BY c.repo ASC, c.filepath ASC, c.line_number ASC"
rows = conn.execute(sql, params).fetchall()
local_roots = LocalSourceRoots.load(conn)

target_method_clean = method.strip().upper() if method and method.strip() else None
all_callers = []
Expand All @@ -159,7 +162,7 @@ async def handle_find_api_callers(
if not match_route_and_call(target_path, c["url_pattern"]):
continue

rel_path = (c["filepath"].split("://", 1)[1] if "://" in c["filepath"] else c["filepath"]).replace("\\", "/")
rel_path = local_roots.relative(c["filepath"], c["repo"])
dedup_key = (c["repo"], rel_path, c["http_method"], c["url_pattern"], c["caller_symbol"], c["line_number"])
if dedup_key in seen_callers:
continue
Expand Down
19 changes: 13 additions & 6 deletions app/mcp/handlers/symbol_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from app.services.database import get_db_connection
from app.services.search import trace_symbol_path
from app.services.source_paths import LocalSourceRoots

logger = logging.getLogger("contextcortex")

Expand Down Expand Up @@ -107,11 +108,12 @@ async def handle_find_symbol(

query += " ORDER BY repo ASC, filepath ASC, start_line ASC, name ASC"
rows = conn.execute(query, params).fetchall()
local_roots = LocalSourceRoots.load(conn)

all_symbols = []
seen_syms = set()
for r in rows:
rel_file = _strip_repo_prefix(r["filepath"])
rel_file = local_roots.relative(r["filepath"], r["repo"])
sym_full = r["full_symbol"] or r["name"]
dedup_key = (r["repo"], rel_file, r["name"], sym_full, r["kind"], r["start_line"], r["end_line"])
if dedup_key in seen_syms:
Expand Down Expand Up @@ -266,6 +268,11 @@ async def handle_get_file_outline(
candidates: List[str] = []
matched_file = _strip_repo_prefix(fp)
with _get_tools_attr("get_db_connection", get_db_connection)() as conn:
local_roots = LocalSourceRoots.load(conn)

def _rel(row) -> str:
return local_roots.relative(row["filepath"], row["repo"])

def _query_outline_rows(target_clean: str):
if repo:
return conn.execute(
Expand Down Expand Up @@ -298,11 +305,11 @@ def _query_outline_rows(target_clean: str):
rows = _query_outline_rows(clean_fp)
if rows:
distinct_files = sorted({
_strip_repo_prefix(r["filepath"]) for r in rows if r["filepath"]
_rel(r) for r in rows if r["filepath"]
})
if len(distinct_files) > 1:
chosen_file = clean_fp if clean_fp in distinct_files else distinct_files[0]
rows = [r for r in rows if _strip_repo_prefix(r["filepath"]) == chosen_file]
rows = [r for r in rows if _rel(r) == chosen_file]
matched_file = chosen_file
candidates = distinct_files

Expand All @@ -313,14 +320,14 @@ def _query_outline_rows(target_clean: str):
cand_rows = _query_outline_rows(suffix)
if cand_rows:
distinct_files = sorted({
_strip_repo_prefix(r["filepath"]) for r in cand_rows if r["filepath"]
_rel(r) for r in cand_rows if r["filepath"]
})
if len(distinct_files) == 1:
rows = cand_rows
matched_file = distinct_files[0]
break
if suffix in distinct_files:
rows = [r for r in cand_rows if _strip_repo_prefix(r["filepath"]) == suffix]
rows = [r for r in cand_rows if _rel(r) == suffix]
matched_file = suffix
candidates = distinct_files
break
Expand All @@ -329,7 +336,7 @@ def _query_outline_rows(target_clean: str):

resolved_repo = repo or (rows[0]["repo"] if rows and "repo" in rows[0].keys() else None)
if rows and rows[0]["filepath"]:
matched_file = _strip_repo_prefix(rows[0]["filepath"])
matched_file = _rel(rows[0])

all_symbols = []
seen_syms = set()
Expand Down
22 changes: 16 additions & 6 deletions app/services/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.services.file_reader import get_file_reader_service
from app.services.git_manager import format_git_permalink
from app.services.search import execute_hybrid_search
from app.services.source_paths import LocalSourceRoots
from app.services.stack_trace_parser import (
RE_COBOL_ABEND_FRAME, RE_COBOL_DIAG_FRAME, RE_COBOL_PARAGRAPH_FRAME,
RE_CSHARP_FRAME, RE_EXCEPTION_NAME, RE_GENERIC_FRAME, RE_HTTP_ROUTE,
Expand All @@ -26,6 +27,11 @@
MAX_MATCHED_SYMBOLS = 20


def _diag_file(roots: LocalSourceRoots, filepath: Optional[str]) -> str:
"""Output path for a stored filepath: relative to its local-source root, else the cleaned diagnostic path."""
return _clean_diag_path(roots.strip_root(filepath) or filepath)


def _cobol_alt_filenames(filename: str) -> List[str]:
lower = filename.lower()
stem, dot, ext = lower.rpartition(".")
Expand Down Expand Up @@ -61,10 +67,11 @@ def _get_callers_and_callees(
c_sql += " AND repo = ?"
c_params.append(effective_repo)
c_rows = conn.execute(c_sql + " LIMIT 80", c_params).fetchall()
roots = LocalSourceRoots.load(conn)

scored_callers, seen_callers = [], set()
for r in c_rows:
clean_fp = _clean_diag_path(r["source_filepath"])
clean_fp = _diag_file(roots, r["source_filepath"])
dedup_k = (r["repo"], r["source_symbol"], clean_fp, r["line_number"])
if dedup_k in seen_callers:
continue
Expand Down Expand Up @@ -100,7 +107,7 @@ def _get_callers_and_callees(

callees, seen_callees = [], set()
for r in ce_rows:
clean_fp = _clean_diag_path(r["source_filepath"])
clean_fp = _diag_file(roots, r["source_filepath"])
dedup_k = (r["target_symbol"], r["line_number"])
if dedup_k in seen_callees:
continue
Expand Down Expand Up @@ -195,11 +202,12 @@ def resolve_stack_frames(
r["name"]: dict(r)
for r in conn.execute("SELECT name, url, commit_sha, provider FROM git_repositories").fetchall()
}
roots = LocalSourceRoots.load(conn)
for frame in frames[:MAX_RESOLVED_FRAMES]:
raw_path, line_no = frame["path"], int(frame["line"])
matched_file = _find_matching_indexed_file(conn, repo, raw_path)
file_path = matched_file["filepath"] if matched_file else raw_path
clean_file_path = _clean_diag_path(file_path)
clean_file_path = _diag_file(roots, file_path)
target_repo = matched_file["repo"] if matched_file else (repo or "unknown")

enclosing = _find_symbol_enclosing(conn, target_repo, file_path, line_no)
Expand Down Expand Up @@ -270,6 +278,7 @@ def match_routes_and_callers(
+ (" WHERE repo = ?" if repo else ""),
([repo] if repo else []),
).fetchall()
roots = LocalSourceRoots.load(conn)

for method, path in extracted_routes:
norm_target = normalize_path_pattern(path)
Expand All @@ -282,7 +291,7 @@ def match_routes_and_callers(
continue
seen.add(route_key)
raw_ext_callers = [
{"repo": c["repo"], "symbol": c["caller_symbol"], "file": _clean_diag_path(c["filepath"]), "line": c["line_number"]}
{"repo": c["repo"], "symbol": c["caller_symbol"], "file": _diag_file(roots, c["filepath"]), "line": c["line_number"]}
for c in all_calls
if match_route_and_call(r["path_pattern"], c["url_pattern"])
]
Expand All @@ -292,7 +301,7 @@ def match_routes_and_callers(
"path": r["path_pattern"],
"handler": {
"repo": r["repo"], "symbol": r["handler_symbol"],
"file": _clean_diag_path(r["filepath"]), "line": r["start_line"],
"file": _diag_file(roots, r["filepath"]), "line": r["start_line"],
},
"external_callers": raw_ext_callers[:10],
})
Expand All @@ -315,6 +324,7 @@ def match_symbols_and_exceptions(
return matched

with get_db_connection() as conn:
roots = LocalSourceRoots.load(conn)
for term in terms:
short_term = term.split(".")[-1] if "." in term else term
sql = (
Expand All @@ -334,7 +344,7 @@ def match_symbols_and_exceptions(
),
)
for r in rows_sorted[:5]:
clean_fp = _clean_diag_path(r["filepath"])
clean_fp = _diag_file(roots, r["filepath"])
if _is_test_path(clean_fp):
continue
key = (r["repo"], r["name"], clean_fp, r["start_line"])
Expand Down
18 changes: 10 additions & 8 deletions app/services/graph_traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.services.chunking import match_route_and_call
from app.services.database import get_db_connection
from app.services.git_manager import format_git_permalink
from app.services.source_paths import LocalSourceRoots

_DIRECTION_ALIASES = {
"inbound": "callers",
Expand Down Expand Up @@ -120,10 +121,11 @@ def trace_symbol_path(
git_repos = {r["name"]: dict(r) for r in conn.execute("SELECT name, url, commit_sha, provider FROM git_repositories").fetchall()}
all_routes = [dict(r) for r in conn.execute("SELECT id, repo, filepath, framework, http_method, path_pattern, handler_symbol, start_line, end_line FROM api_routes").fetchall()]
all_calls = [dict(c) for c in conn.execute("SELECT id, repo, filepath, http_method, url_pattern, caller_symbol, line_number FROM api_client_calls").fetchall()]
_rel_path = LocalSourceRoots.load(conn).relative

def get_permalink(r_repo: str, r_filepath: str, start_l: Optional[int], end_l: Optional[int]) -> Optional[str]:
g_info = git_repos.get(r_repo)
return _fmt_permalink(g_info["url"], g_info["commit_sha"], _clean_path(r_filepath), start_l, end_l, provider=g_info.get("provider")) if g_info else None
return _fmt_permalink(g_info["url"], g_info["commit_sha"], _rel_path(r_filepath), start_l, end_l, provider=g_info.get("provider")) if g_info else None

bfs_repo = repo or (symbols_found[0]["repo"] if symbols_found else None)
bfs_clause = " WHERE repo = ?" if bfs_repo else ""
Expand Down Expand Up @@ -151,7 +153,7 @@ def get_permalink(r_repo: str, r_filepath: str, start_l: Optional[int], end_l: O
class_signatures: Dict[str, str] = {}

for s in repo_symbols:
s["_clean_fp"] = _clean_path(s.get("filepath"))
s["_clean_fp"] = _rel_path(s.get("filepath"))
full_s, name_s = s.get("full_symbol"), s.get("name")
if full_s:
syms_by_full.setdefault(full_s, []).append(s)
Expand Down Expand Up @@ -255,7 +257,7 @@ def _resolve_enclosing_symbol(r_repo: str, r_filepath: str, line_num: Optional[i
if source_symbol_id and source_symbol_id in sym_by_id:
s_obj = sym_by_id[source_symbol_id]
return (s_obj.get("full_symbol") or s_obj.get("name") or fallback_sym), s_obj
file_syms = syms_by_file.get((r_repo, _clean_path(r_filepath)), [])
file_syms = syms_by_file.get((r_repo, _rel_path(r_filepath)), [])
if line_num is not None and file_syms:
enclosing = [
s for s in file_syms
Expand Down Expand Up @@ -283,7 +285,7 @@ def resolve_http_boundaries(sym_name: str, sym_repo: str):
for r_item in matched_routes:
for c in all_calls:
if c["repo"] != sym_repo and _methods_compatible(r_item["http_method"], c.get("http_method")) and match_route_and_call(r_item["path_pattern"], c.get("url_pattern", "")):
entry = {"repo": c["repo"], "symbol": c.get("caller_symbol") or "", "file": _clean_path(c["filepath"]), "line": c.get("line_number"), "method": c.get("http_method", "").upper(), "url": c.get("url_pattern", "")}
entry = {"repo": c["repo"], "symbol": c.get("caller_symbol") or "", "file": _rel_path(c["filepath"]), "line": c.get("line_number"), "method": c.get("http_method", "").upper(), "url": c.get("url_pattern", "")}
if entry not in ext_callers:
ext_callers.append(entry)

Expand All @@ -297,15 +299,15 @@ def resolve_http_boundaries(sym_name: str, sym_repo: str):
ext_handlers = []
for r in all_routes:
if r["repo"] != sym_repo and _methods_compatible(r.get("http_method"), c_method) and match_route_and_call(r.get("path_pattern", ""), c_url):
h_entry = {"repo": r["repo"], "symbol": r.get("handler_symbol") or "", "file": _clean_path(r["filepath"]), "line": r.get("start_line"), "path": r.get("path_pattern")}
h_entry = {"repo": r["repo"], "symbol": r.get("handler_symbol") or "", "file": _rel_path(r["filepath"]), "line": r.get("start_line"), "path": r.get("path_pattern")}
if h_entry not in ext_handlers:
ext_handlers.append(h_entry)
http_calls.append({"method": c_method, "url": c_url, "line": c.get("line_number"), "external_handlers": ext_handlers})
return route_meta, ext_callers, http_calls

if symbols_found:
s0 = symbols_found[0]
s_file = _clean_path(s0["filepath"])
s_file = _rel_path(s0["filepath"])
target_info = {
"symbol": s0["full_symbol"] or s0["name"], "name": s0["name"], "kind": s0["kind"],
"repo": s0["repo"], "file": s_file,
Expand All @@ -320,7 +322,7 @@ def resolve_http_boundaries(sym_name: str, sym_repo: str):
if not row:
row = conn.execute(f"SELECT repo, source_filepath, line_number FROM ast_relationships WHERE target_symbol = ? {r_filter}LIMIT 1", r_args).fetchone()
t_repo = row["repo"] if row else (bfs_repo or "")
t_file = _clean_path(row["source_filepath"]) if row else ""
t_file = _rel_path(row["source_filepath"]) if row else ""
t_line = row["line_number"] if row else 1
target_info = {
"symbol": target_sym, "name": target_sym.split(".")[-1], "kind": "function",
Expand All @@ -337,7 +339,7 @@ def resolve_http_boundaries(sym_name: str, sym_repo: str):
target_info["http_calls"] = t_http_calls

def _make_edge_node(display_sym: str, row_obj: object, child_key: str) -> dict:
clean_fp = _clean_path(row_obj["source_filepath"])
clean_fp = _rel_path(row_obj["source_filepath"])
r_route, r_ext_callers, r_http_calls = resolve_http_boundaries(display_sym, row_obj["repo"])
node = {
"symbol": display_sym,
Expand Down
Loading
Loading