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.
Files changed (47) hide show
  1. sourceindex/__init__.py +30 -0
  2. sourceindex/build/__init__.py +592 -0
  3. sourceindex/build/indexer.py +403 -0
  4. sourceindex/build/linerange/__init__.py +24 -0
  5. sourceindex/build/linerange/python_ast.py +155 -0
  6. sourceindex/build/linerange/treesitter.py +397 -0
  7. sourceindex/build/prompts.py +76 -0
  8. sourceindex/build/state.py +261 -0
  9. sourceindex/build/walker.py +94 -0
  10. sourceindex/claudecode/__init__.py +0 -0
  11. sourceindex/claudecode/savings.py +325 -0
  12. sourceindex/claudecode/savings_summary.py +88 -0
  13. sourceindex/claudecode/statusline.py +116 -0
  14. sourceindex/cli/__init__.py +304 -0
  15. sourceindex/cli/__main__.py +10 -0
  16. sourceindex/cli/api_key.py +171 -0
  17. sourceindex/cli/commands.py +678 -0
  18. sourceindex/cli/install.py +378 -0
  19. sourceindex/daemon/__init__.py +83 -0
  20. sourceindex/daemon/client.py +141 -0
  21. sourceindex/daemon/crypto.py +48 -0
  22. sourceindex/daemon/keyring_store.py +129 -0
  23. sourceindex/daemon/lifecycle.py +237 -0
  24. sourceindex/daemon/protocol.py +134 -0
  25. sourceindex/daemon/server.py +389 -0
  26. sourceindex/daemon/store.py +426 -0
  27. sourceindex/lib/__init__.py +0 -0
  28. sourceindex/lib/backend.py +240 -0
  29. sourceindex/lib/cost.py +96 -0
  30. sourceindex/lib/env.py +30 -0
  31. sourceindex/lib/git.py +33 -0
  32. sourceindex/lib/languages.py +406 -0
  33. sourceindex/lib/llm.py +298 -0
  34. sourceindex/lib/log.py +228 -0
  35. sourceindex/lib/registry.py +75 -0
  36. sourceindex/lib/timing.py +10 -0
  37. sourceindex/search/__init__.py +251 -0
  38. sourceindex/search/experiments.py +276 -0
  39. sourceindex/search/imports.py +155 -0
  40. sourceindex/search/passes.py +51 -0
  41. sourceindex/search/prompts.py +379 -0
  42. sourceindex/search/roadmap.py +265 -0
  43. sourceindex/server.py +127 -0
  44. sourceindex-0.1.0.dist-info/METADATA +73 -0
  45. sourceindex-0.1.0.dist-info/RECORD +47 -0
  46. sourceindex-0.1.0.dist-info/WHEEL +4 -0
  47. 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