SourceIndex 0.1.0__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.
- sourceindex/__init__.py +30 -0
- sourceindex/build/__init__.py +592 -0
- sourceindex/build/indexer.py +403 -0
- sourceindex/build/linerange/__init__.py +24 -0
- sourceindex/build/linerange/python_ast.py +155 -0
- sourceindex/build/linerange/treesitter.py +397 -0
- sourceindex/build/prompts.py +76 -0
- sourceindex/build/state.py +261 -0
- sourceindex/build/walker.py +94 -0
- sourceindex/claudecode/__init__.py +0 -0
- sourceindex/claudecode/savings.py +325 -0
- sourceindex/claudecode/savings_summary.py +88 -0
- sourceindex/claudecode/statusline.py +116 -0
- sourceindex/cli/__init__.py +304 -0
- sourceindex/cli/__main__.py +10 -0
- sourceindex/cli/api_key.py +171 -0
- sourceindex/cli/commands.py +678 -0
- sourceindex/cli/install.py +378 -0
- sourceindex/daemon/__init__.py +83 -0
- sourceindex/daemon/client.py +141 -0
- sourceindex/daemon/crypto.py +48 -0
- sourceindex/daemon/keyring_store.py +129 -0
- sourceindex/daemon/lifecycle.py +237 -0
- sourceindex/daemon/protocol.py +134 -0
- sourceindex/daemon/server.py +389 -0
- sourceindex/daemon/store.py +426 -0
- sourceindex/lib/__init__.py +0 -0
- sourceindex/lib/backend.py +240 -0
- sourceindex/lib/cost.py +96 -0
- sourceindex/lib/env.py +30 -0
- sourceindex/lib/git.py +33 -0
- sourceindex/lib/languages.py +406 -0
- sourceindex/lib/llm.py +298 -0
- sourceindex/lib/log.py +228 -0
- sourceindex/lib/registry.py +75 -0
- sourceindex/lib/timing.py +10 -0
- sourceindex/search/__init__.py +251 -0
- sourceindex/search/experiments.py +276 -0
- sourceindex/search/imports.py +155 -0
- sourceindex/search/passes.py +51 -0
- sourceindex/search/prompts.py +379 -0
- sourceindex/search/roadmap.py +265 -0
- sourceindex/server.py +127 -0
- sourceindex-0.1.0.dist-info/METADATA +73 -0
- sourceindex-0.1.0.dist-info/RECORD +47 -0
- sourceindex-0.1.0.dist-info/WHEEL +4 -0
- sourceindex-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Tree-sitter line-range fixer for non-Python languages.
|
|
2
|
+
|
|
3
|
+
Mirrors what ``fix_python_line_ranges`` (in the sibling ``python_ast``
|
|
4
|
+
module) does for Python via the stdlib ``ast`` module: replaces LLM-emitted
|
|
5
|
+
(Lstart-Lend) tuples with parser-truth ranges so the tier-2 details file
|
|
6
|
+
points the agent's Read at the correct lines.
|
|
7
|
+
|
|
8
|
+
Each grammar is shipped as its own per-language wheel
|
|
9
|
+
(``tree-sitter-go``, ``tree-sitter-rust``, …) — a few hundred KB each — and
|
|
10
|
+
declared as default deps in ``pyproject.toml``. The bulk
|
|
11
|
+
``tree-sitter-language-pack`` (~180MB) was dropped because it bundled
|
|
12
|
+
50+ grammars when we only use 9.
|
|
13
|
+
|
|
14
|
+
Languages added to ``LANG_CFG`` later that aren't yet shipped get an
|
|
15
|
+
optional dynamic-install path: set ``SOURCEINDEX_AUTO_INSTALL_GRAMMARS=1``
|
|
16
|
+
and a missing grammar will be ``pip install``-ed on first use, then
|
|
17
|
+
imported. Off by default so ``sourceindex`` never makes surprise network
|
|
18
|
+
calls.
|
|
19
|
+
|
|
20
|
+
Design notes:
|
|
21
|
+
- One generic walker, parameterized by per-language definition node types
|
|
22
|
+
+ name-extraction heuristics. ~50 LOC of language-specific config.
|
|
23
|
+
- LLM tier-2 entries are matched by qualified name with progressive suffix
|
|
24
|
+
fallback (e.g. ``mypkg.Foo.bar`` → ``Foo.bar`` → ``bar``) and well-known
|
|
25
|
+
trailing-suffix normalization (``.get``/``.set``/``.<init>``/``.constructor``/
|
|
26
|
+
``.closure``/``.lambda``) so package prefixes the LLM invents and accessor
|
|
27
|
+
syntax don't sink the match rate.
|
|
28
|
+
- When tree-sitter finds no match for a name, we leave the LLM's range as-is
|
|
29
|
+
rather than dropping the entry. The agent still gets the description and
|
|
30
|
+
rough location even when the parser can't anchor it exactly.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import importlib
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
import subprocess
|
|
38
|
+
import sys
|
|
39
|
+
from typing import Dict, List, Optional, Tuple
|
|
40
|
+
|
|
41
|
+
from ...lib.log import get_logger
|
|
42
|
+
|
|
43
|
+
_log = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
# (sourceindex language key) ->
|
|
46
|
+
# (tree-sitter grammar name, definition node types,
|
|
47
|
+
# python module attr returning the Language handle)
|
|
48
|
+
# The attr is usually ``language``; typescript's wheel exposes
|
|
49
|
+
# ``language_typescript`` and ``language_tsx`` instead.
|
|
50
|
+
LANG_CFG: Dict[str, Tuple[str, Tuple[str, ...], str]] = {
|
|
51
|
+
"go": ("go", ("function_declaration","method_declaration","type_declaration","type_spec","var_declaration","const_declaration"), "language"),
|
|
52
|
+
"typescript": ("typescript", ("function_declaration","method_definition","class_declaration","interface_declaration","type_alias_declaration","lexical_declaration","function_signature","method_signature","abstract_method_signature","public_field_definition","enum_declaration"), "language_typescript"),
|
|
53
|
+
"javascript": ("javascript", ("function_declaration","method_definition","class_declaration","lexical_declaration","field_definition"), "language"),
|
|
54
|
+
"rust": ("rust", ("function_item","impl_item","struct_item","trait_item","enum_item","mod_item","static_item","const_item","type_item","macro_definition"), "language"),
|
|
55
|
+
"cpp": ("cpp", ("function_definition","class_specifier","struct_specifier","namespace_definition","field_declaration","template_declaration","declaration","enum_specifier"), "language"),
|
|
56
|
+
"c": ("c", ("function_definition","struct_specifier","enum_specifier","declaration"), "language"),
|
|
57
|
+
"java": ("java", ("class_declaration","method_declaration","constructor_declaration","field_declaration","interface_declaration","enum_declaration","record_declaration"), "language"),
|
|
58
|
+
"ruby": ("ruby", ("class","module","method","singleton_method"), "language"),
|
|
59
|
+
"kotlin": ("kotlin", ("class_declaration","function_declaration","property_declaration","object_declaration","secondary_constructor","companion_object"), "language"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_IDENT_TYPES = {
|
|
63
|
+
"identifier","type_identifier","field_identifier","scoped_identifier",
|
|
64
|
+
"property_identifier","simple_identifier","constant",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_CONTAINER_TYPES = {
|
|
68
|
+
"class_declaration","class_specifier","struct_specifier","namespace_definition",
|
|
69
|
+
"impl_item","trait_item","mod_item","object_declaration","companion_object",
|
|
70
|
+
"interface_declaration","class","module","enum_declaration","enum_specifier",
|
|
71
|
+
"record_declaration",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_TRAILING_SUFFIXES = (".get",".set",".<init>",".init",".constructor",".ctor",".closure",".lambda",".function_")
|
|
75
|
+
|
|
76
|
+
_ENTRY_RE = re.compile(
|
|
77
|
+
r"^(?P<lead>\s*)(?P<name>[^()]+?)\s*\(\s*L?(?P<s>\d+)\s*-\s*L?(?P<e>\d+)\s*\)\s*:(?P<rest>.*)$"
|
|
78
|
+
)
|
|
79
|
+
_ENTRY_SINGLE_RE = re.compile(
|
|
80
|
+
r"^(?P<lead>\s*)(?P<name>[^()]+?)\s*\(\s*L?(?P<line>\d+)\s*\)\s*:(?P<rest>.*)$"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_AUTO_INSTALL_ENV = "SOURCEINDEX_AUTO_INSTALL_GRAMMARS"
|
|
85
|
+
|
|
86
|
+
# Per-grammar caches: parser objects (hot path) and warned-once set (so a
|
|
87
|
+
# missing package doesn't spam stderr once per file in a multi-thousand-file
|
|
88
|
+
# repo). Keyed by sourceindex language key, not grammar name, since LANG_CFG
|
|
89
|
+
# is the source of truth for both.
|
|
90
|
+
_PARSER_CACHE: Dict[str, object] = {}
|
|
91
|
+
_WARNED_GRAMMARS: set[str] = set()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _warn_missing_grammar(language_key: str, grammar: str, exc: BaseException) -> None:
|
|
95
|
+
if language_key in _WARNED_GRAMMARS:
|
|
96
|
+
return
|
|
97
|
+
_WARNED_GRAMMARS.add(language_key)
|
|
98
|
+
pkg = f"tree-sitter-{grammar}"
|
|
99
|
+
_log.warning(
|
|
100
|
+
f"grammar package '{pkg}' not available for {language_key} "
|
|
101
|
+
f"({type(exc).__name__}); tier-2 line ranges for this language will use "
|
|
102
|
+
f"LLM output as-is (less accurate). Install with `pip install {pkg}` "
|
|
103
|
+
f"or set {_AUTO_INSTALL_ENV}=1 to auto-install on first use.",
|
|
104
|
+
extra={
|
|
105
|
+
"event": "tree_sitter.missing_grammar",
|
|
106
|
+
"language": language_key,
|
|
107
|
+
"package": pkg,
|
|
108
|
+
"error_type": type(exc).__name__,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _try_pip_install(pkg: str) -> bool:
|
|
114
|
+
"""Best-effort `pip install` of a single package. Returns True on success.
|
|
115
|
+
|
|
116
|
+
Gated by the caller — never invoked unless ``SOURCEINDEX_AUTO_INSTALL_GRAMMARS=1``
|
|
117
|
+
so default usage makes no network calls.
|
|
118
|
+
"""
|
|
119
|
+
_log.info(
|
|
120
|
+
f"{_AUTO_INSTALL_ENV}=1: installing {pkg}…",
|
|
121
|
+
extra={"event": "tree_sitter.install_attempt", "package": pkg},
|
|
122
|
+
)
|
|
123
|
+
try:
|
|
124
|
+
subprocess.run(
|
|
125
|
+
[sys.executable, "-m", "pip", "install", "--quiet", pkg],
|
|
126
|
+
check=True,
|
|
127
|
+
)
|
|
128
|
+
return True
|
|
129
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
130
|
+
_log.error(
|
|
131
|
+
f"auto-install of {pkg} failed: {e}",
|
|
132
|
+
extra={
|
|
133
|
+
"event": "tree_sitter.install_failed",
|
|
134
|
+
"package": pkg,
|
|
135
|
+
"error_type": type(e).__name__,
|
|
136
|
+
"error": str(e),
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_parser(language_key: str) -> Optional[object]:
|
|
143
|
+
"""Resolve and cache a tree-sitter Parser for a sourceindex language key.
|
|
144
|
+
|
|
145
|
+
Returns ``None`` (and warns once) if the per-language wheel is missing
|
|
146
|
+
and either auto-install is disabled or the install attempt failed.
|
|
147
|
+
"""
|
|
148
|
+
cached = _PARSER_CACHE.get(language_key)
|
|
149
|
+
if cached is not None:
|
|
150
|
+
return cached
|
|
151
|
+
if language_key not in LANG_CFG:
|
|
152
|
+
return None
|
|
153
|
+
grammar, _def_types, lang_attr = LANG_CFG[language_key]
|
|
154
|
+
module_name = f"tree_sitter_{grammar}"
|
|
155
|
+
|
|
156
|
+
def _import() -> Optional[object]:
|
|
157
|
+
try:
|
|
158
|
+
return importlib.import_module(module_name)
|
|
159
|
+
except ImportError:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
mod = _import()
|
|
163
|
+
if mod is None and os.environ.get(_AUTO_INSTALL_ENV, "").lower() in ("1", "true", "yes"):
|
|
164
|
+
if _try_pip_install(f"tree-sitter-{grammar}"):
|
|
165
|
+
mod = _import()
|
|
166
|
+
if mod is None:
|
|
167
|
+
_warn_missing_grammar(language_key, grammar, ImportError(module_name))
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
from tree_sitter import Language, Parser # core lib is a hard dep
|
|
171
|
+
lang_fn = getattr(mod, lang_attr)
|
|
172
|
+
parser = Parser(Language(lang_fn()))
|
|
173
|
+
except Exception as e:
|
|
174
|
+
_warn_missing_grammar(language_key, grammar, e)
|
|
175
|
+
return None
|
|
176
|
+
_PARSER_CACHE[language_key] = parser
|
|
177
|
+
return parser
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalize_name(qn: str) -> str:
|
|
181
|
+
"""Normalize separators to ``.`` and strip accessor/ctor/closure suffixes.
|
|
182
|
+
|
|
183
|
+
Also drops whitespace-prefixed language keywords the LLM sometimes folds
|
|
184
|
+
into the qualified name (``func Foo``, ``pub fn bar``, ``def quux``).
|
|
185
|
+
"""
|
|
186
|
+
qn = qn.replace("::", ".").replace("#", ".").strip()
|
|
187
|
+
# Keep only the last whitespace-separated token: the actual identifier path
|
|
188
|
+
if " " in qn:
|
|
189
|
+
qn = qn.split()[-1]
|
|
190
|
+
while qn.endswith(_TRAILING_SUFFIXES):
|
|
191
|
+
for suf in _TRAILING_SUFFIXES:
|
|
192
|
+
if qn.endswith(suf):
|
|
193
|
+
qn = qn[: -len(suf)]
|
|
194
|
+
break
|
|
195
|
+
while ".." in qn:
|
|
196
|
+
qn = qn.replace("..", ".")
|
|
197
|
+
return qn.strip(".")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _bare_name(qn: str) -> str:
|
|
201
|
+
qn = _normalize_name(qn)
|
|
202
|
+
return qn.rsplit(".", 1)[-1]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _walk_defs(parser, src: bytes, def_types: Tuple[str, ...]):
|
|
206
|
+
"""Yield ``(qualified_name, start_line, end_line)`` for every definition node."""
|
|
207
|
+
tree = parser.parse(src)
|
|
208
|
+
|
|
209
|
+
def text(n) -> str:
|
|
210
|
+
return src[n.start_byte:n.end_byte].decode("utf-8", errors="replace")
|
|
211
|
+
|
|
212
|
+
def first_child_of_types(n, types):
|
|
213
|
+
for c in n.children:
|
|
214
|
+
if c.type in types:
|
|
215
|
+
return c
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def deep_first_identifier(n):
|
|
219
|
+
stack = [n]
|
|
220
|
+
while stack:
|
|
221
|
+
cur = stack.pop()
|
|
222
|
+
if cur.type in _IDENT_TYPES:
|
|
223
|
+
return cur
|
|
224
|
+
stack.extend(reversed(cur.children))
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def name_of(node) -> Optional[str]:
|
|
228
|
+
n = node.child_by_field_name("name")
|
|
229
|
+
if n is not None:
|
|
230
|
+
if n.type in _IDENT_TYPES:
|
|
231
|
+
return text(n)
|
|
232
|
+
inner = deep_first_identifier(n)
|
|
233
|
+
return text(inner) if inner else None
|
|
234
|
+
t = node.type
|
|
235
|
+
if t == "lexical_declaration": # TS/JS: const x = ...
|
|
236
|
+
vd = first_child_of_types(node, {"variable_declarator"})
|
|
237
|
+
if vd:
|
|
238
|
+
nm = vd.child_by_field_name("name")
|
|
239
|
+
return text(nm) if nm else None
|
|
240
|
+
if t == "property_declaration": # Kotlin
|
|
241
|
+
vd = first_child_of_types(node, {"variable_declaration"})
|
|
242
|
+
if vd:
|
|
243
|
+
inner = deep_first_identifier(vd)
|
|
244
|
+
return text(inner) if inner else None
|
|
245
|
+
if t in ("field_declaration", "declaration", "function_definition"): # C/C++
|
|
246
|
+
decl = node.child_by_field_name("declarator")
|
|
247
|
+
if decl is not None:
|
|
248
|
+
inner = deep_first_identifier(decl)
|
|
249
|
+
return text(inner) if inner else None
|
|
250
|
+
if t in ("singleton_method", "method"):
|
|
251
|
+
inner = deep_first_identifier(node)
|
|
252
|
+
return text(inner) if inner else None
|
|
253
|
+
inner = deep_first_identifier(node)
|
|
254
|
+
return text(inner) if inner else None
|
|
255
|
+
|
|
256
|
+
def walk(node, prefix: str = ""):
|
|
257
|
+
new_prefix = prefix
|
|
258
|
+
if node.type in def_types:
|
|
259
|
+
nm = name_of(node)
|
|
260
|
+
if nm:
|
|
261
|
+
full = f"{prefix}.{nm}" if prefix else nm
|
|
262
|
+
yield (full, node.start_point[0] + 1, node.end_point[0] + 1)
|
|
263
|
+
if node.type in _CONTAINER_TYPES:
|
|
264
|
+
new_prefix = full
|
|
265
|
+
for c in node.children:
|
|
266
|
+
yield from walk(c, new_prefix)
|
|
267
|
+
|
|
268
|
+
yield from walk(tree.root_node)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _build_truth(parser, src: bytes, def_types: Tuple[str, ...]) -> Dict[str, List[Tuple[int, int]]]:
|
|
272
|
+
"""Return {qualified_name: [(start_line, end_line), ...]} for every definition.
|
|
273
|
+
|
|
274
|
+
Also populates a bare-name fallback (last segment) so LLM entries that
|
|
275
|
+
drop package/class prefixes still match. ``_lookup`` prefers qualified.
|
|
276
|
+
"""
|
|
277
|
+
truth: Dict[str, List[Tuple[int, int]]] = {}
|
|
278
|
+
for full, s, e in _walk_defs(parser, src, def_types):
|
|
279
|
+
truth.setdefault(full, []).append((s, e))
|
|
280
|
+
bare = full.rsplit(".", 1)[-1]
|
|
281
|
+
if bare != full:
|
|
282
|
+
truth.setdefault(bare, []).append((s, e))
|
|
283
|
+
return truth
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def collect_skeleton(language_key: str, source: str) -> List[Tuple[str, int, int]]:
|
|
287
|
+
"""Return AST-derived definition list for prompt pre-processing.
|
|
288
|
+
|
|
289
|
+
Returns sorted ``[(qualified_name, start_line, end_line), ...]`` suitable
|
|
290
|
+
for embedding in the LLM build prompt as authoritative structure. Empty
|
|
291
|
+
list signals fall-through (language unsupported, package missing, parse
|
|
292
|
+
failure) so callers can use the original free-form prompt.
|
|
293
|
+
"""
|
|
294
|
+
if language_key not in LANG_CFG:
|
|
295
|
+
return []
|
|
296
|
+
parser = _get_parser(language_key)
|
|
297
|
+
if parser is None:
|
|
298
|
+
return []
|
|
299
|
+
_grammar, def_types, _attr = LANG_CFG[language_key]
|
|
300
|
+
try:
|
|
301
|
+
defs = list(_walk_defs(parser, source.encode("utf-8"), def_types))
|
|
302
|
+
except Exception as e:
|
|
303
|
+
_log.warning(
|
|
304
|
+
f"tree-sitter skeleton failed for {language_key}: {e}",
|
|
305
|
+
extra={
|
|
306
|
+
"event": "tree_sitter.parse_failed",
|
|
307
|
+
"phase": "skeleton",
|
|
308
|
+
"language": language_key,
|
|
309
|
+
"error_type": type(e).__name__,
|
|
310
|
+
"error": str(e),
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
return []
|
|
314
|
+
# Dedup by (name, start, end) — same qualified name can appear twice via
|
|
315
|
+
# cfg-conditional impls in Rust; we want both.
|
|
316
|
+
seen = set()
|
|
317
|
+
out = []
|
|
318
|
+
for entry in defs:
|
|
319
|
+
if entry in seen:
|
|
320
|
+
continue
|
|
321
|
+
seen.add(entry)
|
|
322
|
+
out.append(entry)
|
|
323
|
+
out.sort(key=lambda x: x[1])
|
|
324
|
+
return out
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _lookup(truth: Dict[str, List[Tuple[int, int]]], qn: str) -> List[Tuple[int, int]]:
|
|
328
|
+
"""Match by qualified name; if missing, peel leading segments off; finally bare."""
|
|
329
|
+
norm = _normalize_name(qn)
|
|
330
|
+
if not norm:
|
|
331
|
+
return []
|
|
332
|
+
if norm in truth:
|
|
333
|
+
return truth[norm]
|
|
334
|
+
parts = norm.split(".")
|
|
335
|
+
for i in range(1, len(parts)):
|
|
336
|
+
suffix = ".".join(parts[i:])
|
|
337
|
+
if suffix in truth:
|
|
338
|
+
return truth[suffix]
|
|
339
|
+
bare = parts[-1]
|
|
340
|
+
return truth.get(bare, [])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def fix_line_ranges(language_key: str, source: str, functions_text: str) -> str:
|
|
344
|
+
"""Replace LLM-emitted line ranges with tree-sitter truth where possible.
|
|
345
|
+
|
|
346
|
+
Returns ``functions_text`` unchanged on:
|
|
347
|
+
- language not in ``LANG_CFG`` (no grammar configured),
|
|
348
|
+
- per-language tree-sitter wheel not installed (one-time warning),
|
|
349
|
+
- parse failure or any unexpected error (best-effort, never raise).
|
|
350
|
+
"""
|
|
351
|
+
if language_key not in LANG_CFG:
|
|
352
|
+
return functions_text
|
|
353
|
+
if not functions_text.strip():
|
|
354
|
+
return functions_text
|
|
355
|
+
parser = _get_parser(language_key)
|
|
356
|
+
if parser is None:
|
|
357
|
+
return functions_text
|
|
358
|
+
|
|
359
|
+
_grammar, def_types, _attr = LANG_CFG[language_key]
|
|
360
|
+
try:
|
|
361
|
+
truth = _build_truth(parser, source.encode("utf-8"), def_types)
|
|
362
|
+
except Exception as e: # parser failure, grammar missing, etc.
|
|
363
|
+
_log.warning(
|
|
364
|
+
f"tree-sitter parse failed for {language_key}: {e}",
|
|
365
|
+
extra={
|
|
366
|
+
"event": "tree_sitter.parse_failed",
|
|
367
|
+
"phase": "fix_line_ranges",
|
|
368
|
+
"language": language_key,
|
|
369
|
+
"error_type": type(e).__name__,
|
|
370
|
+
"error": str(e),
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
return functions_text
|
|
374
|
+
|
|
375
|
+
out_lines: List[str] = []
|
|
376
|
+
for line in functions_text.splitlines():
|
|
377
|
+
m = _ENTRY_RE.match(line)
|
|
378
|
+
single = False
|
|
379
|
+
if not m:
|
|
380
|
+
m = _ENTRY_SINGLE_RE.match(line)
|
|
381
|
+
single = bool(m)
|
|
382
|
+
if not m:
|
|
383
|
+
out_lines.append(line)
|
|
384
|
+
continue
|
|
385
|
+
name = m.group("name").strip()
|
|
386
|
+
reported_start = int(m.group("line") if single else m.group("s"))
|
|
387
|
+
cands = _lookup(truth, name)
|
|
388
|
+
if not cands:
|
|
389
|
+
out_lines.append(line)
|
|
390
|
+
continue
|
|
391
|
+
best = min(cands, key=lambda c: abs(c[0] - reported_start))
|
|
392
|
+
if single:
|
|
393
|
+
line = f"{m.group('lead')}{name} (L{best[0]}):{m.group('rest')}"
|
|
394
|
+
else:
|
|
395
|
+
line = f"{m.group('lead')}{name} (L{best[0]}-L{best[1]}):{m.group('rest')}"
|
|
396
|
+
out_lines.append(line)
|
|
397
|
+
return "\n".join(out_lines)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""LLM prompts used by the build pipeline.
|
|
2
|
+
|
|
3
|
+
INDEX_PROMPT_TEMPLATE — full tier-1 + tier-2 indexing for languages with
|
|
4
|
+
build_tier2=True (the default).
|
|
5
|
+
ONELINER_PROMPT_TEMPLATE — lighter prompt for misc/text/config/docs files
|
|
6
|
+
that only need a tier-1 summary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ..lib.languages import Language
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
INDEX_PROMPT_TEMPLATE = """You are indexing a {language_name} source file for a codebase pseudo-index.
|
|
13
|
+
|
|
14
|
+
File: {relative_path}
|
|
15
|
+
Content:
|
|
16
|
+
{file_content}
|
|
17
|
+
|
|
18
|
+
Produce two sections in this exact format:
|
|
19
|
+
|
|
20
|
+
ONELINER: <one-line description under 80 characters>
|
|
21
|
+
|
|
22
|
+
FUNCTIONS:
|
|
23
|
+
<one entry per line, exactly: QualifiedName (Lstart-Lend): one-line purpose>
|
|
24
|
+
|
|
25
|
+
COMPLETENESS RULE — list EVERY definition in this file at every nesting level. For {language_name}, that includes every class/struct/trait/impl/namespace, every method inside every class (including dunder methods like __init__, __repr__, __eq__, __hash__, __new__), every free function, every property, every nested helper function. Methods MUST appear as separate lines IN ADDITION to the class line. A class with 10 methods produces 11 entries (1 for the class + 10 for the methods), not 1. Never omit an entry just because its behavior is hard to describe — write a brief honest line and move on.
|
|
26
|
+
|
|
27
|
+
DESCRIPTION RULES — different rubrics for different entry kinds:
|
|
28
|
+
|
|
29
|
+
* **CLASS / type entries** (classes, structs, traits, impls, namespaces) — describe the class's PURPOSE and ROLE, not behavior. Answer "what is this class for, what's its main interface, who uses it?" One sentence is enough. Class entries are NEVER optional, even when their methods are listed individually right after.
|
|
30
|
+
|
|
31
|
+
* **METHOD / function entries** — describe WHAT THE CODE DOES (its implementation behavior). For meaty methods, name concrete mechanisms: literal SQL fragments (LIKE, COALESCE, IN), normalization forms (NFC), encoding tricks (BYTELOWER, BLOB, escape sequences), algorithm names, returned exception types, edge-case branches. For genuinely trivial methods (plain attribute store, pass-through, inherited dunder, default __repr__), a 4-8 word HONEST description is fine — brief but truthful beats omission.
|
|
32
|
+
|
|
33
|
+
* **Distinguish siblings** — two classes' __init__s must each say what they uniquely do. Two classes' col_clause methods must each name the actual SQL they emit, not "returns SQL clause".
|
|
34
|
+
|
|
35
|
+
GOOD class entries (purpose-level, role-oriented):
|
|
36
|
+
Query (L75-L115): Abstract base for SQL queries; subclasses provide clause and match.
|
|
37
|
+
RegexpQuery (L325-L362): FieldQuery using compiled regex; raises on invalid patterns.
|
|
38
|
+
AndQuery (L521-L527): CollectionQuery joining sub-queries with SQL AND and Python all().
|
|
39
|
+
TrueQuery (L641-L648): Always-match query — returns ('1', ()) and matches every model.
|
|
40
|
+
|
|
41
|
+
GOOD method entries (concrete mechanism):
|
|
42
|
+
Bar.col_clause (L40-L45): Builds `field LIKE ? ESCAPE '\\\\'` with backslash-escaped wildcards.
|
|
43
|
+
PathQuery.match (L100-L110): Compares model.path BLOB against case-folded directory prefix.
|
|
44
|
+
RegexpQuery.__init__ (L327-L338): Compiles pattern, raises InvalidQueryArgumentValueError on re.error.
|
|
45
|
+
|
|
46
|
+
GOOD trivial-method entries (brief but honest — DON'T omit):
|
|
47
|
+
Foo.__repr__ (L48-L49): Default repr from dataclass.
|
|
48
|
+
ParsingError.__init__ (L46-L47): Inherits Exception.__init__.
|
|
49
|
+
Query.__hash__ (L106-L110): Hashes by exact type to match __eq__ semantics.
|
|
50
|
+
|
|
51
|
+
BAD (avoid these patterns):
|
|
52
|
+
Bar.col_clause (L40-L45): Returns the SQL clause for the field. ← vague restatement
|
|
53
|
+
PathQuery.match (L100-L110): Checks whether a path matches. ← restates the name
|
|
54
|
+
Query.__init__ (L77-L78): Initializes the Query. ← says nothing
|
|
55
|
+
|
|
56
|
+
Use the natural qualification for {language_name} (e.g. ClassName.method, Namespace::function, impl_target::method).
|
|
57
|
+
Each purpose must be under 100 characters. Use the literal line numbers from the file.
|
|
58
|
+
|
|
59
|
+
Output ONLY the two sections. No markdown headers, no commentary."""
|
|
60
|
+
|
|
61
|
+
# Lightweight prompt for files with no code structure (docs/config/text).
|
|
62
|
+
ONELINER_PROMPT_TEMPLATE = """You are summarizing a {language_name} file for a codebase pseudo-index.
|
|
63
|
+
|
|
64
|
+
File: {relative_path}
|
|
65
|
+
Content:
|
|
66
|
+
{file_content}
|
|
67
|
+
|
|
68
|
+
Respond in this exact format:
|
|
69
|
+
|
|
70
|
+
ONELINER: <one-line description under 80 characters>
|
|
71
|
+
|
|
72
|
+
Output ONLY that single line. No markdown, no commentary."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def prompt_for(language: Language) -> str:
|
|
76
|
+
return INDEX_PROMPT_TEMPLATE if language.build_tier2 else ONELINER_PROMPT_TEMPLATE
|