technophyle commited on
Commit
d3dfd51
·
verified ·
1 Parent(s): cc555b9

Sync from GitHub via hub-sync

Browse files
evals/run_eval.py CHANGED
@@ -76,9 +76,19 @@ def matches_expected(actual_path: str, expected_sources) -> bool:
76
 
77
 
78
  def compute_retrieval_metrics(expected_sources, actual_sources):
79
- hit = any(matches_expected(path, expected_sources) for path in actual_sources)
80
- top1 = bool(actual_sources) and matches_expected(actual_sources[0], expected_sources)
81
- return {"retrieval_hit": int(hit), "top1_hit": int(top1)}
 
 
 
 
 
 
 
 
 
 
82
 
83
 
84
  def keyword_hits(answer: str, keywords):
@@ -97,7 +107,7 @@ def judge_faithfulness(rag_system, question: str, answer: str, sources: list):
97
  if not ENABLE_FAITHFULNESS or not answer.strip() or not sources:
98
  return None
99
  context = "\n\n".join(
100
- f"[{i}] {source['file_path']}\n{source['snippet'][:800]}"
101
  for i, source in enumerate(sources, start=1)
102
  )
103
  system_prompt = (
@@ -141,6 +151,7 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
141
  question=case["question"],
142
  top_k=TOP_K,
143
  history=case.get("turns", []),
 
144
  )
145
  elapsed_ms = (time.time() - start) * 1000
146
 
@@ -149,7 +160,18 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
149
  retrieval = compute_retrieval_metrics(case.get("expected_sources", []), cited_paths)
150
  matched, total_keywords = keyword_hits(result.get("answer", ""), case.get("must_include_any", []))
151
  has_citations = bool(result.get("citations"))
152
- grounded = retrieval["retrieval_hit"] == 1 and has_citations and (total_keywords == 0 or matched > 0)
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  return {
155
  "id": case.get("id", case["question"]),
@@ -162,7 +184,11 @@ def run_case(rag_system, repo_id: int, repo_name: str, case: dict):
162
  "retrieved_sources": cited_paths,
163
  "retrieval_hit": retrieval["retrieval_hit"],
164
  "top1_hit": retrieval["top1_hit"],
165
- "grounded": int(grounded),
 
 
 
 
166
  "faithfulness": judge_faithfulness(rag_system, case["question"], result.get("answer", ""), sources),
167
  "latency_ms": round(elapsed_ms, 1),
168
  }
@@ -178,6 +204,11 @@ def summarize(details):
178
  "case_count": len(details),
179
  "retrieval_hit_rate": round(mean(item["retrieval_hit"] for item in details), 4),
180
  "top1_hit_rate": round(mean(item["top1_hit"] for item in details), 4),
 
 
 
 
 
181
  "grounded_answer_rate": round(mean(item["grounded"] for item in details), 4),
182
  "faithfulness": round(mean(faith_scores), 4) if faith_scores else None,
183
  "latency_p95_ms": round(latencies[p95_index], 1),
 
76
 
77
 
78
  def compute_retrieval_metrics(expected_sources, actual_sources):
79
+ matching_ranks = [
80
+ rank
81
+ for rank, path in enumerate(actual_sources, start=1)
82
+ if matches_expected(path, expected_sources)
83
+ ]
84
+ hit = bool(matching_ranks)
85
+ top1 = bool(matching_ranks and matching_ranks[0] == 1)
86
+ reciprocal_rank = 1.0 / matching_ranks[0] if matching_ranks else 0.0
87
+ return {
88
+ "retrieval_hit": int(hit),
89
+ "top1_hit": int(top1),
90
+ "reciprocal_rank": reciprocal_rank,
91
+ }
92
 
93
 
94
  def keyword_hits(answer: str, keywords):
 
107
  if not ENABLE_FAITHFULNESS or not answer.strip() or not sources:
108
  return None
109
  context = "\n\n".join(
110
+ f"[{i}] {source['file_path']}\n{source['snippet'][:1500]}"
111
  for i, source in enumerate(sources, start=1)
112
  )
113
  system_prompt = (
 
151
  question=case["question"],
152
  top_k=TOP_K,
153
  history=case.get("turns", []),
154
+ debug_retrieval=True,
155
  )
156
  elapsed_ms = (time.time() - start) * 1000
157
 
 
160
  retrieval = compute_retrieval_metrics(case.get("expected_sources", []), cited_paths)
161
  matched, total_keywords = keyword_hits(result.get("answer", ""), case.get("must_include_any", []))
162
  has_citations = bool(result.get("citations"))
163
+ expected_source_grounded = (
164
+ retrieval["retrieval_hit"] == 1
165
+ and has_citations
166
+ and (total_keywords == 0 or matched > 0)
167
+ )
168
+
169
+ retrieval_debug = result.get("retrieval_debug", [])
170
+ for item in retrieval_debug:
171
+ item["expected_source"] = matches_expected(
172
+ item.get("file_path", ""),
173
+ case.get("expected_sources", []),
174
+ )
175
 
176
  return {
177
  "id": case.get("id", case["question"]),
 
184
  "retrieved_sources": cited_paths,
185
  "retrieval_hit": retrieval["retrieval_hit"],
186
  "top1_hit": retrieval["top1_hit"],
187
+ "reciprocal_rank": round(retrieval["reciprocal_rank"], 4),
188
+ "expected_source_grounded": int(expected_source_grounded),
189
+ # Backward-compatible alias for older report consumers.
190
+ "grounded": int(expected_source_grounded),
191
+ "retrieval_debug": retrieval_debug,
192
  "faithfulness": judge_faithfulness(rag_system, case["question"], result.get("answer", ""), sources),
193
  "latency_ms": round(elapsed_ms, 1),
194
  }
 
204
  "case_count": len(details),
205
  "retrieval_hit_rate": round(mean(item["retrieval_hit"] for item in details), 4),
206
  "top1_hit_rate": round(mean(item["top1_hit"] for item in details), 4),
207
+ "mrr": round(mean(item["reciprocal_rank"] for item in details), 4),
208
+ "expected_source_grounded_rate": round(
209
+ mean(item["expected_source_grounded"] for item in details), 4
210
+ ),
211
+ # Backward-compatible alias.
212
  "grounded_answer_rate": round(mean(item["grounded"] for item in details), 4),
213
  "faithfulness": round(mean(faith_scores), 4) if faith_scores else None,
214
  "latency_p95_ms": round(latencies[p95_index], 1),
evals/sample_eval_set.json CHANGED
@@ -17,31 +17,31 @@
17
  "id": "documenso-api-v2-document-router",
18
  "category": "api",
19
  "question": "Where is the current document API implemented and how is it exposed?",
20
- "ground_truth": "The current API V2 is implemented under packages/trpc/server, with document operations organized under a document router. It is exposed through /api/v2 and /api/v2-beta with tRPC and OpenAPI support, and it accepts API-token or session-cookie authentication depending on the route.",
21
  "expected_sources": ["ARCHITECTURE.md", "packages/trpc/server", "packages/trpc/server/document-router", "apps/remix/server"],
22
  "must_include_any": ["packages/trpc/server", "document-router", "API V2", "OpenAPI", "tRPC"]
23
  },
24
  {
25
  "id": "documenso-signing-package",
26
  "category": "specific-function",
27
- "question": "What does the signing package do in Documenso?",
28
  "ground_truth": "The @documenso/signing package owns PDF signing behavior. Its signPdf entry point selects a signing transport, applies timestamp authority settings when configured, and supports local P12 signing and Google Cloud KMS/HSM-backed signing through transport implementations.",
29
  "expected_sources": ["ARCHITECTURE.md", "packages/signing/index.ts", "packages/signing/helpers", "packages/signing/transports", ".env.example"],
30
- "must_include_any": ["PDF signing", "transports", "local", "Google", "KMS"]
31
  },
32
  {
33
  "id": "documenso-document-send-flow",
34
  "category": "cross-file",
35
  "question": "How does a document send operation flow across the Documenso codebase?",
36
- "ground_truth": "A document send operation starts at an API or UI route, goes through the API layer such as packages/trpc/server/document-router, delegates core behavior to packages/lib/server-only/document and related recipient or field logic, persists through packages/prisma, and can trigger emails or jobs through packages/email and packages/lib/jobs.",
37
  "expected_sources": ["packages/trpc/server/document-router", "packages/lib/server-only/document", "packages/lib/server-only/recipient", "packages/prisma", "packages/email", "packages/lib/jobs"],
38
- "must_include_any": ["document-router", "server-only/document", "prisma", "email", "jobs"]
39
  },
40
  {
41
  "id": "documenso-required-env",
42
  "category": "config-setup",
43
  "question": "Which environment variables are central to running a self-hosted Documenso instance?",
44
- "ground_truth": "The setup expects values such as NEXTAUTH_SECRET, NEXT_PRIVATE_ENCRYPTION_KEY, NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_DATABASE_URL, NEXT_PRIVATE_DIRECT_DATABASE_URL, NEXT_PRIVATE_SMTP_FROM_NAME, and NEXT_PRIVATE_SMTP_FROM_ADDRESS. The env example and self-hosting docs also cover internal URLs, signing transport, storage transport, and optional OAuth and webhook configuration.",
45
  "expected_sources": ["README.md", ".env.example", "apps/docs/content/docs/self-hosting/configuration/environment.mdx"],
46
  "must_include_any": ["NEXTAUTH_SECRET", "NEXT_PUBLIC_WEBAPP_URL", "DATABASE_URL", "SMTP", "encryption"]
47
  },
@@ -49,7 +49,7 @@
49
  "id": "documenso-playwright-tests",
50
  "category": "tests",
51
  "question": "Where does Documenso keep end-to-end app tests?",
52
- "ground_truth": "The architecture identifies @documenso/app-tests as the E2E test package, and the packages/app-tests directory is intended for Playwright coverage of app behavior.",
53
  "expected_sources": ["ARCHITECTURE.md", "packages/app-tests", "packages/app-tests/package.json"],
54
  "must_include_any": ["app-tests", "E2E", "Playwright"]
55
  },
@@ -57,7 +57,7 @@
57
  "id": "documenso-webhook-security-errors",
58
  "category": "error-handling",
59
  "question": "Where should you look for webhook security or SSRF-related safeguards?",
60
- "ground_truth": "Webhook safeguards belong in packages/lib/server-only/webhooks, with related configuration documented in .env.example such as NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS. These sources explain where outbound webhook behavior and security exceptions are controlled.",
61
  "expected_sources": [".env.example", "packages/lib/server-only/webhooks"],
62
  "must_include_any": ["webhook", "SSRF", "bypass", "hosts"]
63
  },
@@ -66,87 +66,12 @@
66
  "category": "conversation",
67
  "turns": [
68
  {"role": "user", "content": "How does Documenso seal or sign completed documents?"},
69
- {"role": "assistant", "content": "PDF completion crosses server-only PDF/document logic and the @documenso/signing package, which supports local and Google-backed signing transports."}
70
  ],
71
- "question": "show me where signing plugs in",
72
- "ground_truth": "The follow-up should retrieve packages/signing, especially the signPdf entry point, transports, and helpers, plus the seal-document job where completed PDFs are decorated and passed into signing, and signing-related env configuration.",
73
  "expected_sources": ["packages/signing/index.ts", "packages/signing/transports", "packages/signing/helpers", "packages/lib/jobs/definitions/internal/seal-document.handler.ts", ".env.example"],
