code-search-cli 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.
@@ -0,0 +1,722 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ import time
6
+ from collections import Counter
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from .index_store import IndexRefresh, PersistentIndex
11
+ from .models import Evidence, RetrievalResult, Section, SourceFile
12
+ from .query import document_terms, query_terms, split_identifier
13
+ from .repository import RepositoryReader
14
+ from .structure import containing_section, sections_for
15
+
16
+
17
+ SYMBOL_RE = re.compile(r"\b[A-Z][A-Za-z0-9_$]{3,}\b")
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class _FileCandidate:
22
+ source: SourceFile
23
+ terms: Counter[str]
24
+ score: float
25
+ matches: list[str]
26
+
27
+
28
+ class RetrievalEngine:
29
+ """Build a small, explainable evidence bundle for an engineering task."""
30
+
31
+ def __init__(self, repository: str | Path, ref: str | None = None):
32
+ self.reader = RepositoryReader(repository, ref)
33
+ self.index = PersistentIndex(self.reader)
34
+ self._index_refresh: IndexRefresh | None = None
35
+ self._files: list[SourceFile] | None = None
36
+ self._document_terms: dict[str, Counter[str]] = {}
37
+ self._path_terms: dict[str, Counter[str]] = {}
38
+ self._sections: dict[str, list[Section]] = {}
39
+ self._section_terms: dict[tuple[str, int, int], Counter[str]] = {}
40
+
41
+ def retrieve(
42
+ self,
43
+ task: str,
44
+ max_evidence: int = 16,
45
+ ) -> RetrievalResult:
46
+ if not task.strip():
47
+ raise ValueError("task must not be empty")
48
+ started = time.perf_counter()
49
+ weighted, original = query_terms(task)
50
+ files = self._load_files()
51
+ candidates = self._rank_files(task, files, weighted, original)
52
+ section_candidates = self._rank_sections(candidates[:40], weighted, original)
53
+ section_candidates.extend(self._reference_expansion(task, candidates, section_candidates, weighted, original))
54
+ section_candidates.extend(self._structural_expansion(task, candidates))
55
+ section_candidates.extend(self._call_expansion(candidates, section_candidates, weighted))
56
+ section_candidates.extend(self._sibling_contrast_expansion(task, candidates, weighted, original))
57
+ section_candidates.extend(self._initialization_order_expansion(task, candidates, weighted))
58
+ section_candidates.extend(self._dependency_order_expansion(task, candidates, weighted))
59
+ evidence = self._pack(section_candidates, max_evidence)
60
+ elapsed = time.perf_counter() - started
61
+ used = sum(item.estimated_tokens for item in evidence)
62
+ return RetrievalResult(
63
+ task=task,
64
+ repository=str(self.reader.root),
65
+ ref=self.reader.ref,
66
+ used_tokens=used,
67
+ query_terms=original,
68
+ evidence=evidence,
69
+ stats={
70
+ "source_files": len(files),
71
+ "candidate_files": len(candidates),
72
+ "elapsed_ms": round(elapsed * 1000, 2),
73
+ "strategy": "weighted-lexical+structure+references+causal-contrast",
74
+ "index": self._index_refresh.to_dict() if self._index_refresh else None,
75
+ },
76
+ )
77
+
78
+ def prepare(self) -> dict[str, float | int]:
79
+ """Build the reusable in-process lexical and structural index."""
80
+ started = time.perf_counter()
81
+ files = self._load_files()
82
+ section_count = 0
83
+ for source in files:
84
+ self._terms_for_source(source)
85
+ self._terms_for_path(source.path)
86
+ sections = self._sections_for(source)
87
+ section_count += len(sections)
88
+ for section in sections:
89
+ self._terms_for_section(section)
90
+ return {
91
+ "source_files": len(files),
92
+ "sections": section_count,
93
+ "elapsed_ms": round((time.perf_counter() - started) * 1000, 2),
94
+ }
95
+
96
+ def expand(
97
+ self,
98
+ path: str,
99
+ *,
100
+ line: int | None = None,
101
+ symbol: str | None = None,
102
+ relation: str = "enclosing",
103
+ ) -> list[Evidence]:
104
+ if relation not in {"enclosing", "siblings", "references"}:
105
+ raise ValueError("relation must be enclosing, siblings, or references")
106
+ source = self.reader.read_file(path)
107
+ sections = sections_for(source)
108
+ anchor = containing_section(source, line, sections) if line else _section_named(sections, symbol)
109
+ if relation == "enclosing":
110
+ selected = [anchor]
111
+ reason = "enclosing structural unit"
112
+ elif relation == "references":
113
+ name = symbol or anchor.name
114
+ selected = []
115
+ for candidate in self._load_files():
116
+ if candidate.path != path and re.search(rf"\b{re.escape(name)}\b", candidate.content):
117
+ selected.append(
118
+ containing_section(candidate, _first_line(candidate.content, name), self._sections_for(candidate))
119
+ )
120
+ reason = f"references `{name}`"
121
+ else:
122
+ selected = [item for item in sections if item.name != anchor.name]
123
+ reason = f"sibling of `{anchor.name}`"
124
+ items = [_evidence(item, 1.0, [reason]) for item in selected]
125
+ return self._pack(items, 20)
126
+
127
+ def _load_files(self) -> list[SourceFile]:
128
+ if self._files is None:
129
+ self._index_refresh = self.index.refresh()
130
+ snapshot = self.index.load()
131
+ self._files = snapshot.files
132
+ self._document_terms.update(snapshot.document_terms)
133
+ self._sections.update(snapshot.sections)
134
+ return self._files
135
+
136
+ def _rank_files(
137
+ self,
138
+ task: str,
139
+ files: list[SourceFile],
140
+ weighted: dict[str, float],
141
+ original: list[str],
142
+ ) -> list[_FileCandidate]:
143
+ task_lower = task.lower()
144
+ candidates = []
145
+ for source in files:
146
+ terms = self._terms_for_source(source)
147
+ path_terms = self._terms_for_path(source.path)
148
+ score = 0.0
149
+ matches = []
150
+ for term, weight in weighted.items():
151
+ content_count = terms.get(term, 0)
152
+ path_count = path_terms.get(term, 0)
153
+ if content_count or path_count:
154
+ matches.append(term)
155
+ score += weight * (math.log2(1 + min(content_count, 16)) + 5 * min(path_count, 1))
156
+ lowered = source.content.lower()
157
+ if task_lower in lowered:
158
+ score += 20
159
+ matches.append("exact task phrase")
160
+ original_hits = sum(1 for term in original if terms.get(term) or path_terms.get(term))
161
+ if original and original_hits == len(original):
162
+ score += 8
163
+ elif original_hits >= 2:
164
+ score += original_hits
165
+ if score > 0:
166
+ candidates.append(_FileCandidate(source, terms, score, matches))
167
+ return sorted(candidates, key=lambda item: (-item.score, item.source.path))
168
+
169
+ def _rank_sections(
170
+ self,
171
+ candidates: list[_FileCandidate],
172
+ weighted: dict[str, float],
173
+ original: list[str],
174
+ ) -> list[Evidence]:
175
+ result = []
176
+ for file_rank, candidate in enumerate(candidates, 1):
177
+ ranked = []
178
+ for section in self._sections_for(candidate.source):
179
+ terms = self._terms_for_section(section)
180
+ hits = [(term, terms[term], weight) for term, weight in weighted.items() if terms.get(term)]
181
+ if not hits:
182
+ continue
183
+ local = sum(weight * math.log2(1 + min(count, 12)) for term, count, weight in hits)
184
+ original_hits = sum(1 for term in original if terms.get(term))
185
+ local += 2 * original_hits
186
+ score = local + candidate.score * 0.35 - math.log2(file_rank + 1)
187
+ reasons = [f"file rank {file_rank}", "matched " + ", ".join(term for term, _, _ in hits[:6])]
188
+ focused = _focused_sections(section, weighted)
189
+ for window_rank, window in enumerate(focused):
190
+ window_reasons = reasons if len(focused) == 1 else reasons + [f"focused window {window_rank + 1}"]
191
+ ranked.append(_evidence(window, score - window_rank * 0.5, window_reasons))
192
+ ranked.sort(key=lambda item: (-item.score, item.start_line))
193
+ representatives = []
194
+ represented = set()
195
+ for item in ranked:
196
+ key = item.path, item.name
197
+ if key in represented:
198
+ continue
199
+ representatives.append(item)
200
+ represented.add(key)
201
+ if len(representatives) == 4:
202
+ break
203
+ result.extend(representatives)
204
+ if representatives:
205
+ primary = representatives[0]
206
+ second_window = next(
207
+ (
208
+ item for item in ranked
209
+ if item.path == primary.path
210
+ and item.name == primary.name
211
+ and item.key() != primary.key()
212
+ ),
213
+ None,
214
+ )
215
+ if second_window:
216
+ result.append(second_window)
217
+ return result
218
+
219
+ def _reference_expansion(
220
+ self,
221
+ task: str,
222
+ files: list[_FileCandidate],
223
+ sections: list[Evidence],
224
+ weighted: dict[str, float],
225
+ original: list[str],
226
+ ) -> list[Evidence]:
227
+ query_symbols = set(SYMBOL_RE.findall(task))
228
+ for item in sections[:8]:
229
+ query_symbols.update(SYMBOL_RE.findall(item.content))
230
+ # Prefer task symbols and class names of strong anchors; cap fan-out aggressively.
231
+ anchor_names = {Path(item.path).stem for item in sections[:8]}
232
+ symbols = sorted(query_symbols & anchor_names | set(SYMBOL_RE.findall(task)))[:8]
233
+ if not symbols:
234
+ return []
235
+ expanded: list[Evidence] = []
236
+ seen_paths = {item.path for item in sections[:4]}
237
+ for symbol in symbols:
238
+ references = []
239
+ for candidate in files:
240
+ if candidate.source.path in seen_paths or not re.search(rf"\b{re.escape(symbol)}\b", candidate.source.content):
241
+ continue
242
+ line = _first_line(candidate.source.content, symbol)
243
+ section = containing_section(candidate.source, line, self._sections_for(candidate.source))
244
+ terms = document_terms(section.content)
245
+ task_hits = sum(weight for term, weight in weighted.items() if terms.get(term))
246
+ score = candidate.score * 0.15 + task_hits + 2
247
+ references.append(
248
+ _evidence(_focused_section(section, weighted, line), score, [f"references anchor `{symbol}`"])
249
+ )
250
+ references.sort(key=lambda item: (-item.score, item.path))
251
+ expanded.extend(references[:5])
252
+ return expanded
253
+
254
+ def _structural_expansion(self, task: str, files: list[_FileCandidate]) -> list[Evidence]:
255
+ intent = set(split_identifier(task))
256
+ non_schedule_intent = intent - {
257
+ "scheduled", "schedule", "scheduler", "cron", "trigger",
258
+ "resolve", "fix", "failure", "error", "exception", "null", "pointer",
259
+ "nullpointerexception", "npe", "in", "the",
260
+ }
261
+ patterns: list[tuple[str, re.Pattern[str]]] = []
262
+ if intent & {"dependency", "injection", "order", "startup", "initialization"}:
263
+ patterns.extend(
264
+ [
265
+ ("dependency ordering annotation", re.compile(r"@DependsOn\b")),
266
+ ("startup lifecycle hook", re.compile(r"@PostConstruct\b|\bafterPropertiesSet\s*\(")),
267
+ ]
268
+ )
269
+ if intent & {"callback", "event", "events", "listener", "subscriber"}:
270
+ patterns.extend(
271
+ [
272
+ ("event registration", re.compile(r"\bregister(?:Subscriber|Listener|ToPublisher)\s*\(")),
273
+ ("event publication", re.compile(r"\bpublishEvent\s*\(")),
274
+ ]
275
+ )
276
+ if intent & {"scheduled", "schedule", "scheduler", "cron", "trigger"}:
277
+ patterns.append(
278
+ (
279
+ "scheduled execution entry",
280
+ re.compile(
281
+ r"@Scheduled\b|\bconfigureTasks\s*\(|\baddTriggerTask\s*\(|"
282
+ r"\bscheduleAtFixedRate\s*\("
283
+ ),
284
+ )
285
+ )
286
+ if not patterns:
287
+ return []
288
+ expanded = []
289
+ for candidate in files:
290
+ matches = [(label, pattern.search(candidate.source.content)) for label, pattern in patterns]
291
+ matches = [(label, match) for label, match in matches if match]
292
+ if matches and all(label == "scheduled execution entry" for label, _ in matches):
293
+ if not non_schedule_intent.intersection(candidate.matches):
294
+ continue
295
+ if not matches:
296
+ continue
297
+ label, match = matches[0]
298
+ assert match is not None
299
+ line = candidate.source.content.count("\n", 0, match.start()) + 1
300
+ section = containing_section(candidate.source, line, self._sections_for(candidate.source))
301
+ score = candidate.score * 0.35 + 36 + 8 * len(matches)
302
+ labels = [matched_label for matched_label, _ in matches]
303
+ expanded.append(
304
+ _evidence(
305
+ _focused_section(section, {}, line),
306
+ score,
307
+ labels + [f"obligation: {matched_label}" for matched_label in labels]
308
+ + ["task-specific structural pattern"],
309
+ )
310
+ )
311
+ expanded.sort(key=lambda item: (-item.score, item.path))
312
+ return expanded[:16]
313
+
314
+ def _call_expansion(
315
+ self,
316
+ files: list[_FileCandidate],
317
+ evidence: list[Evidence],
318
+ weighted: dict[str, float],
319
+ ) -> list[Evidence]:
320
+ by_path = {candidate.source.path: candidate.source for candidate in files}
321
+ expanded: list[Evidence] = []
322
+ anchors = sorted(evidence, key=lambda item: -item.score)[:20]
323
+ for anchor in anchors:
324
+ source = by_path.get(anchor.path)
325
+ if source is None:
326
+ continue
327
+ definitions = {section.name: section for section in self._sections_for(source)}
328
+ calls = re.findall(r"\b([A-Za-z_$][\w$]*)\s*\(", anchor.content)
329
+ for name in list(dict.fromkeys(calls)):
330
+ target = definitions.get(name)
331
+ if target is None or target.start_line == anchor.start_line:
332
+ continue
333
+ terms = document_terms(target.content + " " + target.name)
334
+ task_signal = sum(weight for term, weight in weighted.items() if terms.get(term))
335
+ if task_signal <= 0:
336
+ continue
337
+ score = anchor.score * 0.85 + task_signal
338
+ expanded.append(
339
+ _evidence(
340
+ _focused_section(target, weighted),
341
+ score,
342
+ [f"called by `{anchor.name}`", "definition expansion"],
343
+ )
344
+ )
345
+ expanded.sort(key=lambda item: (-item.score, item.path, item.start_line))
346
+ return expanded[:20]
347
+
348
+ def _sibling_contrast_expansion(
349
+ self,
350
+ task: str,
351
+ files: list[_FileCandidate],
352
+ weighted: dict[str, float],
353
+ original: list[str],
354
+ ) -> list[Evidence]:
355
+ intent = set(split_identifier(task))
356
+ if not intent.intersection({"callback", "callbacks"}):
357
+ return []
358
+ identity_terms = {
359
+ term: weighted[term]
360
+ for term in original
361
+ if term not in {"callback", "callbacks", "event", "events", "listener", "subscriber"}
362
+ }
363
+ identity_terms = identity_terms or weighted
364
+ publication = re.compile(r"\bpublishEvent\s*\(")
365
+ expanded: list[Evidence] = []
366
+ for candidate in files[:30]:
367
+ sections = [item for item in self._sections_for(candidate.source) if item.kind in {"method", "function"}]
368
+ for target in sections:
369
+ if publication.search(target.content):
370
+ continue
371
+ target_name_terms = set(split_identifier(target.name))
372
+ target_relevance = _term_signal(target, identity_terms)
373
+ if target_relevance <= 0:
374
+ continue
375
+ comparisons = []
376
+ for sibling in sections:
377
+ if sibling is target or not publication.search(sibling.content):
378
+ continue
379
+ sibling_name_terms = set(split_identifier(sibling.name))
380
+ common = target_name_terms & sibling_name_terms
381
+ if len(common) < 2:
382
+ continue
383
+ similarity = len(common) / max(1, min(len(target_name_terms), len(sibling_name_terms)))
384
+ if similarity < 0.6 or target_relevance <= _term_signal(sibling, identity_terms):
385
+ continue
386
+ comparisons.append((similarity, sibling))
387
+ if not comparisons:
388
+ continue
389
+ _, sibling = max(comparisons, key=lambda item: (item[0], -item[1].start_line))
390
+ base = candidate.score * 0.35 + target_relevance
391
+ expanded.extend(
392
+ [
393
+ _evidence(
394
+ _focused_section(target, weighted),
395
+ base + 72,
396
+ [
397
+ f"sibling contrast: `{sibling.name}` publishes an event",
398
+ "callback path lacks the sibling's event publication",
399
+ "obligation: callback completion",
400
+ ],
401
+ ),
402
+ _evidence(
403
+ _focused_section(sibling, weighted),
404
+ base + 64,
405
+ [
406
+ f"comparison sibling for `{target.name}`",
407
+ "contains event publication",
408
+ ],
409
+ ),
410
+ ]
411
+ )
412
+ expanded.sort(key=lambda item: (-item.score, item.path, item.start_line))
413
+ return expanded[:8]
414
+
415
+ def _initialization_order_expansion(
416
+ self,
417
+ task: str,
418
+ files: list[_FileCandidate],
419
+ weighted: dict[str, float],
420
+ ) -> list[Evidence]:
421
+ intent = set(split_identifier(task))
422
+ if not intent.intersection({"startup", "initialization", "dependency", "order"}):
423
+ return []
424
+ if not intent.intersection({"event", "events", "listener", "subscriber", "missed", "missing"}):
425
+ return []
426
+
427
+ task_lower = task.lower()
428
+ subscribers: dict[str, list[tuple[_FileCandidate, Section]]] = {}
429
+ for candidate in files:
430
+ content = candidate.source.content
431
+ if "registerSubscriber" not in content:
432
+ continue
433
+ event_types = re.findall(r"return\s+([A-Z][A-Za-z0-9_$]*)\.class\s*;", content)
434
+ if not event_types:
435
+ continue
436
+ static_registration = next(
437
+ (
438
+ match for match in re.finditer(r"\bregisterSubscriber\s*\(", content)
439
+ if _inside_static_initializer(content, match.start())
440
+ ),
441
+ None,
442
+ )
443
+ if static_registration is None:
444
+ continue
445
+ line = content.count("\n", 0, static_registration.start()) + 1
446
+ section = containing_section(candidate.source, line, self._sections_for(candidate.source))
447
+ for event_type in event_types:
448
+ subscribers.setdefault(event_type, []).append((candidate, section))
449
+
450
+ expanded: list[Evidence] = []
451
+ for publisher in files:
452
+ publisher_name = Path(publisher.source.path).stem
453
+ if publisher_name.lower() not in task_lower:
454
+ continue
455
+ published_events = list(dict.fromkeys(re.findall(
456
+ r"\bpublishEvent\s*\(\s*new\s+([A-Z][A-Za-z0-9_$]*)\b",
457
+ publisher.source.content,
458
+ )))
459
+ if not published_events:
460
+ continue
461
+ constructor = next(
462
+ (section for section in self._sections_for(publisher.source) if section.name == publisher_name),
463
+ None,
464
+ )
465
+ if constructor is None:
466
+ continue
467
+ for event_type in published_events:
468
+ for subscriber, registration in subscribers.get(event_type, []):
469
+ subscriber_name = Path(subscriber.source.path).stem
470
+ if subscriber.source.path == publisher.source.path or subscriber_name in constructor.content:
471
+ continue
472
+ base = publisher.score * 0.35 + _term_signal(constructor, weighted)
473
+ expanded.extend(
474
+ [
475
+ _evidence(
476
+ _focused_section(constructor, weighted),
477
+ base + 84,
478
+ [
479
+ f"publishes `{event_type}` before `{subscriber_name}` is guaranteed initialized",
480
+ f"missing initialization edge to `{subscriber_name}`",
481
+ f"required repair site: initialize `{subscriber_name}` from this component",
482
+ "obligation: initialization responsibility",
483
+ ],
484
+ ),
485
+ _evidence(
486
+ _focused_section(registration, weighted),
487
+ base + 76,
488
+ [
489
+ f"registers the subscriber for `{event_type}`",
490
+ f"initialization counterpart for `{publisher_name}`",
491
+ ],
492
+ ),
493
+ ]
494
+ )
495
+ expanded.sort(key=lambda item: (-item.score, item.path, item.start_line))
496
+ return expanded[:8]
497
+
498
+ def _dependency_order_expansion(
499
+ self,
500
+ task: str,
501
+ files: list[_FileCandidate],
502
+ weighted: dict[str, float],
503
+ ) -> list[Evidence]:
504
+ intent = set(split_identifier(task))
505
+ if not intent.intersection({"startup", "initialization", "dependency", "order"}):
506
+ return []
507
+ task_components = [
508
+ symbol for symbol in SYMBOL_RE.findall(task)
509
+ if symbol.endswith(("Notifier", "Listener", "Initializer", "Manager", "Service"))
510
+ ]
511
+ if not task_components:
512
+ return []
513
+
514
+ expanded: list[Evidence] = []
515
+ for candidate in files:
516
+ content = candidate.source.content
517
+ annotation = re.search(r"@DependsOn\s*\(([^)]*)\)", content, re.DOTALL)
518
+ if annotation is None or not re.search(r"@PostConstruct\b|\bafterPropertiesSet\s*\(", content):
519
+ continue
520
+ declared = annotation.group(1)
521
+ for component in task_components:
522
+ bean_name = component[0].lower() + component[1:]
523
+ if component in content or bean_name in declared:
524
+ continue
525
+ line = content.count("\n", 0, annotation.start()) + 1
526
+ section = containing_section(candidate.source, line, self._sections_for(candidate.source))
527
+ base = candidate.score * 0.35 + _term_signal(section, weighted)
528
+ expanded.append(
529
+ _evidence(
530
+ _focused_section(section, weighted, line),
531
+ base + 96,
532
+ [
533
+ f"startup lifecycle has `@DependsOn` but omits `{bean_name}`",
534
+ f"missing dependency edge to task component `{component}`",
535
+ f"required repair site: add `{bean_name}` to this dependency declaration",
536
+ "joint responsibility: order startup work after notifier initialization",
537
+ "obligation: startup dependency owner",
538
+ ],
539
+ )
540
+ )
541
+ expanded.sort(key=lambda item: (-item.score, item.path, item.start_line))
542
+ return expanded[:4]
543
+
544
+ def _sections_for(self, source: SourceFile) -> list[Section]:
545
+ cached = self._sections.get(source.path)
546
+ if cached is None:
547
+ cached = sections_for(source)
548
+ self._sections[source.path] = cached
549
+ return cached
550
+
551
+ def _terms_for_source(self, source: SourceFile) -> Counter[str]:
552
+ cached = self._document_terms.get(source.path)
553
+ if cached is None:
554
+ cached = document_terms(source.content)
555
+ self._document_terms[source.path] = cached
556
+ return cached
557
+
558
+ def _terms_for_path(self, path: str) -> Counter[str]:
559
+ cached = self._path_terms.get(path)
560
+ if cached is None:
561
+ cached = document_terms(path)
562
+ self._path_terms[path] = cached
563
+ return cached
564
+
565
+ def _terms_for_section(self, section: Section) -> Counter[str]:
566
+ key = section.path, section.start_line, section.end_line
567
+ cached = self._section_terms.get(key)
568
+ if cached is None:
569
+ cached = document_terms(section.content + " " + section.name)
570
+ self._section_terms[key] = cached
571
+ return cached
572
+
573
+ def _pack(self, candidates: list[Evidence], limit: int) -> list[Evidence]:
574
+ unique: dict[tuple[str, int, int], Evidence] = {}
575
+ for item in candidates:
576
+ existing = unique.get(item.key())
577
+ if existing is None or item.score > existing.score:
578
+ unique[item.key()] = item
579
+ ranked = sorted(unique.values(), key=lambda item: (-_packing_score(item), item.path, item.start_line))
580
+ selected: list[Evidence] = []
581
+ # Preserve one strong, preferably production, example for every structural
582
+ # obligation inferred from the task before adding similar hits.
583
+ obligations: dict[str, list[Evidence]] = {}
584
+ for item in ranked:
585
+ for reason in item.reasons:
586
+ if reason.startswith("obligation: "):
587
+ obligations.setdefault(reason, []).append(item)
588
+ for reason in sorted(obligations):
589
+ if len(selected) >= limit:
590
+ break
591
+ options = sorted(obligations[reason], key=lambda item: (-_packing_score(item), item.path))
592
+ for item in options:
593
+ if item not in selected:
594
+ selected.append(item)
595
+ break
596
+
597
+ # Prefer multiple candidate files before adding more results from the same file.
598
+ diversity_limit = min(4, limit)
599
+ representatives: dict[str, Evidence] = {}
600
+ for item in ranked:
601
+ representatives.setdefault(item.path, item)
602
+ diverse = sorted(
603
+ representatives.values(),
604
+ key=lambda item: (-_packing_score(item), item.path),
605
+ )
606
+ for item in diverse:
607
+ if len(selected) >= diversity_limit:
608
+ break
609
+ if item in selected:
610
+ continue
611
+ selected.append(item)
612
+ for item in ranked:
613
+ if len(selected) >= limit or item in selected:
614
+ continue
615
+ selected.append(item)
616
+ return selected
617
+
618
+
619
+ def _packing_score(item: Evidence) -> float:
620
+ test_penalty = 12 if "/test/" in item.path or item.path.startswith("test/") else 0
621
+ return item.score - min(item.estimated_tokens, 2000) / 200 - test_penalty
622
+
623
+
624
+ def _term_signal(section: Section, weighted: dict[str, float]) -> float:
625
+ terms = document_terms(section.content + " " + section.name)
626
+ return sum(weight for term, weight in weighted.items() if terms.get(term))
627
+
628
+
629
+ def _inside_static_initializer(content: str, offset: int) -> bool:
630
+ for match in reversed(list(re.finditer(r"\bstatic\s*\{", content[:offset]))):
631
+ depth = 0
632
+ for index in range(match.end() - 1, len(content)):
633
+ if content[index] == "{":
634
+ depth += 1
635
+ elif content[index] == "}":
636
+ depth -= 1
637
+ if depth == 0:
638
+ return offset < index
639
+ return True
640
+ return False
641
+
642
+
643
+ def _focused_section(
644
+ section: Section,
645
+ weighted: dict[str, float],
646
+ focus_line: int | None = None,
647
+ size: int = 48,
648
+ ) -> Section:
649
+ return _focused_sections(section, weighted, focus_line, size, limit=1)[0]
650
+
651
+
652
+ def _focused_sections(
653
+ section: Section,
654
+ weighted: dict[str, float],
655
+ focus_line: int | None = None,
656
+ size: int = 48,
657
+ limit: int = 2,
658
+ ) -> list[Section]:
659
+ lines = section.content.splitlines()
660
+ if len(lines) <= size:
661
+ return [section]
662
+ if focus_line is not None:
663
+ focus = max(0, min(len(lines) - 1, focus_line - section.start_line))
664
+ starts = [max(0, min(focus - size // 2, len(lines) - size))]
665
+ else:
666
+ lowered = [line.lower() for line in lines]
667
+ candidates = list(range(0, len(lines) - size + 1, size // 2))
668
+ candidates.append(len(lines) - size)
669
+ scored = []
670
+ for candidate in dict.fromkeys(candidates):
671
+ text = "\n".join(lowered[candidate : candidate + size])
672
+ score = sum(weight * text.count(term) for term, weight in weighted.items())
673
+ scored.append((score, candidate))
674
+ starts = []
675
+ for _, candidate in sorted(scored, key=lambda item: (-item[0], item[1])):
676
+ if all(abs(candidate - selected) >= size // 2 for selected in starts):
677
+ starts.append(candidate)
678
+ if len(starts) == limit:
679
+ break
680
+ return [
681
+ Section(
682
+ section.path,
683
+ section.name,
684
+ f"{section.kind}-window",
685
+ section.start_line + start,
686
+ section.start_line + start + size - 1,
687
+ "\n".join(lines[start : start + size]),
688
+ )
689
+ for start in starts
690
+ ]
691
+
692
+
693
+ def _evidence(section: Section, score: float, reasons: list[str]) -> Evidence:
694
+ return Evidence(
695
+ path=section.path,
696
+ start_line=section.start_line,
697
+ end_line=section.end_line,
698
+ kind=section.kind,
699
+ name=section.name,
700
+ score=round(score, 4),
701
+ reasons=reasons,
702
+ content=section.content,
703
+ estimated_tokens=max(1, (len(section.content) + 3) // 4),
704
+ )
705
+
706
+
707
+ def _section_named(sections: list[Section], symbol: str | None) -> Section:
708
+ if not sections:
709
+ raise ValueError("file contains no retrievable sections")
710
+ if symbol:
711
+ for section in sections:
712
+ if section.name == symbol or re.search(rf"\b{re.escape(symbol)}\b", section.content):
713
+ return section
714
+ raise ValueError(f"symbol not found: {symbol}")
715
+ return sections[0]
716
+
717
+
718
+ def _first_line(content: str, value: str) -> int:
719
+ for index, line in enumerate(content.splitlines(), 1):
720
+ if value in line:
721
+ return index
722
+ return 1