codegraph-engine 3.0.0__2-py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"""Lexical search using SQLite FTS5.
|
|
2
|
+
|
|
3
|
+
Retrieval is lexical + path/symbol ranking. No semantic embeddings.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sqlite3
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from codegraph.errors import SecurityError
|
|
14
|
+
from codegraph.indexing.classifier import (
|
|
15
|
+
FileCategory,
|
|
16
|
+
classify_file,
|
|
17
|
+
is_ignored_by_codegraphignore,
|
|
18
|
+
load_codegraphignore,
|
|
19
|
+
)
|
|
20
|
+
from codegraph.indexing.scanner import (
|
|
21
|
+
IGNORED_DIRS,
|
|
22
|
+
_gitignore_patterns,
|
|
23
|
+
_ignored_by_gitignore,
|
|
24
|
+
is_binary,
|
|
25
|
+
)
|
|
26
|
+
from codegraph.security import (
|
|
27
|
+
DEFAULT_MAX_READ_BYTES,
|
|
28
|
+
contains_private_key,
|
|
29
|
+
is_sensitive_path,
|
|
30
|
+
redact_secrets,
|
|
31
|
+
safe_path,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_READABLE_TEXT_EXTENSIONS: frozenset[str] = frozenset({
|
|
35
|
+
".py",
|
|
36
|
+
".pyi",
|
|
37
|
+
".html",
|
|
38
|
+
".htm",
|
|
39
|
+
".jinja",
|
|
40
|
+
".jinja2",
|
|
41
|
+
".j2",
|
|
42
|
+
".ejs",
|
|
43
|
+
".hbs",
|
|
44
|
+
".svelte",
|
|
45
|
+
".vue",
|
|
46
|
+
".js",
|
|
47
|
+
".jsx",
|
|
48
|
+
".ts",
|
|
49
|
+
".tsx",
|
|
50
|
+
".mjs",
|
|
51
|
+
".cjs",
|
|
52
|
+
".css",
|
|
53
|
+
".scss",
|
|
54
|
+
".less",
|
|
55
|
+
".json",
|
|
56
|
+
".yaml",
|
|
57
|
+
".yml",
|
|
58
|
+
".toml",
|
|
59
|
+
".ini",
|
|
60
|
+
".cfg",
|
|
61
|
+
".conf",
|
|
62
|
+
".xml",
|
|
63
|
+
".md",
|
|
64
|
+
".rst",
|
|
65
|
+
".txt",
|
|
66
|
+
".sql",
|
|
67
|
+
".prisma",
|
|
68
|
+
".sh",
|
|
69
|
+
".bash",
|
|
70
|
+
".zsh",
|
|
71
|
+
".graphql",
|
|
72
|
+
".proto",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
_READABLE_TEXT_FILENAMES: frozenset[str] = frozenset({
|
|
76
|
+
"dockerfile",
|
|
77
|
+
"makefile",
|
|
78
|
+
"procfile",
|
|
79
|
+
"gemfile",
|
|
80
|
+
"cmakelists.txt",
|
|
81
|
+
"requirements.txt",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SearchResult:
|
|
87
|
+
file: str
|
|
88
|
+
symbol: str | None
|
|
89
|
+
start_line: int
|
|
90
|
+
end_line: int
|
|
91
|
+
score: float
|
|
92
|
+
reason: str
|
|
93
|
+
snippet: str
|
|
94
|
+
path: str = ""
|
|
95
|
+
line: int = 0
|
|
96
|
+
matched_text: str = ""
|
|
97
|
+
category: str = "SOURCE"
|
|
98
|
+
file_category: str = "SOURCE"
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
if not self.path:
|
|
102
|
+
object.__setattr__(self, "path", self.file)
|
|
103
|
+
if not self.line:
|
|
104
|
+
object.__setattr__(self, "line", self.start_line)
|
|
105
|
+
if not self.matched_text and self.snippet:
|
|
106
|
+
lines = [ln.strip() for ln in self.snippet.splitlines() if ln.strip()]
|
|
107
|
+
object.__setattr__(self, "matched_text", (lines[0][:300] if lines else ""))
|
|
108
|
+
if not self.file_category:
|
|
109
|
+
object.__setattr__(self, "file_category", self.category)
|
|
110
|
+
|
|
111
|
+
def as_dict(self) -> dict[str, object]:
|
|
112
|
+
return asdict(self)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# FTS5 reserved words that cannot appear as bare terms
|
|
116
|
+
_FTS5_RESERVED = frozenset({
|
|
117
|
+
"AND", "OR", "NOT",
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
# FTS5 special characters to strip from terms
|
|
121
|
+
_FTS5_SPECIAL = re.compile(r"[^\w]") # keep only word chars inside terms
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _safe_fts5_terms(query: str) -> list[str]:
|
|
125
|
+
"""Extract word-like tokens safe for FTS5 MATCH expressions.
|
|
126
|
+
|
|
127
|
+
- Splits on non-alphanumeric characters (dots, dashes, etc.)
|
|
128
|
+
- Drops single-character tokens
|
|
129
|
+
- Drops FTS5 reserved words
|
|
130
|
+
- Returns deduplicated list preserving order
|
|
131
|
+
"""
|
|
132
|
+
# Split on anything that is not alphanumeric or underscore
|
|
133
|
+
raw_tokens = re.split(r"[^A-Za-z0-9_]+", query)
|
|
134
|
+
seen: set[str] = set()
|
|
135
|
+
result: list[str] = []
|
|
136
|
+
for tok in raw_tokens:
|
|
137
|
+
tok = tok.strip()
|
|
138
|
+
if len(tok) < 2:
|
|
139
|
+
continue
|
|
140
|
+
if tok.upper() in _FTS5_RESERVED:
|
|
141
|
+
continue
|
|
142
|
+
if tok not in seen:
|
|
143
|
+
seen.add(tok)
|
|
144
|
+
result.append(tok)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _resolve_repository_from_con(
|
|
149
|
+
con: sqlite3.Connection,
|
|
150
|
+
repository: Path | None = None,
|
|
151
|
+
) -> Path | None:
|
|
152
|
+
"""Resolve the repository root path from explicit argument, metadata table, or database file path."""
|
|
153
|
+
if repository is not None:
|
|
154
|
+
try:
|
|
155
|
+
resolved = repository.resolve(strict=True)
|
|
156
|
+
if resolved.is_dir():
|
|
157
|
+
return resolved
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
try:
|
|
161
|
+
row = con.execute("SELECT value FROM metadata WHERE key='repository'").fetchone()
|
|
162
|
+
if row and row[0]:
|
|
163
|
+
cand = Path(str(row[0])).resolve(strict=True)
|
|
164
|
+
if cand.is_dir():
|
|
165
|
+
return cand
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
try:
|
|
169
|
+
for r in con.execute("PRAGMA database_list").fetchall():
|
|
170
|
+
if r[1] == "main" and r[2]:
|
|
171
|
+
db_path = Path(str(r[2])).resolve(strict=True)
|
|
172
|
+
if db_path.parent.is_dir():
|
|
173
|
+
return db_path.parent
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _find_best_line_in_chunk(
|
|
180
|
+
content: str,
|
|
181
|
+
chunk_start_line: int,
|
|
182
|
+
clean_query: str,
|
|
183
|
+
terms: list[str],
|
|
184
|
+
) -> tuple[int, str, bool]:
|
|
185
|
+
"""Locate the specific 1-indexed line and matched text within a chunk."""
|
|
186
|
+
lines = content.splitlines()
|
|
187
|
+
if not lines:
|
|
188
|
+
return chunk_start_line, "", False
|
|
189
|
+
|
|
190
|
+
clean_q_lower = clean_query.lower()
|
|
191
|
+
if clean_query:
|
|
192
|
+
for idx, line in enumerate(lines):
|
|
193
|
+
if clean_query in line:
|
|
194
|
+
return chunk_start_line + idx, line.strip()[:300], True
|
|
195
|
+
for idx, line in enumerate(lines):
|
|
196
|
+
if clean_q_lower in line.lower():
|
|
197
|
+
return chunk_start_line + idx, line.strip()[:300], True
|
|
198
|
+
|
|
199
|
+
if terms:
|
|
200
|
+
best_idx = 0
|
|
201
|
+
best_hits = 0
|
|
202
|
+
terms_lower = [t.lower() for t in terms]
|
|
203
|
+
for idx, line in enumerate(lines):
|
|
204
|
+
ll = line.lower()
|
|
205
|
+
hits = sum(1 for t in terms_lower if t in ll)
|
|
206
|
+
if hits > best_hits:
|
|
207
|
+
best_hits = hits
|
|
208
|
+
best_idx = idx
|
|
209
|
+
if best_hits > 0:
|
|
210
|
+
return chunk_start_line + best_idx, lines[best_idx].strip()[:300], False
|
|
211
|
+
|
|
212
|
+
return chunk_start_line, lines[0].strip()[:300], False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _search_repository_text_files(
|
|
216
|
+
con: sqlite3.Connection,
|
|
217
|
+
repo_root: Path,
|
|
218
|
+
query: str,
|
|
219
|
+
terms: list[str],
|
|
220
|
+
excluded_categories: set[str],
|
|
221
|
+
include_vendor: bool = False,
|
|
222
|
+
include_build_artifacts: bool = False,
|
|
223
|
+
max_file_bytes: int = DEFAULT_MAX_READ_BYTES,
|
|
224
|
+
) -> list[SearchResult]:
|
|
225
|
+
"""Scan readable repository text files (.py, .html, .jinja, .js, .ts, .css, .json, .yaml, .md, etc.)
|
|
226
|
+
for literal and lexical matches without polluting the semantic graph.
|
|
227
|
+
"""
|
|
228
|
+
clean_q = query.strip()
|
|
229
|
+
if not clean_q:
|
|
230
|
+
return []
|
|
231
|
+
clean_q_lower = clean_q.lower()
|
|
232
|
+
terms_lower = [t.lower() for t in terms]
|
|
233
|
+
|
|
234
|
+
gitignore = _gitignore_patterns(repo_root)
|
|
235
|
+
codegraphignore = load_codegraphignore(repo_root)
|
|
236
|
+
|
|
237
|
+
# Preload symbol spans by path so hits inside indexed files can optionally attach enclosing symbol name
|
|
238
|
+
symbols_by_path: dict[str, list[tuple[int, int, str]]] = {}
|
|
239
|
+
try:
|
|
240
|
+
for srow in con.execute(
|
|
241
|
+
"SELECT path, start_line, end_line, qualified_name FROM symbols ORDER BY path, start_line"
|
|
242
|
+
).fetchall():
|
|
243
|
+
symbols_by_path.setdefault(str(srow["path"]), []).append(
|
|
244
|
+
(int(srow["start_line"]), int(srow["end_line"]), str(srow["qualified_name"]))
|
|
245
|
+
)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
skip_dirs = set(IGNORED_DIRS)
|
|
250
|
+
if include_vendor:
|
|
251
|
+
skip_dirs -= {"node_modules", "vendor", ".venv", "venv"}
|
|
252
|
+
if include_build_artifacts:
|
|
253
|
+
skip_dirs -= {"dist", "build"}
|
|
254
|
+
|
|
255
|
+
hits: list[SearchResult] = []
|
|
256
|
+
|
|
257
|
+
for directory, dirs, files in os.walk(repo_root, followlinks=False):
|
|
258
|
+
dirs[:] = sorted(
|
|
259
|
+
d for d in dirs
|
|
260
|
+
if d not in skip_dirs and not d.startswith(".codegraph")
|
|
261
|
+
)
|
|
262
|
+
for filename in sorted(files):
|
|
263
|
+
if filename.startswith(".codegraph"):
|
|
264
|
+
continue
|
|
265
|
+
absolute = Path(directory) / filename
|
|
266
|
+
try:
|
|
267
|
+
rel_unresolved = absolute.relative_to(repo_root)
|
|
268
|
+
except ValueError:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
resolved = safe_path(repo_root, rel_unresolved)
|
|
273
|
+
except SecurityError:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
relative = resolved.relative_to(repo_root)
|
|
278
|
+
except ValueError:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
rel_posix = relative.as_posix()
|
|
282
|
+
if is_sensitive_path(relative):
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
suffix_lower = relative.suffix.lower()
|
|
286
|
+
name_lower = relative.name.lower()
|
|
287
|
+
if suffix_lower not in _READABLE_TEXT_EXTENSIONS and name_lower not in _READABLE_TEXT_FILENAMES:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
if not resolved.is_file():
|
|
292
|
+
continue
|
|
293
|
+
st_size = resolved.stat().st_size
|
|
294
|
+
if st_size == 0 or st_size > max_file_bytes:
|
|
295
|
+
continue
|
|
296
|
+
if _ignored_by_gitignore(relative, gitignore) or is_ignored_by_codegraphignore(
|
|
297
|
+
relative, codegraphignore
|
|
298
|
+
):
|
|
299
|
+
continue
|
|
300
|
+
if is_binary(resolved):
|
|
301
|
+
continue
|
|
302
|
+
raw_content = resolved.read_text(encoding="utf-8", errors="replace")
|
|
303
|
+
except OSError:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
if contains_private_key(raw_content):
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
content = redact_secrets(raw_content)
|
|
310
|
+
cat_enum = classify_file(relative, content)
|
|
311
|
+
cat_str = cat_enum.value
|
|
312
|
+
if cat_str in excluded_categories:
|
|
313
|
+
continue
|
|
314
|
+
if cat_enum == FileCategory.BINARY:
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
content_lower = content.lower()
|
|
318
|
+
has_substring = clean_q_lower in content_lower
|
|
319
|
+
has_all_terms = bool(terms_lower and len(terms_lower) >= 2 and all(t in content_lower for t in terms_lower))
|
|
320
|
+
if not has_substring and not has_all_terms:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
lines = content.splitlines()
|
|
324
|
+
path_bonus = 0.05 if (
|
|
325
|
+
clean_q_lower in rel_posix.lower()
|
|
326
|
+
or any(t in rel_posix.lower() for t in terms_lower)
|
|
327
|
+
) else 0.0
|
|
328
|
+
|
|
329
|
+
file_spans = symbols_by_path.get(rel_posix, [])
|
|
330
|
+
file_match_count = 0
|
|
331
|
+
|
|
332
|
+
for idx, raw_line in enumerate(lines):
|
|
333
|
+
line_lower = raw_line.lower()
|
|
334
|
+
if clean_q in raw_line:
|
|
335
|
+
base_score = 0.92
|
|
336
|
+
reason = "exact literal match"
|
|
337
|
+
elif clean_q_lower in line_lower:
|
|
338
|
+
base_score = 0.88
|
|
339
|
+
reason = "literal match"
|
|
340
|
+
elif terms_lower and len(terms_lower) >= 2 and all(t in line_lower for t in terms_lower):
|
|
341
|
+
base_score = 0.78
|
|
342
|
+
reason = "lexical line match"
|
|
343
|
+
else:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
line_no = idx + 1
|
|
347
|
+
enclosing_sym: str | None = None
|
|
348
|
+
for s_start, s_end, s_qname in file_spans:
|
|
349
|
+
if s_start <= line_no <= s_end:
|
|
350
|
+
enclosing_sym = s_qname
|
|
351
|
+
|
|
352
|
+
sym_bonus = 0.03 if (
|
|
353
|
+
enclosing_sym
|
|
354
|
+
and (
|
|
355
|
+
clean_q_lower in enclosing_sym.lower()
|
|
356
|
+
or any(t in enclosing_sym.lower() for t in terms_lower)
|
|
357
|
+
)
|
|
358
|
+
) else 0.0
|
|
359
|
+
if sym_bonus > 0:
|
|
360
|
+
reason += ", symbol match"
|
|
361
|
+
if path_bonus > 0:
|
|
362
|
+
reason += ", path match"
|
|
363
|
+
|
|
364
|
+
score = round(min(1.0, base_score + path_bonus + sym_bonus), 2)
|
|
365
|
+
win_start = max(1, line_no - 2)
|
|
366
|
+
win_end = min(len(lines), line_no + 2)
|
|
367
|
+
snippet = redact_secrets("\n".join(lines[win_start - 1 : win_end])[:900])
|
|
368
|
+
matched_text = redact_secrets(raw_line.strip()[:300])
|
|
369
|
+
|
|
370
|
+
hits.append(
|
|
371
|
+
SearchResult(
|
|
372
|
+
file=rel_posix,
|
|
373
|
+
symbol=enclosing_sym,
|
|
374
|
+
start_line=win_start,
|
|
375
|
+
end_line=win_end,
|
|
376
|
+
score=score,
|
|
377
|
+
reason=reason,
|
|
378
|
+
snippet=snippet,
|
|
379
|
+
path=rel_posix,
|
|
380
|
+
line=line_no,
|
|
381
|
+
matched_text=matched_text,
|
|
382
|
+
category=cat_str,
|
|
383
|
+
file_category=cat_str,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
file_match_count += 1
|
|
387
|
+
if file_match_count >= 5:
|
|
388
|
+
break
|
|
389
|
+
|
|
390
|
+
return hits
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def search(
|
|
394
|
+
con: sqlite3.Connection,
|
|
395
|
+
query: str,
|
|
396
|
+
top_k: int = 10,
|
|
397
|
+
include_generated: bool = False,
|
|
398
|
+
include_vendor: bool = False,
|
|
399
|
+
include_bundles: bool = False,
|
|
400
|
+
include_build_artifacts: bool = False,
|
|
401
|
+
repository: Path | None = None,
|
|
402
|
+
include_text_files: bool = True,
|
|
403
|
+
) -> list[SearchResult]:
|
|
404
|
+
clean_query = query.strip()
|
|
405
|
+
if not clean_query:
|
|
406
|
+
return []
|
|
407
|
+
|
|
408
|
+
terms = _safe_fts5_terms(clean_query)
|
|
409
|
+
|
|
410
|
+
# Determine excluded categories
|
|
411
|
+
excluded: list[str] = ["BINARY"]
|
|
412
|
+
if not include_generated:
|
|
413
|
+
excluded.append("GENERATED")
|
|
414
|
+
if not include_vendor:
|
|
415
|
+
excluded.append("VENDOR")
|
|
416
|
+
if not include_bundles:
|
|
417
|
+
excluded.extend(["BUNDLE", "MINIFIED"])
|
|
418
|
+
if not include_build_artifacts:
|
|
419
|
+
excluded.append("BUILD_ARTIFACT")
|
|
420
|
+
|
|
421
|
+
rows: list[sqlite3.Row] = []
|
|
422
|
+
if terms:
|
|
423
|
+
clauses = " OR ".join(terms)
|
|
424
|
+
try:
|
|
425
|
+
if not excluded:
|
|
426
|
+
rows = con.execute(
|
|
427
|
+
"SELECT c.path, c.symbol, c.start_line, c.end_line, c.content, "
|
|
428
|
+
"f.category AS category, bm25(chunks_fts) AS rank "
|
|
429
|
+
"FROM chunks_fts "
|
|
430
|
+
"JOIN chunks c ON c.id = chunks_fts.rowid "
|
|
431
|
+
"LEFT JOIN files f ON f.path = c.path "
|
|
432
|
+
"WHERE chunks_fts MATCH ? ORDER BY rank LIMIT ?",
|
|
433
|
+
(clauses, top_k * 3),
|
|
434
|
+
).fetchall()
|
|
435
|
+
else:
|
|
436
|
+
placeholders = ",".join("?" for _ in excluded)
|
|
437
|
+
rows = con.execute(
|
|
438
|
+
"SELECT c.path, c.symbol, c.start_line, c.end_line, c.content, "
|
|
439
|
+
"f.category AS category, bm25(chunks_fts) AS rank "
|
|
440
|
+
"FROM chunks_fts "
|
|
441
|
+
"JOIN chunks c ON c.id = chunks_fts.rowid "
|
|
442
|
+
"LEFT JOIN files f ON f.path = c.path "
|
|
443
|
+
f"WHERE chunks_fts MATCH ? AND (f.category IS NULL OR f.category NOT IN ({placeholders})) "
|
|
444
|
+
"ORDER BY rank LIMIT ?",
|
|
445
|
+
(clauses, *excluded, top_k * 3),
|
|
446
|
+
).fetchall()
|
|
447
|
+
except sqlite3.OperationalError:
|
|
448
|
+
try:
|
|
449
|
+
rows = con.execute(
|
|
450
|
+
"SELECT c.path, c.symbol, c.start_line, c.end_line, c.content, "
|
|
451
|
+
"bm25(chunks_fts) AS rank "
|
|
452
|
+
"FROM chunks_fts "
|
|
453
|
+
"JOIN chunks c ON c.id = chunks_fts.rowid "
|
|
454
|
+
"WHERE chunks_fts MATCH ? ORDER BY rank LIMIT ?",
|
|
455
|
+
(clauses, top_k * 3),
|
|
456
|
+
).fetchall()
|
|
457
|
+
except sqlite3.OperationalError:
|
|
458
|
+
rows = []
|
|
459
|
+
|
|
460
|
+
results: list[SearchResult] = []
|
|
461
|
+
for row in rows:
|
|
462
|
+
rel_path = str(row["path"])
|
|
463
|
+
if is_sensitive_path(rel_path):
|
|
464
|
+
continue
|
|
465
|
+
path_bonus = 1.0 if any(t.lower() in rel_path.lower() for t in terms) else 0.0
|
|
466
|
+
symbol_bonus = (
|
|
467
|
+
1.0
|
|
468
|
+
if row["symbol"] and any(t.lower() in str(row["symbol"]).lower() for t in terms)
|
|
469
|
+
else 0.0
|
|
470
|
+
)
|
|
471
|
+
# Exact symbol match (highest bonus)
|
|
472
|
+
exact_bonus = (
|
|
473
|
+
0.5
|
|
474
|
+
if row["symbol"] and any(
|
|
475
|
+
t.lower() == str(row["symbol"]).lower()
|
|
476
|
+
or t.lower() == str(row["symbol"]).split(".")[-1].lower()
|
|
477
|
+
for t in terms
|
|
478
|
+
)
|
|
479
|
+
else 0.0
|
|
480
|
+
)
|
|
481
|
+
content_str = redact_secrets(str(row["content"]))
|
|
482
|
+
if (
|
|
483
|
+
clean_query.lower() not in content_str.lower()
|
|
484
|
+
and not any(t.lower() in content_str.lower() for t in terms)
|
|
485
|
+
and not path_bonus
|
|
486
|
+
and not symbol_bonus
|
|
487
|
+
):
|
|
488
|
+
continue
|
|
489
|
+
start_ln = int(row["start_line"])
|
|
490
|
+
end_ln = int(row["end_line"])
|
|
491
|
+
hit_line, matched_text, has_literal = _find_best_line_in_chunk(
|
|
492
|
+
content_str, start_ln, clean_query, terms
|
|
493
|
+
)
|
|
494
|
+
matched_text = redact_secrets(matched_text)
|
|
495
|
+
literal_bonus = 0.15 if has_literal else 0.0
|
|
496
|
+
score = round(
|
|
497
|
+
min(1.0, 0.3 + path_bonus * 0.15 + symbol_bonus * 0.2 + exact_bonus * 0.35 + literal_bonus),
|
|
498
|
+
2,
|
|
499
|
+
)
|
|
500
|
+
reason = "exact literal match" if (clean_query in content_str) else (
|
|
501
|
+
"literal match" if has_literal else "lexical match"
|
|
502
|
+
)
|
|
503
|
+
if symbol_bonus:
|
|
504
|
+
reason += ", symbol match"
|
|
505
|
+
if path_bonus:
|
|
506
|
+
reason += ", path match"
|
|
507
|
+
snippet = redact_secrets(content_str[:900])
|
|
508
|
+
row_keys = row.keys() if hasattr(row, "keys") else []
|
|
509
|
+
cat_val = str(row["category"]) if ("category" in row_keys and row["category"]) else classify_file(rel_path).value
|
|
510
|
+
results.append(
|
|
511
|
+
SearchResult(
|
|
512
|
+
file=rel_path,
|
|
513
|
+
symbol=row["symbol"],
|
|
514
|
+
start_line=start_ln,
|
|
515
|
+
end_line=end_ln,
|
|
516
|
+
score=score,
|
|
517
|
+
reason=reason,
|
|
518
|
+
snippet=snippet,
|
|
519
|
+
path=rel_path,
|
|
520
|
+
line=hit_line,
|
|
521
|
+
matched_text=matched_text,
|
|
522
|
+
category=cat_val,
|
|
523
|
+
file_category=cat_val,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
if include_text_files:
|
|
528
|
+
repo_root = _resolve_repository_from_con(con, repository=repository)
|
|
529
|
+
if repo_root is not None:
|
|
530
|
+
text_hits = _search_repository_text_files(
|
|
531
|
+
con=con,
|
|
532
|
+
repo_root=repo_root,
|
|
533
|
+
query=clean_query,
|
|
534
|
+
terms=terms,
|
|
535
|
+
excluded_categories=set(excluded),
|
|
536
|
+
include_vendor=include_vendor,
|
|
537
|
+
include_build_artifacts=include_build_artifacts,
|
|
538
|
+
)
|
|
539
|
+
for th in text_hits:
|
|
540
|
+
covered = False
|
|
541
|
+
for idx, existing in enumerate(results):
|
|
542
|
+
if (
|
|
543
|
+
existing.file == th.file
|
|
544
|
+
and existing.start_line <= th.line <= existing.end_line
|
|
545
|
+
):
|
|
546
|
+
covered = True
|
|
547
|
+
if th.score > existing.score:
|
|
548
|
+
results[idx] = SearchResult(
|
|
549
|
+
file=existing.file,
|
|
550
|
+
symbol=existing.symbol or th.symbol,
|
|
551
|
+
start_line=existing.start_line,
|
|
552
|
+
end_line=existing.end_line,
|
|
553
|
+
score=th.score,
|
|
554
|
+
reason=th.reason,
|
|
555
|
+
snippet=existing.snippet,
|
|
556
|
+
path=existing.path,
|
|
557
|
+
line=th.line,
|
|
558
|
+
matched_text=th.matched_text,
|
|
559
|
+
category=existing.category,
|
|
560
|
+
file_category=existing.file_category,
|
|
561
|
+
)
|
|
562
|
+
break
|
|
563
|
+
if not covered:
|
|
564
|
+
results.append(th)
|
|
565
|
+
|
|
566
|
+
# Deterministic ordering: (-score, path, line, start_line)
|
|
567
|
+
results.sort(key=lambda r: (-r.score, r.path, r.line, r.start_line))
|
|
568
|
+
return results[:top_k]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def search_lexical(con: sqlite3.Connection, query: str, top_k: int = 10) -> list[SearchResult]:
|
|
572
|
+
"""Lexical FTS5 search (wrapper around search)."""
|
|
573
|
+
return search(con, query, top_k=top_k)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def search_code(
|
|
577
|
+
con: sqlite3.Connection,
|
|
578
|
+
query: str,
|
|
579
|
+
repo_path: Path | None = None,
|
|
580
|
+
repository: Path | None = None,
|
|
581
|
+
path_filter: str | None = None,
|
|
582
|
+
file_types: list[str] | tuple[str, ...] | None = None,
|
|
583
|
+
include_tests: bool = True,
|
|
584
|
+
include_configs: bool = True,
|
|
585
|
+
max_results: int = 20,
|
|
586
|
+
top_k: int | None = None,
|
|
587
|
+
include_generated: bool = False,
|
|
588
|
+
include_vendor: bool = False,
|
|
589
|
+
include_bundles: bool = False,
|
|
590
|
+
include_build_artifacts: bool = False,
|
|
591
|
+
) -> list[dict[str, object]]:
|
|
592
|
+
"""Deterministic repository text search across code, templates, UI files, configs, and docs.
|
|
593
|
+
|
|
594
|
+
Never creates or emits semantic graph relationships.
|
|
595
|
+
"""
|
|
596
|
+
clean_query = query.strip()
|
|
597
|
+
if not clean_query:
|
|
598
|
+
return []
|
|
599
|
+
|
|
600
|
+
effective_limit = top_k if top_k is not None else max_results
|
|
601
|
+
if effective_limit <= 0:
|
|
602
|
+
return []
|
|
603
|
+
|
|
604
|
+
effective_repo = repo_path if repo_path is not None else repository
|
|
605
|
+
raw_hits = search(
|
|
606
|
+
con,
|
|
607
|
+
clean_query,
|
|
608
|
+
top_k=max(effective_limit * 5, 100),
|
|
609
|
+
include_generated=include_generated,
|
|
610
|
+
include_vendor=include_vendor,
|
|
611
|
+
include_bundles=include_bundles,
|
|
612
|
+
include_build_artifacts=include_build_artifacts,
|
|
613
|
+
repository=effective_repo,
|
|
614
|
+
include_text_files=True,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
norm_path_filter = path_filter.strip().lower() if path_filter and path_filter.strip() else None
|
|
618
|
+
norm_types: set[str] | None = None
|
|
619
|
+
if file_types:
|
|
620
|
+
cleaned_types = {ft.strip().lower().lstrip(".") for ft in file_types if ft and ft.strip()}
|
|
621
|
+
if cleaned_types:
|
|
622
|
+
norm_types = cleaned_types
|
|
623
|
+
|
|
624
|
+
config_exts = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"}
|
|
625
|
+
filtered: list[dict[str, object]] = []
|
|
626
|
+
seen_file_lines: set[tuple[str, int]] = set()
|
|
627
|
+
files_with_exact_matches: set[str] = set()
|
|
628
|
+
|
|
629
|
+
query_terms = _safe_fts5_terms(clean_query)
|
|
630
|
+
query_terms_lower = [t.lower() for t in query_terms]
|
|
631
|
+
|
|
632
|
+
for item in raw_hits:
|
|
633
|
+
rel_path = item.path or item.file
|
|
634
|
+
if is_sensitive_path(rel_path):
|
|
635
|
+
continue
|
|
636
|
+
p_obj = Path(rel_path)
|
|
637
|
+
ext_no_dot = p_obj.suffix.lower().lstrip(".")
|
|
638
|
+
cat_upper = (item.category or item.file_category or "SOURCE").upper()
|
|
639
|
+
|
|
640
|
+
if norm_path_filter and norm_path_filter not in rel_path.lower():
|
|
641
|
+
continue
|
|
642
|
+
if norm_types is not None and ext_no_dot not in norm_types:
|
|
643
|
+
continue
|
|
644
|
+
if not include_tests:
|
|
645
|
+
is_test_file = (
|
|
646
|
+
cat_upper == "TEST"
|
|
647
|
+
or "tests" in p_obj.parts
|
|
648
|
+
or p_obj.name.startswith("test_")
|
|
649
|
+
or p_obj.name.endswith("_test.py")
|
|
650
|
+
)
|
|
651
|
+
if is_test_file:
|
|
652
|
+
continue
|
|
653
|
+
if not include_configs:
|
|
654
|
+
is_config_file = cat_upper == "CONFIG" or p_obj.suffix.lower() in config_exts
|
|
655
|
+
if is_config_file:
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
d = item.as_dict()
|
|
659
|
+
# Add match_type for schema clarity
|
|
660
|
+
reason_str = str(d.get("reason", ""))
|
|
661
|
+
if "exact literal" in reason_str:
|
|
662
|
+
d["match_type"] = "exact"
|
|
663
|
+
elif "literal" in reason_str:
|
|
664
|
+
d["match_type"] = "case_insensitive"
|
|
665
|
+
else:
|
|
666
|
+
d["match_type"] = "token"
|
|
667
|
+
|
|
668
|
+
hit_line = item.line
|
|
669
|
+
file_line_key = (rel_path, hit_line)
|
|
670
|
+
if file_line_key in seen_file_lines:
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
# If multi-word query and this hit is only a token match:
|
|
674
|
+
# verify that all query terms appear in the snippet or matched text
|
|
675
|
+
if len(query_terms_lower) >= 2 and d["match_type"] == "token":
|
|
676
|
+
haystack = (str(d.get("snippet", "")) + " " + str(d.get("matched_text", ""))).lower()
|
|
677
|
+
if not all(t in haystack for t in query_terms_lower):
|
|
678
|
+
continue
|
|
679
|
+
|
|
680
|
+
# If a file already had an exact/case-insensitive match, discard subsequent loose token matches
|
|
681
|
+
if rel_path in files_with_exact_matches and d["match_type"] == "token":
|
|
682
|
+
continue
|
|
683
|
+
|
|
684
|
+
if d["match_type"] in ("exact", "case_insensitive"):
|
|
685
|
+
files_with_exact_matches.add(rel_path)
|
|
686
|
+
|
|
687
|
+
seen_file_lines.add(file_line_key)
|
|
688
|
+
filtered.append(d)
|
|
689
|
+
if len(filtered) >= effective_limit:
|
|
690
|
+
break
|
|
691
|
+
|
|
692
|
+
return filtered
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _fetch_snippet(
|
|
697
|
+
con: sqlite3.Connection, path: str, start: int, end: int, symbol: str | None = None
|
|
698
|
+
) -> str:
|
|
699
|
+
try:
|
|
700
|
+
row = con.execute(
|
|
701
|
+
"SELECT content FROM chunks WHERE path=? AND start_line<=? AND end_line>=? LIMIT 1",
|
|
702
|
+
(path, start, end),
|
|
703
|
+
).fetchone()
|
|
704
|
+
if row and row["content"]:
|
|
705
|
+
return redact_secrets(str(row["content"])[:900])
|
|
706
|
+
if symbol:
|
|
707
|
+
row = con.execute(
|
|
708
|
+
"SELECT content FROM chunks WHERE symbol=? LIMIT 1", (symbol,)
|
|
709
|
+
).fetchone()
|
|
710
|
+
if row and row["content"]:
|
|
711
|
+
return redact_secrets(str(row["content"])[:900])
|
|
712
|
+
except sqlite3.OperationalError:
|
|
713
|
+
pass
|
|
714
|
+
return ""
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def search_exact_canonical_id(
|
|
718
|
+
con: sqlite3.Connection, canonical_id: str
|
|
719
|
+
) -> list[SearchResult]:
|
|
720
|
+
"""Look up symbol by exact canonical ID."""
|
|
721
|
+
clean = canonical_id.strip()
|
|
722
|
+
if not clean:
|
|
723
|
+
return []
|
|
724
|
+
try:
|
|
725
|
+
rows = con.execute(
|
|
726
|
+
"SELECT canonical_id, qualified_name, path, start_line, end_line "
|
|
727
|
+
"FROM symbols WHERE canonical_id=? ORDER BY path, start_line",
|
|
728
|
+
(clean,),
|
|
729
|
+
).fetchall()
|
|
730
|
+
except sqlite3.OperationalError:
|
|
731
|
+
return []
|
|
732
|
+
|
|
733
|
+
results: list[SearchResult] = []
|
|
734
|
+
for r in rows:
|
|
735
|
+
snippet = _fetch_snippet(con, r["path"], r["start_line"], r["end_line"], r["qualified_name"])
|
|
736
|
+
results.append(
|
|
737
|
+
SearchResult(
|
|
738
|
+
file=r["path"],
|
|
739
|
+
symbol=r["qualified_name"] or r["canonical_id"],
|
|
740
|
+
start_line=r["start_line"],
|
|
741
|
+
end_line=r["end_line"],
|
|
742
|
+
score=1.0,
|
|
743
|
+
reason="exact canonical id match",
|
|
744
|
+
snippet=snippet,
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
return results
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def search_exact_qualified(
|
|
751
|
+
con: sqlite3.Connection, qualified_name: str
|
|
752
|
+
) -> list[SearchResult]:
|
|
753
|
+
"""Look up symbol by exact qualified name."""
|
|
754
|
+
clean = qualified_name.strip()
|
|
755
|
+
if not clean:
|
|
756
|
+
return []
|
|
757
|
+
try:
|
|
758
|
+
rows = con.execute(
|
|
759
|
+
"SELECT canonical_id, qualified_name, path, start_line, end_line "
|
|
760
|
+
"FROM symbols WHERE qualified_name=? OR canonical_id=? ORDER BY path, start_line",
|
|
761
|
+
(clean, clean),
|
|
762
|
+
).fetchall()
|
|
763
|
+
except sqlite3.OperationalError:
|
|
764
|
+
return []
|
|
765
|
+
|
|
766
|
+
results: list[SearchResult] = []
|
|
767
|
+
for r in rows:
|
|
768
|
+
snippet = _fetch_snippet(con, r["path"], r["start_line"], r["end_line"], r["qualified_name"])
|
|
769
|
+
results.append(
|
|
770
|
+
SearchResult(
|
|
771
|
+
file=r["path"],
|
|
772
|
+
symbol=r["qualified_name"],
|
|
773
|
+
start_line=r["start_line"],
|
|
774
|
+
end_line=r["end_line"],
|
|
775
|
+
score=0.95,
|
|
776
|
+
reason="exact qualified match",
|
|
777
|
+
snippet=snippet,
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
return results
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def search_exact_symbol(
|
|
784
|
+
con: sqlite3.Connection, name: str
|
|
785
|
+
) -> list[SearchResult]:
|
|
786
|
+
"""Look up symbol by short identifier name."""
|
|
787
|
+
clean = name.strip()
|
|
788
|
+
if not clean:
|
|
789
|
+
return []
|
|
790
|
+
try:
|
|
791
|
+
rows = con.execute(
|
|
792
|
+
"SELECT canonical_id, qualified_name, path, start_line, end_line "
|
|
793
|
+
"FROM symbols WHERE name=? ORDER BY path, start_line",
|
|
794
|
+
(clean,),
|
|
795
|
+
).fetchall()
|
|
796
|
+
except sqlite3.OperationalError:
|
|
797
|
+
return []
|
|
798
|
+
|
|
799
|
+
results: list[SearchResult] = []
|
|
800
|
+
for r in rows:
|
|
801
|
+
snippet = _fetch_snippet(con, r["path"], r["start_line"], r["end_line"], r["qualified_name"])
|
|
802
|
+
results.append(
|
|
803
|
+
SearchResult(
|
|
804
|
+
file=r["path"],
|
|
805
|
+
symbol=r["qualified_name"],
|
|
806
|
+
start_line=r["start_line"],
|
|
807
|
+
end_line=r["end_line"],
|
|
808
|
+
score=0.90,
|
|
809
|
+
reason="exact symbol match",
|
|
810
|
+
snippet=snippet,
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
return results
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def search_route(
|
|
817
|
+
con: sqlite3.Connection, route_or_endpoint: str
|
|
818
|
+
) -> list[SearchResult]:
|
|
819
|
+
"""Look up framework route by path or endpoint ID."""
|
|
820
|
+
clean = route_or_endpoint.strip()
|
|
821
|
+
if not clean:
|
|
822
|
+
return []
|
|
823
|
+
try:
|
|
824
|
+
rows = con.execute(
|
|
825
|
+
"SELECT route_path, http_method, handler_name, file_path, line, endpoint_id, evidence "
|
|
826
|
+
"FROM framework_routes WHERE route_path=? OR endpoint_id=? ORDER BY file_path, line",
|
|
827
|
+
(clean, clean),
|
|
828
|
+
).fetchall()
|
|
829
|
+
except sqlite3.OperationalError:
|
|
830
|
+
return []
|
|
831
|
+
|
|
832
|
+
results: list[SearchResult] = []
|
|
833
|
+
for r in rows:
|
|
834
|
+
snippet = _fetch_snippet(con, r["file_path"], r["line"], r["line"], r["handler_name"])
|
|
835
|
+
results.append(
|
|
836
|
+
SearchResult(
|
|
837
|
+
file=r["file_path"],
|
|
838
|
+
symbol=r["handler_name"] or r["route_path"],
|
|
839
|
+
start_line=r["line"],
|
|
840
|
+
end_line=r["line"],
|
|
841
|
+
score=0.95,
|
|
842
|
+
reason=f"route match: {r['http_method']} {r['route_path']}",
|
|
843
|
+
snippet=snippet,
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
return results
|