74
- "must_include_any": ["packages/signing", "transports", "PDF", "signing"]
75
- }
76
- ]
77
- },
78
- {
79
- "id": "sqlite",
80
- "name": "SQLite",
81
- "github_url": "https://github.com/sqlite/sqlite.git",
82
- "cases": [
83
- {
84
- "id": "sqlite-purpose",
85
- "category": "architecture",
86
- "question": "What is SQLite and how is the project organized at a high level?",
87
- "ground_truth": "SQLite is a self-contained, serverless, zero-configuration embedded SQL database engine. Its core sources live under src/, tests are TCL scripts, the build produces both a library and a single-file amalgamation, and the project's own AGENTS.md documents the execution pipeline for contributors.",
88
- "expected_sources": ["README.md", "src", "AGENTS.md"],
89
- "must_include_any": ["embedded", "serverless", "amalgamation", "SQL database engine"]
90
- },
91
- {
92
- "id": "sqlite-query-pipeline",
93
- "category": "cross-file",
94
- "question": "How does a SQL statement flow through SQLite's execution pipeline?",
95
- "ground_truth": "SQL text is tokenized in tokenize.c, parsed by the Lemon-generated parser from parse.y, turned into VDBE bytecode by the code generator files (build.c, select.c, insert.c, update.c, delete.c, expr.c) after being optimized by the where*.c query optimizer, then executed by the VDBE in vdbe.c against the B-tree layer in btree.c, the pager in pager.c, the WAL in wal.c, and finally the OS-level VFS such as os_unix.c.",
96
- "expected_sources": ["src/tokenize.c", "src/parse.y", "src/vdbe.c", "src/btree.c", "src/pager.c", "src/wal.c", "AGENTS.md"],
97
- "must_include_any": ["tokenizer", "parser", "VDBE", "B-Tree", "pager", "WAL"]
98
- },
99
- {
100
- "id": "sqlite-btree-role",
101
- "category": "specific-function",
102
- "question": "What role does btree.c play in SQLite?",
103
- "ground_truth": "btree.c implements the B-tree storage engine SQLite uses to organize table and index data on disk. Its public interface is declared in btree.h, while btreeInt.h defines the data structures used only internally by the module.",
104
- "expected_sources": ["src/btree.c", "src/btree.h", "src/btreeInt.h"],
105
- "must_include_any": ["B-Tree", "storage engine", "btree.h"]
106
- },
107
- {
108
- "id": "sqlite-vdbe-opcodes",
109
- "category": "specific-function",
110
- "question": "How are VDBE opcodes generated and where do they come from?",
111
- "ground_truth": "VDBE opcode numbers and names are extracted automatically by scanning src/vdbe.c with the mkopcodeh.tcl script, which generates opcodes.h; a second script, mkopcodec.tcl, then generates opcodes.c, which provides the reverse opcode-to-name mapping used for EXPLAIN output.",
112
- "expected_sources": ["src/vdbe.c", "mkopcodeh.tcl", "mkopcodec.tcl"],
113
- "must_include_any": ["opcodes.h", "mkopcodeh.tcl", "VDBE", "EXPLAIN"]
114
- },
115
- {
116
- "id": "sqlite-parser-generation",
117
- "category": "config-setup",
118
- "question": "How is the SQL grammar parser built for SQLite?",
119
- "ground_truth": "The grammar is defined in src/parse.y and compiled into parse.c by the Lemon LALR(1) parser generator in tool/lemon.c, which uses tool/lempar.c as a template and also emits the parse.h header as a side effect.",
120
- "expected_sources": ["src/parse.y", "tool/lemon.c", "tool/lempar.c"],
121
- "must_include_any": ["parse.y", "Lemon", "LALR", "parse.c"]
122
- },
123
- {
124
- "id": "sqlite-testing",
125
- "category": "tests",
126
- "question": "How does SQLite run its test suite and what kind of tests does it use?",
127
- "ground_truth": "SQLite's tests are TCL scripts executed through the testfixture binary, which is built with make testfixture. AGENTS.md instructs contributors to run at least make devtest after any change under src/, and make sqlite3.c builds the amalgamation used for distribution.",
128
- "expected_sources": ["test", "AGENTS.md", "Makefile.in"],
129
- "must_include_any": ["testfixture", "TCL", "devtest", "test suite"]
130
- },
131
- {
132
- "id": "sqlite-build-amalgamation",
133
- "category": "config-setup",
134
- "question": "What is the SQLite amalgamation and how is it produced?",
135
- "ground_truth": "The amalgamation is the single-file distribution form of SQLite, sqlite3.c, assembled from the individual sources under src/ during the build. The public C API is declared in the src/sqlite.h.in template, which is expanded into the sqlite3.h header shipped with the amalgamation.",
136
- "expected_sources": ["src/sqlite.h.in", "Makefile.in", "AGENTS.md"],
137
- "must_include_any": ["amalgamation", "sqlite3.c", "sqlite.h.in"]
138
- },
139
- {
140
- "id": "sqlite-followup-wal",
141
- "category": "conversation",
142
- "turns": [
143
- {"role": "user", "content": "How does SQLite guarantee that transactions survive a crash?"},
144
- {"role": "assistant", "content": "Durability is enforced through the pager and, depending on journal mode, the write-ahead log."}
145
- ],
146
- "question": "which files implement that WAL behavior?",
147
- "ground_truth": "WAL-mode durability is implemented mainly in src/wal.c, working together with src/pager.c, which coordinates pager and journal behavior, and the OS-level VFS layer such as src/os_unix.c, which performs the actual fsync/durability calls.",
148
- "expected_sources": ["src/wal.c", "src/pager.c", "src/os_unix.c"],
149
- "must_include_any": ["wal.c", "pager.c", "fsync", "VFS"]
150
  }
151
  ]
152
  },
@@ -159,7 +84,7 @@
159
  "id": "fastapi-purpose",
160
  "category": "architecture",
161
  "question": "What is FastAPI and what is it built on top of?",
162
- "ground_truth": "FastAPI is a Python web framework for building APIs. It is built on top of Starlette for the web-facing parts and Pydantic for data validation and serialization, and it automatically generates an OpenAPI schema along with interactive Swagger UI and ReDoc documentation.",
163
  "expected_sources": ["README.md", "fastapi/applications.py", "pyproject.toml"],
164
  "must_include_any": ["Starlette", "Pydantic", "OpenAPI"]
165
  },
@@ -167,7 +92,7 @@
167
  "id": "fastapi-app-class",
168
  "category": "specific-function",
169
  "question": "What does the FastAPI application class do and where is it defined?",
170
- "ground_truth": "The central FastAPI class is defined in fastapi/applications.py. It ties together routing, dependency injection, middleware, exception handling, and OpenAPI schema generation for the whole application.",
171
  "expected_sources": ["fastapi/applications.py"],
172
  "must_include_any": ["applications.py", "routing", "OpenAPI"]
173
  },
@@ -175,7 +100,7 @@
175
  "id": "fastapi-routing",
176
  "category": "implementation",
177
  "question": "Where is path operation routing implemented in FastAPI?",
178
- "ground_truth": "Routing is implemented in fastapi/routing.py, which defines APIRoute and APIRouter. It handles path matching, resolves dependencies for each incoming request, and serializes the response for every registered endpoint.",
179
  "expected_sources": ["fastapi/routing.py"],
180
  "must_include_any": ["APIRoute", "APIRouter", "routing.py"]
181
  },
@@ -183,15 +108,15 @@
183
  "id": "fastapi-dependency-injection",
184
  "category": "cross-file",
185
  "question": "How does FastAPI resolve dependencies declared with Depends()?",
186
- "ground_truth": "Depends() and related parameter markers are defined in fastapi/params.py and fastapi/param_functions.py. The actual dependency tree resolution for each request happens in fastapi/dependencies/utils.py, which fastapi/routing.py calls while handling a request.",
187
  "expected_sources": ["fastapi/dependencies/utils.py", "fastapi/params.py", "fastapi/param_functions.py", "fastapi/routing.py"],
188
- "must_include_any": ["Depends", "dependencies/utils.py", "dependency"]
189
  },
190
  {
191
  "id": "fastapi-openapi-generation",
192
  "category": "api",
193
  "question": "How does FastAPI generate the OpenAPI schema and interactive docs?",
194
- "ground_truth": "The OpenAPI JSON schema is generated in fastapi/openapi/utils.py from the app's routes and Pydantic models. The Swagger UI and ReDoc HTML pages are served through helper functions such as get_swagger_ui_html and get_redoc_html in fastapi/openapi/docs.py.",
195
  "expected_sources": ["fastapi/openapi/utils.py", "fastapi/openapi/docs.py"],
196
  "must_include_any": ["OpenAPI", "Swagger", "ReDoc", "openapi/utils.py"]
197
  },
@@ -199,7 +124,7 @@
199
  "id": "fastapi-error-handling",
200
  "category": "error-handling",
201
  "question": "How does FastAPI turn validation failures and raised exceptions into HTTP responses?",
202
- "ground_truth": "FastAPI defines HTTPException and RequestValidationError in fastapi/exceptions.py. The default handlers that convert those exceptions into JSON error responses are registered in fastapi/exception_handlers.py.",
203
  "expected_sources": ["fastapi/exceptions.py", "fastapi/exception_handlers.py"],
204
  "must_include_any": ["HTTPException", "RequestValidationError", "exception_handlers.py"]
205
  },
@@ -207,7 +132,7 @@
207
  "id": "fastapi-security",
208
  "category": "specific-function",
209
  "question": "How does FastAPI support authentication schemes like OAuth2 and HTTP Bearer tokens?",
210
- "ground_truth": "Authentication helpers live under fastapi/security, which provides classes such as OAuth2PasswordBearer and HTTPBearer. They integrate with the dependency injection system and are automatically reflected in the generated OpenAPI security schema.",
211
  "expected_sources": ["fastapi/security", "fastapi/openapi/utils.py"],
212
  "must_include_any": ["OAuth2", "HTTPBearer", "security", "dependency"]
213
  },
@@ -216,14 +141,89 @@
216
  "category": "conversation",
217
  "turns": [
218
  {"role": "user", "content": "How does FastAPI encode response data before it goes back to the client?"},
219
- {"role": "assistant", "content": "Response bodies are converted with jsonable_encoder before being serialized, factoring in the declared response_model."}
220
  ],
221
- "question": "where is that encoder implemented and how is it tested?",
222
- "ground_truth": "jsonable_encoder is implemented in fastapi/encoders.py, converting Pydantic models and other Python objects into JSON-compatible structures. Its behavior is covered by the pytest suite under the tests directory.",
223
  "expected_sources": ["fastapi/encoders.py", "tests"],
224
  "must_include_any": ["jsonable_encoder", "encoders.py", "tests"]
225
  }
226
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  }
228
  ]
229
  }
 
17
  "id": "documenso-api-v2-document-router",
18
  "category": "api",
19
  "question": "Where is the current document API implemented and how is it exposed?",
20
+ "ground_truth": "The current API V2 is implemented under packages/trpc/server, with document operations organized under a document router. It is exposed through API V2 routes with tRPC and OpenAPI support.",
21
  "expected_sources": ["ARCHITECTURE.md", "packages/trpc/server", "packages/trpc/server/document-router", "apps/remix/server"],
22
  "must_include_any": ["packages/trpc/server", "document-router", "API V2", "OpenAPI", "tRPC"]
23
  },
24
  {
25
  "id": "documenso-signing-package",
26
  "category": "specific-function",
27
+ "question": "What does the @documenso/signing package do, and where is it implemented?",
28
  "ground_truth": "The @documenso/signing package owns PDF signing behavior. Its signPdf entry point selects a signing transport, applies timestamp authority settings when configured, and supports local P12 signing and Google Cloud KMS/HSM-backed signing through transport implementations.",
29
  "expected_sources": ["ARCHITECTURE.md", "packages/signing/index.ts", "packages/signing/helpers", "packages/signing/transports", ".env.example"],
30
+ "must_include_any": ["PDF signing", "packages/signing", "transports", "local", "Google", "KMS"]
31
  },
