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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: Test repository import boundaries
run: PYTHONPATH=backend python -m unittest discover -s backend/tests -v

- name: Check catalog retrieval evidence
run: PYTHONPATH=backend python examples/evaluate_retrieval.py --check

frontend:
runs-on: ubuntu-latest
strategy:
Expand Down
85 changes: 66 additions & 19 deletions backend/services/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,54 +76,101 @@ def search_code(root,q,limit=50):
if len(out)>=limit:return out
return out

def _context_excerpt(text, path, tokens, budget):
def _context_excerpt(text, path, tokens, budget, evidence=None):
prefix = text[:budget]
if not tokens or len(text) <= budget:
return prefix
visible = (path + "\n" + prefix).casefold()
if any(token in visible for token in tokens):
return prefix
if evidence is not None:
source_start, source_end = evidence
if source_end <= budget:
return prefix
else:
visible = (path + "\n" + prefix).casefold()
if any(token in visible for token in tokens):
return prefix

folded = text.casefold()
matches = [(pos, token) for token in tokens if (pos := folded.find(token)) >= 0]
if not matches:
return prefix
position, token = min(matches)
source_start = _source_offset(text, position)
source_end = _source_offset(text, position + len(token) - 1) + 1
folded = text.casefold()
matches = [(pos, token) for token in tokens if (pos := folded.find(token)) >= 0]
if not matches:
return prefix
position, token = min(matches)
source_start = _source_offset(text, position)
source_end = _source_offset(text, position + len(token) - 1) + 1
spare = max(0, budget - (source_end - source_start))
marker = "..." if spare >= 3 else ""
start = max(0, source_start - min(160, spare - len(marker)))
start = max(0, source_start - min(160, (spare - len(marker)) // 2))
if start == 0:
return prefix
return marker + text[start:start + budget - len(marker)]


_DEFINITION_QUESTION = re.compile(
r"where\s+is\s+(?:the\s+)?`?(?P<symbol>[^\W\d]\w*)(?:\(\))?`?"
r"\s+(?:defined|declared)\s*\??", re.IGNORECASE,
)


def build_context(root,q,max_files=8,max_chars=18000):
if max_files < 1 or max_chars < 1:
return "No context budget available."
tokens={x for x in re.findall(r"[\w-]+", q.casefold()) if len(x)>2}; ranked=[]
definition = _DEFINITION_QUESTION.fullmatch(q.strip())
declaration = None
if definition:
symbol = definition["symbol"].casefold()
tokens = {symbol}
exact_symbol = re.compile(rf"\b{re.escape(symbol)}\b")
declaration = re.compile(
rf"^[ \t]*(?:async[ \t]+)?(?:def|class)[ \t]+(?P<name>{re.escape(symbol)})\b",
re.MULTILINE | re.IGNORECASE,
)
else:
tokens = {x for x in re.findall(r"[\w-]+", q.casefold()) if len(x) > 2}
ranked = []
for p in iter_files(root):
text=read_text(p); rel=str(p.relative_to(root)).replace("\\","/")
low=(rel+"\n"+text).casefold()
evidence = None
if definition:
folded = text.casefold()
reference = exact_symbol.search(folded)
if reference:
evidence = (
_source_offset(text, reference.start()),
_source_offset(text, reference.end() - 1) + 1,
)
elif not exact_symbol.search(rel.casefold()):
continue
early=(rel+"\n"+text[:4000]).casefold()
matched=2*sum(t in early for t in tokens)+sum(t in low for t in tokens)
bonus=sum(k in rel.casefold() for k in ["readme","app.","main.","index.","route","api"])
if matched or (not q.strip() and bonus): ranked.append((matched,bonus,rel,text))
if declaration and p.suffix.lower() == ".py":
found = declaration.search(text)
if found:
# One identifier can score at most three points as a reference.
matched += 4
evidence = found.span("name")
if matched or (not q.strip() and bonus):
ranked.append((matched, bonus, rel, text, evidence))
ranked.sort(key=lambda x: (-x[0], -x[1], x[2].casefold(), x[2])); out=""; used=0; selected=0
for _, _, rel, text in ranked:
displayed = _context_excerpt(text, rel, tokens, 4000)
block=f"\n--- FILE: {rel} ---\n{displayed}\n"
for _, _, rel, text, evidence in ranked:
header = f"\n--- FILE: {rel} ---\n"
budget = 4000
if evidence:
budget = min(budget, max_chars - used - len(header) - 1)
if budget < 1:
continue
displayed = _context_excerpt(text, rel, tokens, budget, evidence)
block = header + displayed + "\n"
if used+len(block)>max_chars: continue
out+=block; used+=len(block); selected+=1
if selected >= max_files:
break
if out:
return out
if ranked:
_,_,rel,text = ranked[0]
_, _, rel, text, evidence = ranked[0]
header = f"\n--- FILE: {rel} ---\n"
room = max_chars - len(header) - 1
if room > 0:
return header + _context_excerpt(text, rel, tokens, room) + "\n"
return header + _context_excerpt(text, rel, tokens, room, evidence) + "\n"
return "No strongly matching files were found. Say that the supplied context is insufficient."
27 changes: 27 additions & 0 deletions backend/tests/test_analyzer_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,33 @@ def test_tight_context_budget_retains_deep_evidence(self):
self.assertIn("target_symbol", context)
self.assertLessEqual(len(context), budget)

def test_definition_lookup_abstains_on_partial_names_and_question_words(self):
self.write("api.py", "# where the methods are defined\ndef recorded_item(): pass")
result = build_context(self.root, "Where is the `item()` defined?")
self.assertIn("No strongly matching files", result)

def test_definition_lookup_accepts_short_identifiers(self):
self.write("store.py", "class DB: pass")
result = build_context(self.root, "Where is db declared?")
self.assertIn("class DB", result)

def test_definition_lookup_prefers_the_declaration_over_imports_and_examples(self):
self.write("api.py", "from worker import process_job")
self.write("README.md", "def process_job(): ...")
self.write("worker.py", "# process_job\n" + "#" * 6000 + "\nasync def process_job(): pass")
for budget in (18000, 160):
with self.subTest(budget=budget):
context = build_context(self.root, "Where is process_job defined?", 1, budget)
self.assertIn("--- FILE: worker.py", context)
self.assertIn("async def process_job", context)
self.assertLessEqual(len(context), budget)

def test_reference_excerpt_contains_the_whole_identifier(self):
self.write("ops.js", "const db_name = 'archive';\n" + "/" * 6000 + "\nopen(db);")
context = build_context(self.root, "Where is db defined?", max_chars=180)
self.assertIn("open(db)", context)
self.assertNotIn("db_name", context)

def test_context_ties_are_ordered_by_path(self):
self.write("zeta.py", "shared_keyword = 1\n")
self.write("alpha.py", "shared_keyword = 2\n")
Expand Down
Loading