32
  {
33
  "id": "documenso-document-send-flow",
34
  "category": "cross-file",
35
  "question": "How does a document send operation flow across the Documenso codebase?",
36
+ "ground_truth": "A document send operation starts at an API or UI route, delegates core behavior to server-only document logic and related recipient or field logic, persists state through the data layer, and can trigger emails or jobs.",
37
  "expected_sources": ["packages/trpc/server/document-router", "packages/lib/server-only/document", "packages/lib/server-only/recipient", "packages/prisma", "packages/email", "packages/lib/jobs"],
38
+ "must_include_any": ["document-router", "server-only/document", "recipient", "prisma", "email", "jobs"]
39
  },
40
  {
41
  "id": "documenso-required-env",
42
  "category": "config-setup",
43
  "question": "Which environment variables are central to running a self-hosted Documenso instance?",
44
+ "ground_truth": "Self-hosting requires configuration for authentication, encryption, the public web URL, database connectivity, and SMTP/email delivery. The environment example and self-hosting documentation are the primary sources.",
45
  "expected_sources": ["README.md", ".env.example", "apps/docs/content/docs/self-hosting/configuration/environment.mdx"],
46
  "must_include_any": ["NEXTAUTH_SECRET", "NEXT_PUBLIC_WEBAPP_URL", "DATABASE_URL", "SMTP", "encryption"]
47
  },
 
49
  "id": "documenso-playwright-tests",
50
  "category": "tests",
51
  "question": "Where does Documenso keep end-to-end app tests?",
52
+ "ground_truth": "Documenso keeps end-to-end application tests in the @documenso/app-tests package under packages/app-tests, using Playwright for app-level coverage.",
53
  "expected_sources": ["ARCHITECTURE.md", "packages/app-tests", "packages/app-tests/package.json"],
54
  "must_include_any": ["app-tests", "E2E", "Playwright"]
55
  },
 
57
  "id": "documenso-webhook-security-errors",
58
  "category": "error-handling",
59
  "question": "Where should you look for webhook security or SSRF-related safeguards?",
60
+ "ground_truth": "Webhook security safeguards are implemented in packages/lib/server-only/webhooks, with related configuration such as SSRF bypass hosts documented in environment configuration.",
61
  "expected_sources": [".env.example", "packages/lib/server-only/webhooks"],
62
  "must_include_any": ["webhook", "SSRF", "bypass", "hosts"]
63
  },
 
66
  "category": "conversation",
67
  "turns": [
68
  {"role": "user", "content": "How does Documenso seal or sign completed documents?"},
69
+ {"role": "assistant", "content": "Completed PDFs cross server-only document/job logic and the @documenso/signing package, whose signPdf entry point supports multiple signing transports."}
70
  ],
71
+ "question": "show me where that signing package plugs in",
72
+ "ground_truth": "The follow-up should connect the @documenso/signing package, especially its entry point, transports, and helpers, with the seal-document job that passes completed PDFs into signing and with relevant signing configuration.",
73
  "expected_sources": ["packages/signing/index.ts", "packages/signing/transports", "packages/signing/helpers", "packages/lib/jobs/definitions/internal/seal-document.handler.ts", ".env.example"],
74
+ "must_include_any": ["packages/signing", "transports", "seal-document", "PDF", "signing"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
  ]
77
  },
 
84
  "id": "fastapi-purpose",
85
  "category": "architecture",
86
  "question": "What is FastAPI and what is it built on top of?",
87
+ "ground_truth": "FastAPI is a Python web framework for building APIs. It builds on Starlette for web functionality and Pydantic for data validation and serialization, and it generates OpenAPI-based documentation.",
88
  "expected_sources": ["README.md", "fastapi/applications.py", "pyproject.toml"],
89
  "must_include_any": ["Starlette", "Pydantic", "OpenAPI"]
90
  },
 
92
  "id": "fastapi-app-class",
93
  "category": "specific-function",
94
  "question": "What does the FastAPI application class do and where is it defined?",
95
+ "ground_truth": "The central FastAPI class is defined in fastapi/applications.py and coordinates application routing, middleware, exception handling, dependency-related behavior, and OpenAPI generation.",
96
  "expected_sources": ["fastapi/applications.py"],
97
  "must_include_any": ["applications.py", "routing", "OpenAPI"]
98
  },
 
100
  "id": "fastapi-routing",
101
  "category": "implementation",
102
  "question": "Where is path operation routing implemented in FastAPI?",
103
+ "ground_truth": "Path operation routing is implemented in fastapi/routing.py, including APIRoute and APIRouter and request handling that integrates dependency resolution and response serialization.",
104
  "expected_sources": ["fastapi/routing.py"],
105
  "must_include_any": ["APIRoute", "APIRouter", "routing.py"]
106
  },
 
108
  "id": "fastapi-dependency-injection",
109
  "category": "cross-file",
110
  "question": "How does FastAPI resolve dependencies declared with Depends()?",
111
+ "ground_truth": "Depends-related parameter declarations are defined in FastAPI's parameter modules, while dependency tree construction and request-time resolution live under fastapi/dependencies and are integrated into routing.",
112
  "expected_sources": ["fastapi/dependencies/utils.py", "fastapi/params.py", "fastapi/param_functions.py", "fastapi/routing.py"],
113
+ "must_include_any": ["Depends", "dependencies/utils.py", "dependency", "routing.py"]
114
  },
115
  {
116
  "id": "fastapi-openapi-generation",
117
  "category": "api",
118
  "question": "How does FastAPI generate the OpenAPI schema and interactive docs?",
119
+ "ground_truth": "FastAPI generates its OpenAPI schema from routes and models using its OpenAPI utilities, while Swagger UI and ReDoc HTML are produced by helpers in the OpenAPI docs module.",
120
  "expected_sources": ["fastapi/openapi/utils.py", "fastapi/openapi/docs.py"],
121
  "must_include_any": ["OpenAPI", "Swagger", "ReDoc", "openapi/utils.py"]
122
  },
 
124
  "id": "fastapi-error-handling",
125
  "category": "error-handling",
126
  "question": "How does FastAPI turn validation failures and raised exceptions into HTTP responses?",
127
+ "ground_truth": "FastAPI defines framework exceptions such as HTTPException and RequestValidationError and provides default exception handlers that convert them into HTTP responses.",
128
  "expected_sources": ["fastapi/exceptions.py", "fastapi/exception_handlers.py"],
129
  "must_include_any": ["HTTPException", "RequestValidationError", "exception_handlers.py"]
130
  },
 
132
  "id": "fastapi-security",
133
  "category": "specific-function",
134
  "question": "How does FastAPI support authentication schemes like OAuth2 and HTTP Bearer tokens?",
135
+ "ground_truth": "FastAPI provides authentication helpers under fastapi/security, including OAuth2 and HTTP bearer classes. They integrate with dependency injection and contribute security information to OpenAPI generation.",
136
  "expected_sources": ["fastapi/security", "fastapi/openapi/utils.py"],
137
  "must_include_any": ["OAuth2", "HTTPBearer", "security", "dependency"]
138
  },
 
141
  "category": "conversation",
142
  "turns": [
143
  {"role": "user", "content": "How does FastAPI encode response data before it goes back to the client?"},
144
+ {"role": "assistant", "content": "Response data can be converted with jsonable_encoder before serialization, taking declared response behavior into account."}
145
  ],
146
+ "question": "where is jsonable_encoder implemented and where is that behavior tested?",
147
+ "ground_truth": "jsonable_encoder is implemented in fastapi/encoders.py and converts supported Python and Pydantic values into JSON-compatible structures. Its behavior is exercised by the test suite.",
148
  "expected_sources": ["fastapi/encoders.py", "tests"],
149
  "must_include_any": ["jsonable_encoder", "encoders.py", "tests"]
150
  }
151
  ]
152
+ },
153
+ {
154
+ "id": "django",
155
+ "name": "Django",
156
+ "github_url": "https://github.com/django/django.git",
157
+ "cases": [
158
+ {
159
+ "id": "django-purpose",
160
+ "category": "architecture",
161
+ "question": "What is Django and how is the repository organized at a high level?",
162
+ "ground_truth": "Django is a high-level Python web framework. Framework implementation code lives under django/, documentation under docs/, and the main test suite under tests/.",
163
+ "expected_sources": ["README.rst", "django", "docs", "tests"],
164
+ "must_include_any": ["web framework", "django", "docs", "tests"]
165
+ },
166
+ {
167
+ "id": "django-url-routing",
168
+ "category": "specific-function",
169
+ "question": "Where is Django's URL routing machinery implemented and what does it do?",
170
+ "ground_truth": "Django's URL routing machinery is implemented under django/urls. It provides URL configuration and resolution that maps incoming request paths to views.",
171
+ "expected_sources": ["django/urls", "django/urls/resolvers.py"],
172
+ "must_include_any": ["django/urls", "resolver", "URL", "view"]
173
+ },
174
+ {
175
+ "id": "django-request-response",
176
+ "category": "cross-file",
177
+ "question": "How does an incoming HTTP request reach a Django view and become a response?",
178
+ "ground_truth": "Django request handling crosses the core handler layer, middleware processing, URL resolution, the selected view, and HTTP response classes before the response is returned.",
179
+ "expected_sources": ["django/core/handlers", "django/urls", "django/http"],
180
+ "must_include_any": ["handler", "middleware", "URL", "view", "response"]
181
+ },
182
+ {
183
+ "id": "django-queryset",
184
+ "category": "specific-function",
185
+ "question": "Where is Django's QuerySet implemented and what role does it play?",
186
+ "ground_truth": "QuerySet is implemented in django/db/models/query.py. It is a central ORM abstraction representing database queries and collections of model instances and supports operations such as filtering and ordering.",
187
+ "expected_sources": ["django/db/models/query.py"],
188
+ "must_include_any": ["QuerySet", "query.py", "ORM", "database"]
189
+ },
190
+ {
191
+ "id": "django-settings",
192
+ "category": "config-setup",
193
+ "question": "How does Django load and expose project settings?",
194
+ "ground_truth": "Django's settings machinery lives under django/conf. The settings proxy loads configuration from the configured project settings module and exposes it to framework and application code.",
195
+ "expected_sources": ["django/conf", "django/conf/__init__.py"],
196
+ "must_include_any": ["settings", "django/conf", "SETTINGS_MODULE", "configuration"]
197
+ },
198
+ {
199
+ "id": "django-tests",
200
+ "category": "tests",
201
+ "question": "Where is Django's main test suite and where are contributors told how to run it?",
202
+ "ground_truth": "Django keeps its main test suite under tests/. The repository README points contributors to the unit-test instructions under docs/internals/contributing/writing-code/unit-tests.txt.",
203
+ "expected_sources": ["tests", "README.rst", "docs/internals/contributing/writing-code/unit-tests.txt"],
204
+ "must_include_any": ["tests", "unit-tests.txt", "test suite"]
205
+ },
206
+ {
207
+ "id": "django-security-csrf",
208
+ "category": "error-handling",
209
+ "question": "Where is Django's CSRF protection implemented and what does it validate?",
210
+ "ground_truth": "Django's CSRF protection is implemented in django/middleware/csrf.py. The middleware validates CSRF-related request state and tokens and rejects requests that fail its checks.",
211
+ "expected_sources": ["django/middleware/csrf.py"],
212
+ "must_include_any": ["CSRF", "middleware", "token", "csrf.py"]
213
+ },
214
+ {
215
+ "id": "django-followup-template",
216
+ "category": "conversation",
217
+ "turns": [
218
+ {"role": "user", "content": "How does Django render HTML templates?"},
219
+ {"role": "assistant", "content": "Django's built-in template system loads templates through configured engines and renders them with a context."}
220
+ ],
221
+ "question": "where is that built-in template engine implemented?",
222
+ "ground_truth": "Django's built-in template engine is implemented under django/template, including engine configuration, parsing, loading, and rendering machinery.",
223
+ "expected_sources": ["django/template", "django/template/engine.py", "django/template/base.py"],
224
+ "must_include_any": ["django/template", "Engine", "Template", "render"]
225
+ }
226
+ ]
227
  }
228
  ]
229
  }
src/code_parser.py CHANGED
@@ -14,8 +14,16 @@ LANGUAGE_BY_EXTENSION = {
14
  ".java": "java",
15
  ".go": "go",
16
  ".rs": "rust",
 
 
 
 
 
 
 
17
  }
18
 
 
19
  SYMBOL_NODE_TYPES = {
20
  "python": {"function_definition", "class_definition"},
21
  "javascript": {
@@ -62,6 +70,39 @@ SYMBOL_NODE_TYPES = {
62
  "enum_item",
63
  "trait_item",
64
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
 
67
  IDENTIFIER_TYPES = {
@@ -71,6 +112,10 @@ IDENTIFIER_TYPES = {
71
  "field_identifier",
72
  }
73
 
 
 
 
 
74
 
75
  class CodeParser:
76
  def __init__(self):
@@ -102,58 +147,282 @@ class CodeParser:
102
  lines = source.splitlines()
103
  chunks = []
104
  capture_types = SYMBOL_NODE_TYPES.get(language, set())
 
105
 
106
- def visit(node):
107
  if node.type in capture_types:
108
- chunk = self._build_chunk(node, source, lines, relative_path, language)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  if chunk:
110
  chunks.append(chunk)
111
  return
112
  for child in node.children:
113
- visit(child)
114
 
115
  visit(tree.root_node)
116
 
117
  if not chunks:
118
- return self._fallback_chunks(source, relative_path, language)
 
 
 
 
 
 
119
 
120
  return chunks
121
 
122
- def _build_chunk(self, node, source: str, lines: List[str], relative_path: str, language: str) -> Optional[Dict]:
 
 
 
 
 
 
 
 
123
  start_line = node.start_point[0] + 1
124
  end_line = node.end_point[0] + 1
125
  snippet = "\n".join(lines[start_line - 1 : end_line]).strip()
126
  if len(snippet.splitlines()) < 2:
127
  return None
128
 
129
- name_node = node.child_by_field_name("name")
130
- symbol_name = None
131
- if name_node is not None:
132
- symbol_name = source[name_node.start_byte : name_node.end_byte].strip()
133
- if not symbol_name:
134
- symbol_name = self._find_identifier(node, source)
135
 
136
  signature = lines[start_line - 1].strip() if start_line - 1 < len(lines) else ""
137
  searchable_text = "\n".join(
138
- part for part in [relative_path, symbol_name or "", signature, snippet] if part
 
 
139
  )
140
 
 
 
 
 
141
  return {
142
  "file_path": relative_path,
143
  "language": language,
144
- "symbol_name": symbol_name or relative_path.split("/")[-1],
145
  "symbol_type": node.type,
146
  "line_start": start_line,
147
  "line_end": end_line,
148
  "signature": signature,
149
  "content": snippet,
150
  "searchable_text": searchable_text,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  "metadata_json": {
152
  "parser": "tree-sitter",
 
 
153
  },
154
  }
155
 
156
- def _find_identifier(self, node, source: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  stack = list(node.children)
158
  while stack:
159
  current = stack.pop(0)
@@ -220,4 +489,4 @@ class CodeParser:
220
  },
221
  }
222
  )
223
- return blocks
 
14
  ".java": "java",
15
  ".go": "go",
16
  ".rs": "rust",
17
+ ".c": "c",
18
+ ".h": "c",
19
+ ".cc": "cpp",
20
+ ".cpp": "cpp",
21
+ ".cxx": "cpp",
22
+ ".hpp": "cpp",
23
+ ".hh": "cpp",
24
  }
25
 
26
+ # Node types that make a good standalone retrieval unit (function/method-sized).
27
  SYMBOL_NODE_TYPES = {
28
  "python": {"function_definition", "class_definition"},
29
  "javascript": {
 
70
  "enum_item",
71
  "trait_item",
72
  },
73
+ "c": {
74
+ "function_definition",
75
+ "struct_specifier",
76
+ "enum_specifier",
77
+ "type_definition",
78
+ },
79
+ "cpp": {
80
+ "function_definition",
81
+ "class_specifier",
82
+ "struct_specifier",
83
+ "enum_specifier",
84
+ "namespace_definition",
85
+ "type_definition",
86
+ },
87
+ }
88
+
89
+ # Node types that are "containers": they hold nested members (methods, fields)
90
+ # that are themselves worth indexing as their own chunks. For these, we emit a
91
+ # compact *overview* chunk (signature + docstring + head-of-body + member
92
+ # list) instead of dumping the entire body into one chunk. Without this, a
93
+ # large class becomes a single multi-hundred-line chunk whose embedding is a
94
+ # blurry average of everything inside it (hurts hit rate for method-specific
95
+ # queries) and whose content is silently cut off by the ~1500 char preview
96
+ # used when the chunk is fed to the LLM (hurts faithfulness, since the method
97
+ # the user actually asked about may fall outside the truncated window).
98
+ CONTAINER_NODE_TYPES = {
99
+ "python": {"class_definition"},
100
+ "javascript": {"class_declaration"},
101
+ "typescript": {"class_declaration"},
102
+ "tsx": {"class_declaration"},
103
+ "java": {"class_declaration", "interface_declaration", "enum_declaration"},
104
+ "rust": {"impl_item", "trait_item"},
105
+ "cpp": {"class_specifier", "namespace_definition"},
106
  }
107
 
108
  IDENTIFIER_TYPES = {
 
112
  "field_identifier",
113
  }
114
 
115
+ MAX_OVERVIEW_BODY_LINES = 40
116
+ MAX_OVERVIEW_CHARS = 1200
117
+ MAX_MEMBERS_LISTED = 25
118
+
119
 
120
  class CodeParser:
121
  def __init__(self):
 
147
  lines = source.splitlines()
148
  chunks = []
149
  capture_types = SYMBOL_NODE_TYPES.get(language, set())
150
+ container_types = CONTAINER_NODE_TYPES.get(language, set())
151
 
152
+ def visit(node, context_name: Optional[str] = None):
153
  if node.type in capture_types:
154
+ if node.type in container_types:
155
+ overview = self._build_container_overview(
156
+ node, source, lines, relative_path, language, context_name, capture_types
157
+ )
158
+ if overview:
159
+ chunks.append(overview)
160
+
161
+ own_name = self._extract_own_name(node, source)
162
+ nested_context = own_name
163
+ if context_name and own_name:
164
+ nested_context = f"{context_name}.{own_name}"
165
+ elif context_name and not own_name:
166
+ nested_context = context_name
167
+
168
+ for child in node.children:
169
+ visit(child, nested_context)
170
+ return
171
+
172
+ chunk = self._build_chunk(node, source, lines, relative_path, language, context_name)
173
  if chunk:
174
  chunks.append(chunk)
175
  return
176
  for child in node.children:
177
+ visit(child, context_name)
178
 
179
  visit(tree.root_node)
180
 
181
  if not chunks:
182
+ chunks = self._fallback_chunks(source, relative_path, language)
183
+
184
+ file_overview = self._build_file_overview(
185
+ tree.root_node, source, lines, relative_path, language, chunks
186
+ )
187
+ if file_overview:
188
+ chunks.insert(0, file_overview)
189
 
190
  return chunks
191
 
192
+ def _build_chunk(
193
+ self,
194
+ node,
195
+ source: str,
196
+ lines: List[str],
197
+ relative_path: str,
198
+ language: str,
199
+ context_name: Optional[str] = None,
200
+ ) -> Optional[Dict]:
201
  start_line = node.start_point[0] + 1
202
  end_line = node.end_point[0] + 1
203
  snippet = "\n".join(lines[start_line - 1 : end_line]).strip()
204
  if len(snippet.splitlines()) < 2:
205
  return None
206
 
207
+ own_name = self._extract_own_name(node, source)
208
+ qualified_name = own_name
209
+ if context_name and own_name:
210
+ qualified_name = f"{context_name}.{own_name}"
211
+ elif context_name and not own_name:
212
+ qualified_name = context_name
213
 
214
  signature = lines[start_line - 1].strip() if start_line - 1 < len(lines) else ""
215
  searchable_text = "\n".join(
216
+ part
217
+ for part in [relative_path, context_name or "", qualified_name or "", signature, snippet]
218
+ if part
219
  )
220
 
221
+ metadata = {"parser": "tree-sitter"}
222
+ if context_name:
223
+ metadata["parent"] = context_name
224
+
225
  return {
226
  "file_path": relative_path,
227
  "language": language,
228
+ "symbol_name": qualified_name or relative_path.split("/")[-1],
229
  "symbol_type": node.type,
230
  "line_start": start_line,
231
  "line_end": end_line,
232
  "signature": signature,
233
  "content": snippet,
234
  "searchable_text": searchable_text,
235
+ "metadata_json": metadata,
236
+ }
237
+
238
+ def _build_container_overview(
239
+ self,
240
+ node,
241
+ source: str,
242
+ lines: List[str],
243
+ relative_path: str,
244
+ language: str,
245
+ context_name: Optional[str],
246
+ capture_types: set,
247
+ ) -> Optional[Dict]:
248
+ start_line = node.start_point[0] + 1
249
+ end_line = node.end_point[0] + 1
250
+ body_lines = lines[start_line - 1 : end_line]
251
+ if not body_lines:
252
+ return None
253
+
254
+ own_name = self._extract_own_name(node, source)
255
+ qualified_name = own_name
256
+ if context_name and own_name:
257
+ qualified_name = f"{context_name}.{own_name}"
258
+ elif context_name and not own_name:
259
+ qualified_name = context_name
260
+ if not qualified_name:
261
+ qualified_name = relative_path.split("/")[-1]
262
+
263
+ # Head-of-body preview: naturally captures the signature, docstring,
264
+ # and field declarations that come before the first nested method,
265
+ # so simple data classes / structs keep their field list even though
266
+ # we no longer store the entire body verbatim.
267
+ preview_lines = body_lines[:MAX_OVERVIEW_BODY_LINES]
268
+ content = "\n".join(preview_lines).strip()
269
+ truncated_body = len(body_lines) > MAX_OVERVIEW_BODY_LINES
270
+ if len(content) > MAX_OVERVIEW_CHARS:
271
+ content = content[:MAX_OVERVIEW_CHARS].rstrip()
272
+ truncated_body = True
273
+
274
+ member_names = self._collect_member_names(node, source, capture_types)
275
+ members_line = ""
276
+ if member_names:
277
+ shown = member_names[:MAX_MEMBERS_LISTED]
278
+ members_line = f"Members: {', '.join(shown)}"
279
+ remaining = len(member_names) - len(shown)
280
+ if remaining > 0:
281
+ members_line += f" (+{remaining} more, indexed separately)"
282
+ elif truncated_body:
283
+ members_line = "(body truncated; see file for full contents)"
284
+
285
+ content_parts = [part for part in [content, members_line] if part]
286
+ full_content = "\n\n".join(content_parts)
287
+
288
+ signature = body_lines[0].strip() if body_lines else ""
289
+ searchable_text = "\n".join(
290
+ part
291
+ for part in [relative_path, context_name or "", qualified_name, signature, full_content]
292
+ if part
293
+ )
294
+
295
+ return {
296
+ "file_path": relative_path,
297
+ "language": language,
298
+ "symbol_name": qualified_name,
299
+ "symbol_type": f"{node.type}_overview",
300
+ "line_start": start_line,
301
+ "line_end": end_line,
302
+ "signature": signature,
303
+ "content": full_content,
304
+ "searchable_text": searchable_text,
305
  "metadata_json": {
306
  "parser": "tree-sitter",
307
+ "kind": "container_overview",
308
+ **({"parent": context_name} if context_name else {}),
309
  },
310
  }
311
 
312
+ def _build_file_overview(self, root_node, source: str, lines: List[str], relative_path: str, language: str, symbol_chunks: List[Dict]) -> Optional[Dict]:
313
+ if language == "text":
314
+ return None
315
+ path = Path(relative_path)
316
+ parts = list(path.parts)
317
+ directory = str(path.parent) if str(path.parent) != "." else "repository root"
318
+ filename = path.name
319
+ component = parts[1] if len(parts) >= 2 and parts[0] in {"packages", "apps"} else (parts[-2] if len(parts) >= 2 else "")
320
+ role = self._infer_file_role(relative_path)
321
+ imports, exports, export_targets = self._extract_module_edges(root_node, source)
322
+ symbols, seen = [], set()
323
+ for chunk in symbol_chunks:
324
+ name = chunk.get("symbol_name")
325
+ if name and name not in seen and chunk.get("symbol_type") != "fallback_chunk":
326
+ seen.add(name); symbols.append(name)
327
+ if len(symbols) >= 40:
328
+ break
329
+ overview = [f"File: {relative_path}", f"Filename: {filename}", f"Directory: {directory}", f"Language: {language}"]
330
+ if component: overview.append(f"Package/component: {component}")
331
+ overview.append(f"Role: {role}")
332
+ if imports: overview.append("Imports: " + ", ".join(imports[:30]))
333
+ if exports: overview.append("Exports: " + ", ".join(exports[:40]))
334
+ if export_targets: overview.append("Export targets: " + ", ".join(export_targets[:30]))
335
+ if symbols: overview.append("Symbols: " + ", ".join(symbols))
336
+ content = "\n".join(overview)
337
+ path_terms = " ".join(t for t in re.split(r"[\/._:-]+", relative_path) if t)
338
+ return {
339
+ "file_path": relative_path, "language": language,
340
+ "symbol_name": f"{filename}:module-overview", "symbol_type": "file_overview",
341
+ "line_start": 1, "line_end": 1, "signature": f"module {relative_path}",
342
+ "content": content,
343
+ "searchable_text": content + f"\nPath terms: {path_terms}\nModule: {path.stem}",
344
+ "metadata_json": {"parser": "tree-sitter", "kind": "file_overview", "directory": directory, "component": component, "role": role, "imports": imports[:30], "exports": exports[:40], "export_targets": export_targets[:30]},
345
+ }
346
+
347
+ @staticmethod
348
+ def _infer_file_role(relative_path: str) -> str:
349
+ path = relative_path.lower().replace("\\", "/")
350
+ name = Path(path).name
351
+ if name in {"index.ts", "index.tsx", "index.js", "index.jsx"}:
352
+ return "module/package entry point or barrel export"
353
+ for needle, role in [("router", "router/API composition"), ("route", "request route/endpoint"), ("handler", "handler/job execution"), ("controller", "request controller"), ("service", "service/business logic"), ("transport", "transport/integration adapter"), ("adapter", "integration adapter"), ("repository", "data repository"), ("config", "configuration"), ("schema", "schema/type definition"), ("test", "test"), ("spec", "test/specification")]:
354
+ if needle in name or f"/{needle}" in path:
355
+ return role
356
+ return "source module"
357
+
358
+ def _extract_module_edges(self, root_node, source: str):
359
+ imports, exports, targets = [], [], []
360
+ for child in root_node.children:
361
+ text = source[child.start_byte:child.end_byte].strip()
362
+ if not text: continue
363
+ if child.type == "import_statement" or text.startswith("import "):
364
+ target = self._module_target(text)
365
+ if target and target not in imports: imports.append(target)
366
+ if "export" in child.type or text.startswith("export ") or text.startswith("module.exports"):
367
+ target = self._module_target(text)
368
+ if target and target not in targets: targets.append(target)
369
+ for name in self._exported_names(text):
370
+ if name not in exports: exports.append(name)
371
+ return imports, exports, targets
372
+
373
+ @staticmethod
374
+ def _module_target(statement: str) -> Optional[str]:
375
+ for pattern in [r"from\s+[\"']([^\"']+)[\"']", r"(?:import|export)\s+[\"']([^\"']+)[\"']", r"require\(\s*[\"']([^\"']+)[\"']\s*\)"]:
376
+ match = re.search(pattern, statement)
377
+ if match: return match.group(1)
378
+ return None
379
+
380
+ @staticmethod
381
+ def _exported_names(statement: str) -> List[str]:
382
+ names = []
383
+ match = re.search(r"export\s*\{([^}]+)\}", statement, re.S)
384
+ if match:
385
+ for part in match.group(1).split(","):
386
+ item = part.strip()
387
+ if item: names.append(re.split(r"\s+as\s+", item)[-1].strip())
388
+ match = re.search(r"export\s+(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)", statement)
389
+ if match: names.append(match.group(1))
390
+ if statement.startswith("export default") and not match: names.append("default")
391
+ if statement.startswith("export *"): names.append("*")
392
+ return names
393
+
394
+ def _collect_member_names(self, node, source: str, capture_types: set) -> List[str]:
395
+ """Collect names of direct member symbols (methods/fields) inside a
396
+ container without descending into nested containers, so a class's
397
+ member list doesn't pick up grandchildren from an inner class."""
398
+ names = []
399
+ seen = set()
400
+
401
+ def walk(current):
402
+ for child in current.children:
403
+ if child.type in capture_types:
404
+ name = self._extract_own_name(child, source)
405
+ if name and name not in seen:
406
+ seen.add(name)
407
+ names.append(name)
408
+ # Don't descend further into this member's own body.
409
+ continue
410
+ walk(child)
411
+
412
+ walk(node)
413
+ return names
414
+
415
+ @staticmethod
416
+ def _extract_own_name(node, source: str) -> Optional[str]:
417
+ name_node = node.child_by_field_name("name")
418
+ if name_node is not None:
419
+ candidate = source[name_node.start_byte : name_node.end_byte].strip()
420
+ if candidate:
421
+ return candidate
422
+ return CodeParser._find_identifier(node, source)
423
+
424
+ @staticmethod
425
+ def _find_identifier(node, source: str) -> Optional[str]:
426
  stack = list(node.children)
427
  while stack:
428
  current = stack.pop(0)
 
489
  },
490
  }
491
  )
492
+ return blocks
src/embeddings.py CHANGED
@@ -320,13 +320,17 @@ class EmbeddingGenerator:
320
  return np.array(embeddings, dtype="float32")
321
 
322
  def _build_bedrock_embedding_request(self, texts: List[str], input_type: str) -> dict:
 
 
323
  payload = {
324
- "texts": texts,
325
  "input_type": input_type,
326
  "embedding_types": ["float"],
327
  }
 
328
  if self.bedrock_output_dimensionality:
329
  payload["output_dimension"] = self.bedrock_output_dimensionality
 
330
  return payload
331
 
332
  def _encode_with_backoff(
 
320
  return np.array(embeddings, dtype="float32")
321
 
322
  def _build_bedrock_embedding_request(self, texts: List[str], input_type: str) -> dict:
323
+ max_chars = int(os.getenv("BEDROCK_EMBEDDING_MAX_CHARS", "2000"))
324
+
325
  payload = {
326
+ "texts": [text[:max_chars] for text in texts],
327
  "input_type": input_type,
328
  "embedding_types": ["float"],
329
  }
330
+
331
  if self.bedrock_output_dimensionality:
332
  payload["output_dimension"] = self.bedrock_output_dimensionality
333
+
334
  return payload
335
 
336
  def _encode_with_backoff(
src/hybrid_search.py CHANGED
@@ -1,6 +1,7 @@
1
  import re
 
2
  from collections import defaultdict
3
- from typing import List
4
 
5
  from rank_bm25 import BM25Okapi
6
  from sentence_transformers import CrossEncoder
@@ -9,34 +10,95 @@ TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_./:-]*")
9
 
10
 
11
  def tokenize(text: str) -> List[str]:
12
- return [token.lower() for token in TOKEN_RE.findall(text)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
  class HybridSearchEngine:
16
  def __init__(self, reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
17
  self.reranker = CrossEncoder(reranker_model)
 
 
 
 
 
 
18
 
19
  def build_for_repository(self, repo_id: int, chunks: List[dict]):
20
- return None
 
 
 
 
 
 
 
 
21
 
22
  def remove_repository(self, repo_id: int):
23
- return None
 
24
 
25
- def bm25_search(self, chunks: List[dict], query: str, top_k: int = 12) -> List[dict]:
 
 
 
 
 
 
26
  if not chunks:
27
  return []
28
  tokens = tokenize(query)
29
  if not tokens:
30
  return []
31
 
32
- corpus_tokens = [tokenize(chunk["searchable_text"]) for chunk in chunks]
33
- bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  if not bm25:
35
  return []
36
 
37
  scores = bm25.get_scores(tokens)
38
  ranked = sorted(
39
- zip(chunks, scores),
40
  key=lambda item: item[1],
41
  reverse=True,
42
  )[:top_k]
@@ -69,11 +131,17 @@ class HybridSearchEngine:
69
  merged = sorted(fused.values(), key=lambda item: item["rrf_score"], reverse=True)
70
  return merged[:top_k]
71
 
72
- def rerank(self, query: str, candidates: List[dict], top_k: int = 6) -> List[dict]:
73
- """
74
- FIX: top_k now defaults to 6 and callers should pass a small final number (4-6),
75
- NOT search_depth (which was up to 120). Reranking 120 items then dumping them
76
- all into the LLM context was the main faithfulness killer.
 
 
 
 
 
 
77
  """
78
  if not candidates:
79
  return []
@@ -91,7 +159,7 @@ class HybridSearchEngine:
91
  reranked.append(enriched)
92
 
93
  reranked.sort(key=lambda item: item["rerank_score"], reverse=True)
94
- return reranked[:top_k]
95
 
96
  @staticmethod
97
  def normalize_semantic_results(results: List[dict]) -> List[dict]:
 
1
  import re
2
+ import threading
3
  from collections import defaultdict
4
+ from typing import Dict, List, Optional
5
 
6
  from rank_bm25 import BM25Okapi
7
  from sentence_transformers import CrossEncoder
 
10
 
11
 
12
  def tokenize(text: str) -> List[str]:
13
+ raw_tokens = TOKEN_RE.findall(text or "")
14
+ tokens = []
15
+
16
+ for raw in raw_tokens:
17
+ lowered = raw.lower()
18
+ tokens.append(lowered)
19
+
20
+ # Keep the original code/path token, but also expose its components to
21
+ # BM25. This makes sendDocument, seal-document.handler.ts, etc. match
22
+ # natural-language queries much more reliably.
23
+ pieces = re.split(r"[./:_-]+", raw)
24
+ for piece in pieces:
25
+ if not piece:
26
+ continue
27
+ tokens.append(piece.lower())
28
+ camel_parts = re.findall(
29
+ r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+",
30
+ piece,
31
+ )
32
+ tokens.extend(part.lower() for part in camel_parts if part)
33
+
34
+ return [token for token in tokens if token]
35
 
36
 
37
  class HybridSearchEngine:
38
  def __init__(self, reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
39
  self.reranker = CrossEncoder(reranker_model)
40
+ # Per-repo cached BM25 index so a question doesn't have to
41
+ # re-tokenize and re-build the lexical index over every chunk in the
42
+ # repo on every single request. Built once when indexing finishes,
43
+ # evicted when the repo is reset/deleted/expired.
44
+ self._repo_indexes: Dict[int, dict] = {}
45
+ self._index_lock = threading.Lock()
46
 
47
  def build_for_repository(self, repo_id: int, chunks: List[dict]):
48
+ if not chunks:
49
+ with self._index_lock:
50
+ self._repo_indexes.pop(repo_id, None)
51
+ return
52
+
53
+ corpus_tokens = [tokenize(chunk["searchable_text"]) for chunk in chunks]
54
+ bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None
55
+ with self._index_lock:
56
+ self._repo_indexes[repo_id] = {"bm25": bm25, "chunks": chunks}
57
 
58
  def remove_repository(self, repo_id: int):
59
+ with self._index_lock:
60
+ self._repo_indexes.pop(repo_id, None)
61
 
62
+ def bm25_search(
63
+ self,
64
+ chunks: List[dict],
65
+ query: str,
66
+ top_k: int = 12,
67
+ repo_id: Optional[int] = None,
68
+ ) -> List[dict]:
69
  if not chunks:
70
  return []
71
  tokens = tokenize(query)
72
  if not tokens:
73
  return []
74
 
75
+ bm25 = None
76
+ source_chunks = chunks
77
+
78
+ if repo_id is not None:
79
+ with self._index_lock:
80
+ cached = self._repo_indexes.get(repo_id)
81
+ # Guard against a stale cache (e.g. repo was re-indexed but the
82
+ # cache write raced with this read) by checking the corpus size
83
+ # still lines up before trusting it.
84
+ if cached is not None and len(cached["chunks"]) == len(chunks):
85
+ bm25 = cached["bm25"]
86
+ source_chunks = cached["chunks"]
87
+
88
+ if bm25 is None:
89
+ # Fall back to building an ephemeral index. Keeps this method
90
+ # correct on its own even if build_for_repository wasn't called
91
+ # first (e.g. direct/test usage), just without the caching win.
92
+ corpus_tokens = [tokenize(chunk["searchable_text"]) for chunk in chunks]
93
+ bm25 = BM25Okapi(corpus_tokens) if corpus_tokens else None
94
+ source_chunks = chunks
95
+
96
  if not bm25:
97
  return []
98
 
99
  scores = bm25.get_scores(tokens)
100
  ranked = sorted(
101
+ zip(source_chunks, scores),
102
  key=lambda item: item[1],
103
  reverse=True,
104
  )[:top_k]
 
131
  merged = sorted(fused.values(), key=lambda item: item["rrf_score"], reverse=True)
132
  return merged[:top_k]
133
 
134
+ def rerank(
135
+ self,
136
+ query: str,
137
+ candidates: List[dict],
138
+ top_k: Optional[int] = None,
139
+ ) -> List[dict]:
140
+ """Score candidates with the cross-encoder and optionally truncate.
141
+
142
+ Reranking depth is intentionally separate from answer-context depth.
143
+ Callers can rerank a broad candidate set and still send only a small
144
+ final source set to the LLM.
145
  """
146
  if not candidates:
147
  return []
 
159
  reranked.append(enriched)
160
 
161
  reranked.sort(key=lambda item: item["rerank_score"], reverse=True)
162
+ return reranked[:top_k] if top_k is not None else reranked
163
 
164
  @staticmethod
165
  def normalize_semantic_results(results: List[dict]) -> List[dict]:
src/rag_system.py CHANGED
@@ -278,6 +278,9 @@ class CodebaseRAGSystem:
278
  repo.session_expires_at = self._session_expiry()
279
  self._mark_repo_updated(repo)
280
  self.repo_chunks[repo.id] = serialized
 
 
 
281
  self.vector_store.save()
282
  with self.repo_lock:
283
  self.indexing_progress.pop(repo.id, None)
@@ -343,6 +346,7 @@ class CodebaseRAGSystem:
343
  question: str,
344
  top_k: int = 8,
345
  history=None,
 
346
  ) -> dict:
347
  with self.repo_lock:
348
  self._cleanup_expired_sessions()
@@ -390,9 +394,17 @@ class CodebaseRAGSystem:
390
  repo_chunks,
391
  retrieval_query,
392
  top_k=search_depth,
 
393
  )
 
394
  semantic_hits = self.hybrid_search.normalize_semantic_results(semantic_hits)
395
- fused = self.hybrid_search.reciprocal_rank_fusion(lexical_hits, semantic_hits, top_k=search_depth)
 
 
 
 
 
 
396
 
397
  path_hits = self._path_intent_search(
398
  repo_chunks,
@@ -400,26 +412,67 @@ class CodebaseRAGSystem:
400
  retrieval_query,
401
  top_k=search_depth,
402
  )
403
- fused = self._merge_ranked_candidates(fused, path_hits, top_k=search_depth)
 
 
404
 
405
  rerank_query = retrieval_query if question_intent in deep_search_intents else question
406
 
407
- # FIX: rerank to a small candidate pool first (20), then let
408
- # _prioritize_results and _select_answer_sources trim to final top_k.
409
- # Previously rerank was called with search_depth (up to 120), meaning
410
- # the LLM received far too many chunks and faithfulness dropped.
411
- rerank_pool = min(search_depth, 20)
412
- reranked = self.hybrid_search.rerank(rerank_query, fused, top_k=rerank_pool)
413
 
414
- reranked = self._prioritize_results(question, retrieval_query, reranked, top_k=top_k)
 
 
415
 
416
- # FIX: cap final sources at 5 instead of top_k (8).
417
- # 5 sources × 1500 chars = ~7500 chars context, which the LLM handles well.
418
- # 8 sources × 2500 chars = ~20000 chars, which causes lost-in-the-middle issues.
419
  final_top_k = min(top_k, 5)
420
- reranked = self._select_answer_sources(question, reranked, top_k=final_top_k)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
- answer = self._generate_answer(repo, question, reranked, normalized_history)
 
 
423
  return answer
424
 
425
  def end_session(self, session_key: str):
@@ -928,6 +981,29 @@ Do not leave the answer unfinished.
928
  parts.append(f"Previous answer: {recent_assistant[0][:300]}")
929
  return "\n".join(parts)
930
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  def _merge_ranked_candidates(
932
  self,
933
  ranked_results: List[dict],
@@ -1036,6 +1112,9 @@ Do not leave the answer unfinished.
1036
  results: List[dict],
1037
  top_k: int,
1038
  ) -> List[dict]:
 
 
 
1039
  combined_query = f"{question} {retrieval_query}".lower()
1040
  wants_code = any(
1041
  token in combined_query
@@ -1046,41 +1125,47 @@ Do not leave the answer unfinished.
1046
  "docs",
1047
  "overview",
1048
  }
1049
- wants_repo_overview = self._is_repo_overview_question(question) or self._is_repo_overview_question(
1050
- retrieval_query
 
 
 
 
 
 
 
1051
  )
1052
 
1053
- def sort_key(item: dict):
 
 
1054
  is_doc = self._is_doc_source(item)
1055
- return (
1056
- self._canonical_path_priority(item, combined_query),
1057
- float(item.get("path_score", 0.0)),
1058
- self._doc_priority(item),
1059
- 1 if wants_repo_overview and is_doc else 0,
1060
- 1 if (wants_docs and is_doc) or (not wants_docs and not is_doc) else 0,
1061
- 1 if wants_code and not is_doc else 0,
1062
- 1 if question_intent in {"api", "implementation", "cross_file", "error_handling", "setup"} and not is_doc else 0,
1063
- float(item.get("rerank_score", 0.0)),
1064
- float(item.get("semantic_score", 0.0)),
1065
- float(item.get("bm25_score", 0.0)),
 
 
 
 
 
 
 
 
 
 
1066
  )
 
1067
 
1068
- ranked = sorted(results, key=sort_key, reverse=True)
1069
- if wants_docs or wants_repo_overview:
1070
- return ranked[:top_k]
1071
-
1072
- selected = []
1073
- doc_items = []
1074
- for item in ranked:
1075
- if self._is_doc_source(item):
1076
- doc_items.append(item)
1077
- continue
1078
- selected.append(item)
1079
- if len(selected) == top_k:
1080
- return selected
1081
-
1082
- selected.extend(doc_items[: max(1, top_k - len(selected))])
1083
- return selected[:top_k]
1084
 
1085
  def _select_answer_sources(
1086
  self,
@@ -1094,14 +1179,33 @@ Do not leave the answer unfinished.
1094
  intent = self._question_intent(question)
1095
  max_per_file = 2 if intent in {"overview", "docs"} else 1
1096
  selected = []
 
1097
  file_counts = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
 
1099
  for item in results:
 
 
1100
  file_path = item.get("file_path", "")
1101
  count = file_counts.get(file_path, 0)
1102
  if count >= max_per_file:
1103
  continue
1104
  selected.append(item)
 
1105
  file_counts[file_path] = count + 1
1106
  if len(selected) == top_k:
1107
  break
@@ -1260,27 +1364,44 @@ Do not leave the answer unfinished.
1260
  @staticmethod
1261
  def _is_repo_overview_question(question: str) -> bool:
1262
  normalized = " ".join((question or "").lower().split())
1263
- return any(
1264
- phrase in normalized
1265
- for phrase in {
1266
- "what is the repo about",
1267
- "what is this repo about",
1268
- "what does the repo do",
1269
- "what does this repo do",
1270
- "what is the repository about",
1271
- "what does the repository do",
1272
- "what is this project about",
1273
- "what does this project do",
1274
- "repo summary",
1275
- "repository summary",
1276
- "project summary",
1277
- "summarize the repo",
1278
- "summarize this repo",
1279
- "repo overview",
1280
- "repository overview",
1281
- "project overview",
1282
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1283
  )
 
1284
 
1285
  @staticmethod
1286
  def _is_doc_source(item: dict) -> bool:
@@ -1301,189 +1422,104 @@ Do not leave the answer unfinished.
1301
 
1302
  @staticmethod
1303
  def _domain_path_hints(query: str) -> List[str]:
 
 
 
 
 
 
 
 
 
 
 
 
1304
  normalized = " ".join((query or "").lower().split())
1305
  hints = []
1306
 
1307
  def has_any(terms: set[str]) -> bool:
1308
- matched = False
1309
- for term in terms:
1310
- if term.startswith("/"):
1311
- matched = matched or term in normalized
1312
- continue
1313
- matched = matched or bool(
1314
- re.search(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", normalized)
1315
- )
1316
- return matched
1317
 
1318
  rules = [
1319
  (
1320
- {"email", "emails", "mailer", "notification", "template"},
1321
- [
1322
- "packages/email",
1323
- "packages/lib/server-only/email",
1324
- "packages/lib/jobs/definitions/emails",
1325
- ],
1326
  ),
1327
  (
1328
- {"signing", "certificate", "gcloud", "hsm", "p12", "pdf signing"},
1329
- [
1330
- "packages/signing",
1331
- "packages/signing/transports",
1332
- "packages/signing/helpers",
1333
- "packages/lib/jobs/definitions/internal/seal-document",
1334
- ],
1335
  ),
1336
  (
1337
- {"job", "jobs", "background", "inngest", "bullmq", "queue"},
1338
- [
1339
- "packages/lib/jobs",
1340
- "packages/lib/jobs/client",
1341
- "packages/lib/jobs/definitions",
1342
- "apps/remix/server/router.ts",
1343
- ],
1344
  ),
1345
  (
1346
- {"webhook", "webhooks", "ssrf"},
1347
- [
1348
- "packages/lib/server-only/webhooks",
1349
- "packages/lib/jobs/definitions/internal/execute-webhook",
1350
- ],
1351
  ),
1352
  (
1353
- {"recipient", "recipients"},
1354
  [
1355
- "packages/lib/server-only/recipient",
1356
- "packages/trpc/server/recipient-router",
 
 
 
 
 
 
1357
  ],
1358
  ),
1359
  (
1360
- {"field", "fields"},
1361
- [
1362
- "packages/lib/server-only/field",
1363
- "packages/trpc/server/field-router",
1364
- "packages/lib/universal/field-renderer",
1365
- ],
1366
  ),
1367
  (
1368
- {"template", "templates"},
1369
- [
1370
- "packages/lib/server-only/template",
1371
- "packages/trpc/server/template-router",
1372
- ],
1373
  ),
1374
  (
1375
- {"envelope", "envelopes"},
1376
- [
1377
- "packages/lib/server-only/envelope",
1378
- "packages/lib/server-only/envelope-item",
1379
- "packages/trpc/server/envelope-router",
1380
- ],
1381
  ),
1382
  (
1383
- {"document", "documents"},
1384
- [
1385
- "packages/lib/server-only/document",
1386
- "packages/lib/server-only/document-data",
1387
- "packages/trpc/server/document-router",
1388
- ],
1389
  ),
1390
  (
1391
- {"pdf", "storage", "upload", "s3"},
1392
- [
1393
- "packages/lib/server-only/pdf",
1394
- "packages/lib/server-only/document-data",
1395
- "packages/lib/universal/upload",
1396
- "apps/remix/server/api/files",
1397
- ],
1398
  ),
1399
  (
1400
- {"api v1", "/api/v1", "ts-rest", "ts rest"},
1401
- [
1402
- "packages/api",
1403
- "packages/api/v1",
1404
- "packages/api/hono.ts",
1405
- ],
1406
  ),
1407
  (
1408
- {"api v2", "/api/v2", "openapi", "trpc-to-openapi"},
1409
- [
1410
- "packages/trpc/server",
1411
- "packages/trpc/server/open-api.ts",
1412
- "apps/remix/server/router.ts",
1413
- "apps/remix/server/trpc",
1414
- ],
1415
  ),
1416
  (
1417
- {"trpc", "frontend", "backend", "/api/trpc", "internal api"},
1418
- [
1419
- "packages/trpc",
1420
- "packages/trpc/react",
1421
- "packages/trpc/client",
1422
- "packages/trpc/server/context.ts",
1423
- "apps/remix/server/trpc",
1424
- "apps/remix/server/router.ts",
1425
- ],
1426
- ),
1427
- (
1428
- {"auth", "authentication", "session", "api token", "authorization", "bearer"},
1429
- [
1430
- "packages/auth",
1431
- "packages/lib/server-only/auth",
1432
- "packages/lib/server-only/public-api",
1433
- "packages/trpc/server/context.ts",
1434
- "packages/trpc/server/trpc.ts",
1435
- "packages/api/v1/middleware/authenticated.ts",
1436
- "apps/remix/server/context.ts",
1437
- ],
1438
  ),
1439
  (
1440
- {"database", "postgres", "postgresql", "prisma", "kysely", "migration"},
1441
- [
1442
- "packages/prisma",
1443
- "packages/prisma/schema.prisma",
1444
- "packages/prisma/migrations",
1445
- ".env.example",
1446
- ],
1447
  ),
1448
  (
1449
- {"remix", "hono", "react router", "route", "routes", "user interface"},
1450
- [
1451
- "apps/remix/server",
1452
- "apps/remix/app/routes",
1453
- "apps/remix/app/root.tsx",
1454
- "apps/remix/app/routes.ts",
1455
- ],
1456
  ),
1457
  (
1458
- {"test", "tests", "e2e", "playwright", "spec", "vitest"},
1459
- [
1460
- "packages/app-tests",
1461
- "packages/lib/vitest.config.ts",
1462
- "packages/lib/package.json",
1463
- ],
1464
  ),
1465
  (
1466
- {
1467
- "config",
1468
- "configuration",
1469
- "env",
1470
- "environment",
1471
- "local development",
1472
- "self-host",
1473
- "self hosting",
1474
- "workspace",
1475
- "workspaces",
1476
- "turborepo",
1477
- "turbo",
1478
- },
1479
- [
1480
- ".env.example",
1481
- "README.md",
1482
- "package.json",
1483
- "turbo.json",
1484
- "apps/docs/content/docs/developers/local-development",
1485
- "apps/docs/content/docs/self-hosting/configuration",
1486
- ],
1487
  ),
1488
  ]
1489
 
 
278
  repo.session_expires_at = self._session_expiry()
279
  self._mark_repo_updated(repo)
280
  self.repo_chunks[repo.id] = serialized
281
+ # Build the lexical (BM25) index once now, instead of
282
+ # re-tokenizing every chunk in the repo on every question.
283
+ self.hybrid_search.build_for_repository(repo.id, serialized)
284
  self.vector_store.save()
285
  with self.repo_lock:
286
  self.indexing_progress.pop(repo.id, None)
 
346
  question: str,
347
  top_k: int = 8,
348
  history=None,
349
+ debug_retrieval: bool = False,
350
  ) -> dict:
351
  with self.repo_lock:
352
  self._cleanup_expired_sessions()
 
394
  repo_chunks,
395
  retrieval_query,
396
  top_k=search_depth,
397
+ repo_id=repo_id,
398
  )
399
+
400
  semantic_hits = self.hybrid_search.normalize_semantic_results(semantic_hits)
401
+ semantic_ranks = self._rank_map(semantic_hits)
402
+ lexical_ranks = self._rank_map(lexical_hits)
403
+
404
+ fused = self.hybrid_search.reciprocal_rank_fusion(
405
+ lexical_hits, semantic_hits, top_k=search_depth
406
+ )
407
+ fused_ranks = self._rank_map(fused)
408
 
409
  path_hits = self._path_intent_search(
410
  repo_chunks,
 
412
  retrieval_query,
413
  top_k=search_depth,
414
  )
415
+ path_ranks = self._rank_map(path_hits)
416
+
417
+ merged = self._merge_ranked_candidates(fused, path_hits, top_k=search_depth)
418
 
419
  rerank_query = retrieval_query if question_intent in deep_search_intents else question
420
 
421
+ # Rerank broadly for recall. This is independent of the final LLM
422
+ # context size, which remains capped below.
423
+ rerank_pool = min(search_depth, 50)
424
+ reranked = self.hybrid_search.rerank(rerank_query, merged, top_k=rerank_pool)
425
+ rerank_ranks = self._rank_map(reranked)
 
426
 
427
+ prioritized = self._prioritize_results(
428
+ question, retrieval_query, reranked, top_k=top_k
429
+ )
430
 
 
 
 
431
  final_top_k = min(top_k, 5)
432
+ final_sources = self._select_answer_sources(
433
+ question, prioritized, top_k=final_top_k
434
+ )
435
+ final_ranks = self._rank_map(final_sources)
436
+
437
+ retrieval_debug = []
438
+ if debug_retrieval:
439
+ all_candidates = {}
440
+ for stage_items in (semantic_hits, lexical_hits, fused, path_hits, merged, reranked, prioritized, final_sources):
441
+ for item in stage_items:
442
+ all_candidates[item["id"]] = {**all_candidates.get(item["id"], {}), **item}
443
+
444
+ for chunk_id, item in all_candidates.items():
445
+ retrieval_debug.append(
446
+ {
447
+ "id": chunk_id,
448
+ "file_path": item.get("file_path"),
449
+ "symbol_name": item.get("symbol_name"),
450
+ "semantic_rank": semantic_ranks.get(chunk_id),
451
+ "bm25_rank": lexical_ranks.get(chunk_id),
452
+ "fused_rank": fused_ranks.get(chunk_id),
453
+ "path_rank": path_ranks.get(chunk_id),
454
+ "rerank_rank": rerank_ranks.get(chunk_id),
455
+ "final_rank": final_ranks.get(chunk_id),
456
+ "semantic_score": item.get("semantic_score"),
457
+ "bm25_score": item.get("bm25_score"),
458
+ "rrf_score": item.get("rrf_score"),
459
+ "path_score": item.get("path_score"),
460
+ "rerank_score": item.get("rerank_score"),
461
+ "final_score": item.get("final_score"),
462
+ }
463
+ )
464
+
465
+ retrieval_debug.sort(
466
+ key=lambda item: (
467
+ item["final_rank"] is None,
468
+ item["final_rank"] or 10**9,
469
+ item["rerank_rank"] or 10**9,
470
+ )
471
+ )
472
 
473
+ answer = self._generate_answer(repo, question, final_sources, normalized_history)
474
+ if debug_retrieval:
475
+ answer["retrieval_debug"] = retrieval_debug
476
  return answer
477
 
478
  def end_session(self, session_key: str):
 
981
  parts.append(f"Previous answer: {recent_assistant[0][:300]}")
982
  return "\n".join(parts)
983
 
984
+ @staticmethod
985
+ def _rank_map(items: List[dict]) -> Dict[str, int]:
986
+ return {item["id"]: rank for rank, item in enumerate(items, start=1)}
987
+
988
+ @staticmethod
989
+ def _minmax(values: List[float]) -> List[float]:
990
+ if not values:
991
+ return []
992
+ low = min(values)
993
+ high = max(values)
994
+ if high == low:
995
+ return [0.0 for _ in values]
996
+ return [(value - low) / (high - low) for value in values]
997
+
998
+ @staticmethod
999
+ def _source_family(file_path: str) -> str:
1000
+ parts = (file_path or "").strip("/").split("/")
1001
+ if not parts or not parts[0]:
1002
+ return ""
1003
+ if parts[0] in {"packages", "apps"} and len(parts) >= 2:
1004
+ return "/".join(parts[:2])
1005
+ return parts[0]
1006
+
1007
  def _merge_ranked_candidates(
1008
  self,
1009
  ranked_results: List[dict],
 
1112
  results: List[dict],
1113
  top_k: int,
1114
  ) -> List[dict]:
1115
+ if not results:
1116
+ return []
1117
+
1118
  combined_query = f"{question} {retrieval_query}".lower()
1119
  wants_code = any(
1120
  token in combined_query
 
1125
  "docs",
1126
  "overview",
1127
  }
1128
+ wants_repo_overview = self._is_repo_overview_question(
1129
+ question
1130
+ ) or self._is_repo_overview_question(retrieval_query)
1131
+
1132
+ rerank_norm = self._minmax([float(item.get("rerank_score", 0.0)) for item in results])
1133
+ rrf_norm = self._minmax([float(item.get("rrf_score", 0.0)) for item in results])
1134
+ path_norm = self._minmax([float(item.get("path_score", 0.0)) for item in results])
1135
+ canonical_norm = self._minmax(
1136
+ [float(self._canonical_path_priority(item, combined_query)) for item in results]
1137
  )
1138
 
1139
+ scored = []
1140
+ for index, item in enumerate(results):
1141
+ enriched = dict(item)
1142
  is_doc = self._is_doc_source(item)
1143
+ intent_bonus = 0.0
1144
+
1145
+ if wants_repo_overview and is_doc:
1146
+ intent_bonus += 1.0
1147
+ if wants_docs and is_doc:
1148
+ intent_bonus += 0.8
1149
+ if wants_code and not is_doc:
1150
+ intent_bonus += 0.5
1151
+ if (
1152
+ question_intent
1153
+ in {"api", "implementation", "cross_file", "error_handling", "setup"}
1154
+ and not is_doc
1155
+ ):
1156
+ intent_bonus += 0.4
1157
+
1158
+ enriched["final_score"] = (
1159
+ 0.50 * rerank_norm[index]
1160
+ + 0.20 * rrf_norm[index]
1161
+ + 0.12 * path_norm[index]
1162
+ + 0.10 * canonical_norm[index]
1163
+ + 0.08 * min(intent_bonus, 1.0)
1164
  )
1165
+ scored.append(enriched)
1166
 
1167
+ scored.sort(key=lambda item: item["final_score"], reverse=True)
1168
+ return scored[:top_k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
 
1170
  def _select_answer_sources(
1171
  self,
 
1179
  intent = self._question_intent(question)
1180
  max_per_file = 2 if intent in {"overview", "docs"} else 1
1181
  selected = []
1182
+ selected_ids = set()
1183
  file_counts = {}
1184
+ used_families = set()
1185
+
1186
+ if intent == "cross_file":
1187
+ for item in results:
1188
+ file_path = item.get("file_path", "")
1189
+ family = self._source_family(file_path)
1190
+ if family and family in used_families:
1191
+ continue
1192
+ selected.append(item)
1193
+ selected_ids.add(item["id"])
1194
+ if family:
1195
+ used_families.add(family)
1196
+ file_counts[file_path] = 1
1197
+ if len(selected) == top_k:
1198
+ return selected
1199
 
1200
  for item in results:
1201
+ if item["id"] in selected_ids:
1202
+ continue
1203
  file_path = item.get("file_path", "")
1204
  count = file_counts.get(file_path, 0)
1205
  if count >= max_per_file:
1206
  continue
1207
  selected.append(item)
1208
+ selected_ids.add(item["id"])
1209
  file_counts[file_path] = count + 1
1210
  if len(selected) == top_k:
1211
  break
 
1364
  @staticmethod
1365
  def _is_repo_overview_question(question: str) -> bool:
1366
  normalized = " ".join((question or "").lower().split())
1367
+ explicit_phrases = {
1368
+ "what is the repo about",
1369
+ "what is this repo about",
1370
+ "what does the repo do",
1371
+ "what does this repo do",
1372
+ "what is the repository about",
1373
+ "what does the repository do",
1374
+ "what is this project about",
1375
+ "what does this project do",
1376
+ "repo summary",
1377
+ "repository summary",
1378
+ "project summary",
1379
+ "summarize the repo",
1380
+ "summarize this repo",
1381
+ "repo overview",
1382
+ "repository overview",
1383
+ "project overview",
1384
+ }
1385
+ if any(phrase in normalized for phrase in explicit_phrases):
1386
+ return True
1387
+
1388
+ code_markers = {
1389
+ "function", "class", "method", "endpoint", "api", "router",
1390
+ "route", "implementation", "implemented", "file", "package",
1391
+ "module", "where", "tests",
1392
+ }
1393
+ if any(re.search(rf"(?<![a-z0-9]){re.escape(marker)}(?![a-z0-9])", normalized) for marker in code_markers):
1394
+ return False
1395
+
1396
+ purpose_patterns = (
1397
+ r"^what is [\w.-]+(?:\s+and\s+what\s+.+)?\??$",
1398
+ r"^what does [\w.-]+ do\??$",
1399
+ r"^what problem does [\w.-]+ solve\??$",
1400
+ r"^what product problem .+ solve\??$",
1401
+ r"^(?:describe|explain) [\w.-]+\??$",
1402
+ r"^(?:what is|explain|describe) the (?:purpose|project|repository)\b",
1403
  )
1404
+ return any(re.search(pattern, normalized) for pattern in purpose_patterns)
1405
 
1406
  @staticmethod
1407
  def _is_doc_source(item: dict) -> bool:
 
1422
 
1423
  @staticmethod
1424
  def _domain_path_hints(query: str) -> List[str]:
1425
+ """Map generic question concepts to the directory/file naming
1426
+ conventions real-world repos tend to use for that concept.
1427
+
1428
+ This intentionally stays convention-level (not tied to any single
1429
+ project's folder layout) because this system indexes arbitrary
1430
+ GitHub repositories: a hint list hardcoded to one repo's paths would
1431
+ never match anything in any other repo, silently doing nothing for
1432
+ almost every user while looking like it's helping. These patterns
1433
+ are matched as path *substrings* by the callers, so they work across
1434
+ Python/JS/TS/Go/Java/Rust project layouts without needing to know
1435
+ the specific repo's structure in advance.
1436
+ """
1437
  normalized = " ".join((query or "").lower().split())
1438
  hints = []
1439
 
1440
  def has_any(terms: set[str]) -> bool:
1441
+ return any(
1442
+ bool(re.search(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", normalized))
1443
+ for term in terms
1444
+ )
 
 
 
 
 
1445
 
1446
  rules = [
1447
  (
1448
+ {"auth", "authentication", "authorization", "login", "session", "bearer", "oauth", "jwt", "token"},
1449
+ ["auth", "authentication", "authorization", "login", "session", "middleware/auth"],
 
 
 
 
1450
  ),
1451
  (
1452
+ {"api", "endpoint", "route", "router", "controller", "handler", "rest", "graphql", "trpc"},
1453
+ ["api", "routes", "routers", "controllers", "handlers", "endpoints", "resolvers"],
 
 
 
 
 
1454
  ),
1455
  (
1456
+ {"database", "db", "sql", "orm", "migration", "migrations", "schema", "model", "models"},
1457
+ ["models", "schema", "migrations", "db", "database", "entities", "repositories", "prisma"],
 
 
 
 
 
1458
  ),
1459
  (
1460
+ {"test", "tests", "testing", "pytest", "spec", "e2e", "unit test", "integration test"},
1461
+ ["test", "tests", "__tests__", "spec", "e2e", "testing"],
 
 
 
1462
  ),
1463
  (
1464
+ {"config", "configuration", "env", "environment", "settings", "setup", "install", "installation"},
1465
  [
1466
+ "config",
1467
+ "settings",
1468
+ ".env.example",
1469
+ "readme.md",
1470
+ "package.json",
1471
+ "pyproject.toml",
1472
+ "docker-compose",
1473
+ "dockerfile",
1474
  ],
1475
  ),
1476
  (
1477
+ {"job", "jobs", "background", "worker", "workers", "queue", "task", "tasks", "cron", "scheduler"},
1478
+ ["jobs", "workers", "queue", "tasks", "scheduler"],
 
 
 
 
1479
  ),
1480
  (
1481
+ {"webhook", "webhooks", "callback", "callbacks"},
1482
+ ["webhook", "webhooks", "callbacks"],
 
 
 
1483
  ),
1484
  (
1485
+ {"email", "emails", "mailer", "notification", "notifications"},
1486
+ ["email", "mailer", "notifications", "templates"],
 
 
 
 
1487
  ),
1488
  (
1489
+ {"upload", "storage", "s3", "file", "files", "attachment", "blob"},
1490
+ ["storage", "upload", "uploads", "files", "attachments"],
 
 
 
 
1491
  ),
1492
  (
1493
+ {"cli", "command line", "command-line"},
1494
+ ["cli", "commands", "bin"],
 
 
 
 
 
1495
  ),
1496
  (
1497
+ {"ui", "frontend", "component", "components", "page", "pages", "view", "views"},
1498
+ ["components", "pages", "views", "ui", "frontend", "client", "app"],
 
 
 
 
1499
  ),
1500
  (
1501
+ {"backend", "server", "service", "services"},
1502
+ ["server", "backend", "services", "api"],
 
 
 
 
 
1503
  ),
1504
  (
1505
+ {"middleware", "interceptor"},
1506
+ ["middleware", "interceptors"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1507
  ),
1508
  (
1509
+ {"docker", "container", "deployment", "deploy", "kubernetes", "helm", "ci", "cd", "pipeline"},
1510
+ [".github/workflows", "docker", "dockerfile", "docker-compose", "helm", "deploy", "deployment", "ci"],
 
 
 
 
 
1511
  ),
1512
  (
1513
+ {"docs", "documentation", "readme"},
1514
+ ["readme.md", "docs", "documentation"],
 
 
 
 
 
1515
  ),
1516
  (
1517
+ {"util", "utils", "utility", "helper", "helpers", "common", "shared"},
1518
+ ["utils", "util", "helpers", "common", "shared", "lib"],
 
 
 
 
1519
  ),
1520
  (
1521
+ {"type", "types", "interface", "schema"},
1522
+ ["types", "type", "interfaces", "schemas"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1523
  ),
1524
  ]
1525
 
src/repo_fetcher.py CHANGED
@@ -20,6 +20,16 @@ SUPPORTED_EXTENSIONS = {
20
  ".java",
21
  ".go",
22
  ".rs",
 
 
 
 
 
 
 
 
 
 
23
  ".md",
24
  ".mdx",
25
  ".json",
@@ -32,11 +42,20 @@ SUPPORTED_EXTENSIONS = {
32
  ".prisma",
33
  }
34
 
 
 
35
  SUPPORTED_FILENAMES = {
36
  ".env.example",
37
  "Dockerfile",
38
  }
39
 
 
 
 
 
 
 
 
40
  IGNORED_FILENAMES = {
41
  "package-lock.json",
42
  "yarn.lock",
@@ -68,7 +87,7 @@ IGNORED_DIRS = {
68
  "__pycache__",
69
  }
70
 
71
- MAX_FILE_SIZE_BYTES = 250_000
72
 
73
 
74
  class RepoFetcher:
@@ -181,9 +200,10 @@ class RepoFetcher:
181
  continue
182
  if (
183
  file_path.suffix.lower() not in SUPPORTED_EXTENSIONS
 
184
  and file_path.name not in SUPPORTED_FILENAMES
185
  ):
186
  continue
187
  if file_path.stat().st_size > MAX_FILE_SIZE_BYTES:
188
  continue
189
- yield file_path
 
20
  ".java",
21
  ".go",
22
  ".rs",
23
+ ".c",
24
+ ".h",
25
+ ".cc",
26
+ ".cpp",
27
+ ".cxx",
28
+ ".hpp",
29
+ ".hh",
30
+ ".y",
31
+ ".test",
32
+ ".tcl",
33
  ".md",
34
  ".mdx",
35
  ".json",
 
42
  ".prisma",
43
  }
44
 
45
+ # Extensionless / templated files that matter for a repo even though their
46
+ # suffix (".in", no suffix, etc.) isn't a language extension on its own.
47
  SUPPORTED_FILENAMES = {
48
  ".env.example",
49
  "Dockerfile",
50
  }
51
 
52
+ # Suffixes matched in addition to SUPPORTED_EXTENSIONS, for files like
53
+ # "sqlite.h.in" or "Makefile.in" where the *last* suffix (".in") is a
54
+ # build-template marker rather than the real language.
55
+ SUPPORTED_TEMPLATE_SUFFIXES = {
56
+ ".in",
57
+ }
58
+
59
  IGNORED_FILENAMES = {
60
  "package-lock.json",
61
  "yarn.lock",
 
87
  "__pycache__",
88
  }
89
 
90
+ MAX_FILE_SIZE_BYTES = 400_000
91
 
92
 
93
  class RepoFetcher:
 
200
  continue
201
  if (
202
  file_path.suffix.lower() not in SUPPORTED_EXTENSIONS
203
+ and file_path.suffix.lower() not in SUPPORTED_TEMPLATE_SUFFIXES
204
  and file_path.name not in SUPPORTED_FILENAMES
205
  ):
206
  continue
207
  if file_path.stat().st_size > MAX_FILE_SIZE_BYTES:
208
  continue
209
+ yield file_path