vectr 1.0.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 (61) hide show
  1. agent/__init__.py +0 -0
  2. agent/cartographer.py +428 -0
  3. agent/chunk_quality.py +1119 -0
  4. agent/config.py +648 -0
  5. agent/config.yaml +1153 -0
  6. agent/eviction_advisor.py +447 -0
  7. agent/identifier_hint.py +68 -0
  8. agent/indexer/__init__.py +112 -0
  9. agent/indexer/_chunking.py +458 -0
  10. agent/indexer/_constants.py +105 -0
  11. agent/indexer/_core.py +878 -0
  12. agent/indexer/_types.py +157 -0
  13. agent/instance_registry.py +127 -0
  14. agent/llm_client.py +8 -0
  15. agent/model_cache.py +101 -0
  16. agent/prompt_templates.py +31 -0
  17. agent/searcher.py +848 -0
  18. agent/strategy_selector.py +299 -0
  19. agent/symbol_graph/__init__.py +131 -0
  20. agent/symbol_graph/_constants.py +366 -0
  21. agent/symbol_graph/_extraction.py +528 -0
  22. agent/symbol_graph/_graph.py +1878 -0
  23. agent/symbol_graph/_types.py +35 -0
  24. agent/templates/claude_md.md +65 -0
  25. agent/templates/claude_md_search_only.md +27 -0
  26. agent/templates/cursor_mcp.json.template +7 -0
  27. agent/templates/cursor_rules_header.txt +5 -0
  28. agent/templates/hook_no_double_recall.txt +1 -0
  29. agent/templates/mcp.json.template +8 -0
  30. agent/templates/session_start_guidance_default.txt +3 -0
  31. agent/templates/session_start_guidance_hooks_aware.txt +1 -0
  32. agent/templates/tool_loading_guidance_claude.txt +1 -0
  33. agent/templates/tool_loading_guidance_claude_search_only.txt +1 -0
  34. agent/templates/vscode_mcp.json.template +8 -0
  35. agent/tool_necessity_probe.py +177 -0
  36. agent/version_stamp.py +58 -0
  37. agent/watcher.py +515 -0
  38. agent/working_context_store/__init__.py +76 -0
  39. agent/working_context_store/_audit.py +46 -0
  40. agent/working_context_store/_encryption.py +75 -0
  41. agent/working_context_store/_store.py +1130 -0
  42. agent/working_context_store/_types.py +50 -0
  43. api.py +101 -0
  44. app/__init__.py +0 -0
  45. app/models.py +368 -0
  46. app/routes.py +451 -0
  47. app/service.py +1055 -0
  48. integrations/__init__.py +0 -0
  49. integrations/mcp_server/__init__.py +73 -0
  50. integrations/mcp_server/_dispatch.py +782 -0
  51. integrations/mcp_server/_schemas.py +484 -0
  52. integrations/mcp_server/_session.py +86 -0
  53. integrations/vscode_bridge.py +71 -0
  54. integrations/workspace_detect.py +259 -0
  55. main.py +2037 -0
  56. vectr-1.0.0.dist-info/METADATA +37 -0
  57. vectr-1.0.0.dist-info/RECORD +61 -0
  58. vectr-1.0.0.dist-info/WHEEL +5 -0
  59. vectr-1.0.0.dist-info/entry_points.txt +2 -0
  60. vectr-1.0.0.dist-info/licenses/LICENSE +21 -0
  61. vectr-1.0.0.dist-info/top_level.txt +5 -0
agent/chunk_quality.py ADDED
@@ -0,0 +1,1119 @@
1
+ """Language-agnostic chunk-quality heuristics (Opus audit Wave 1).
2
+
3
+ The four audited corpora (Python-multi, C, Zig, Rust) all showed the same
4
+ failure: low-information chunks outranking real code — bare ``}`` / ``return 0;``
5
+ (C), ``const log = std.log;`` (Zig), ``pub use …`` re-export blocks (Rust), and
6
+ heading-only markdown. This module centralises the predicates used to:
7
+
8
+ * drop / merge trivial chunks at index time (UPG-1.1)
9
+ * tag re-export / import-only "navigational" chunks (UPG-1.2)
10
+ * skip vectr-generated config + machine-generated files (UPG-1.3)
11
+ * fold a quality prior into search ranking (UPG-2.1)
12
+ * de-prioritise test files vs implementation (UPG-2.3)
13
+
14
+ Everything here is pure (no I/O) and cheap so it can run per-chunk at both index
15
+ and query time.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from pathlib import PurePosixPath
21
+
22
+ from agent.config import (
23
+ QUALITY_TRIVIAL as _Q_TRIVIAL,
24
+ QUALITY_NAVIGATIONAL as _Q_NAVIGATIONAL,
25
+ QUALITY_NAV_DECLARATION_RESCUE as _Q_NAV_DECLARATION_RESCUE,
26
+ QUALITY_HEADING_ONLY as _Q_HEADING_ONLY,
27
+ QUALITY_GENERATED as _Q_GENERATED,
28
+ QUALITY_VECTR_CONFIG as _Q_VECTR_CONFIG,
29
+ QUALITY_TEST_DEPRIORITISED as _Q_TEST_DEPRIORITISED,
30
+ TEST_FRAMEWORK_FAN_IN_THRESHOLD as _TEST_FRAMEWORK_FAN_IN_THRESHOLD,
31
+ QUALITY_DOC_PROSE as _Q_DOC_PROSE,
32
+ QUALITY_SHORT_PENALTY as _Q_SHORT_PENALTY,
33
+ QUALITY_PRIVATE_SYMBOL as _Q_PRIVATE_SYMBOL,
34
+ TRIVIAL_DOC_MAX_LINES as _TRIVIAL_DOC_MAX_LINES,
35
+ TRIVIAL_ATTR_CLASS_MAX_ATTRS as _TRIVIAL_ATTR_CLASS_MAX_ATTRS,
36
+ INDEXING_BUILD_ARTIFACT_DIR_SUFFIXES as _BUILD_ARTIFACT_DIR_SUFFIXES,
37
+ DUAL_VECTOR_MAX_SIGNATURE_LINES as _DV_MAX_SIGNATURE_LINES,
38
+ DUAL_VECTOR_MAX_DOCSTRING_LINES as _DV_MAX_DOCSTRING_LINES,
39
+ DUAL_VECTOR_MAX_DOCSTRING_CHARS as _DV_MAX_DOCSTRING_CHARS,
40
+ DOCSTRING_DEDUP_LINES as _DOCSTRING_DEDUP_LINES,
41
+ DOCSTRING_DEDUP_MIN_CHARS as _DOCSTRING_DEDUP_MIN_CHARS,
42
+ )
43
+
44
+ # A synthetic node_type stamped on re-export / import-only chunks so the ranker
45
+ # can recognise them without re-parsing.
46
+ NAVIGATIONAL_NODE_TYPE = "navigational"
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Line classification
51
+ # ---------------------------------------------------------------------------
52
+
53
+ # Prefixes the chunker prepends (leading comments, "# class: X" context). These
54
+ # are not the chunk's substance, so they're stripped before judging triviality.
55
+ _COMMENT_PREFIXES = ("#", "//", "/*", "*", "*/", "<!--", "-->")
56
+
57
+ # A line that carries no semantic payload on its own.
58
+ _TRIVIAL_LINE_RE = re.compile(
59
+ r"""^[\s]*(
60
+ [{}()\[\];,]+ # pure punctuation: } ; ){ etc.
61
+ | \#\s*(endif|else|elif|pragma\b.*) # C preprocessor noise
62
+ | return(\s+(0|NULL|nullptr|None|true|false|nil|-?\d+))?\s*;? # bare returns
63
+ | (pass|break|continue|\.\.\.);? # python/keyword stubs
64
+ | (else|do|try)\s*[:{]? # lone block openers
65
+ )[\s]*$""",
66
+ re.VERBOSE,
67
+ )
68
+
69
+ # A single-line variable/const declaration. On its own (Zig `variable_declaration`
70
+ # nodes, lone module constants) these carry almost no retrieval signal — the audit
71
+ # flagged `const log = std.log;`, `const seed = 42;`, `const fs = std.fs;`.
72
+ _DECL_LINE_RE = re.compile(r"^[\s]*(pub\s+)?(const|let|var|val|static|final)\s+\w+\s*[:=]")
73
+
74
+ # A single import / re-export / alias line (navigational, not implementation).
75
+ _IMPORT_LINE_RE = re.compile(
76
+ r"""^[\s]*(
77
+ (pub\s+)?use\s # rust use / pub use
78
+ | import\s | from\s.+\simport\s # python / js import
79
+ | export\s+\* # js barrel re-export
80
+ | export\s+\{ # js named re-export
81
+ | (export\s+)?\{?\s*[\w,\s]+\}?\s*from\s # ts re-export
82
+ | \#include\b # C include
83
+ | using\s # C++/C# using
84
+ | (const|let|var)\s+\w+\s*=\s*(require\(|@import\() # js require / zig @import alias
85
+ | const\s+\w+\s*=\s*@import\( # zig @import alias
86
+ | package\s | module\s # go/zig package decls
87
+ )""",
88
+ re.VERBOSE,
89
+ )
90
+
91
+ _MD_HEADING_RE = re.compile(r"^\s{0,3}#{1,6}\s")
92
+
93
+ # Two-line stub detection (UPG-15.1, Python-focused).
94
+ # A chunk whose only content is a declaration header followed by a lone stub body
95
+ # (pass / ... / raise NotImplementedError) has no retrieval value. Structural
96
+ # patterns — not tunables.
97
+ #
98
+ # _STUB_BODY_RE: the second meaningful line is a pure empty-body placeholder.
99
+ _STUB_BODY_RE = re.compile(
100
+ r"^[\s]*(pass|\.\.\.|(raise\s+NotImplementedError(\s*\(.*\))?));?[\s]*$"
101
+ )
102
+
103
+ # _DECL_HEADER_RE: the first meaningful line is a class declaration or a
104
+ # PARAMETER-LESS function declaration (empty parens, or self/cls only, with an
105
+ # optional return-type annotation). Functions with real parameters
106
+ # (e.g. ``def handle(request, uid=None):``) carry semantic signal in their
107
+ # signature even when the body is ``pass``, so those are excluded here.
108
+ # Class declarations are always matched regardless of base classes, because
109
+ # a bare class name + bases without any body is not a useful code chunk.
110
+ #
111
+ # Matches:
112
+ # class Foo: class Foo(Base, Meta):
113
+ # def foo(): def foo() -> T:
114
+ # async def foo(): async def foo() -> None:
115
+ # def foo(self): def foo(cls) -> T:
116
+ # Does NOT match (functions with real parameters):
117
+ # def handle(request): def send(sender, uid=None, **kwargs):
118
+ _DECL_HEADER_RE = re.compile(
119
+ r"^[\s]*(?:"
120
+ r"class\s+\w+[^:]*:" # class Name (any bases OK) + colon
121
+ r"|(?:async\s+)?def\s+\w+\s*\(" # def/async def Name(
122
+ r"(?:\s*(?:self|cls)\s*)?" # optional self or cls only
123
+ r"\s*\)\s*(?:->[^:]+)?" # optional -> return type
124
+ r":" # colon
125
+ r")\s*$"
126
+ )
127
+
128
+ # Attribute-assignment-only class body detection (UPG-15.9 / F25).
129
+ # A class header whose body consists ONLY of simple attribute assignments
130
+ # (e.g. ``class Meta:\n model = Writer\n fields = '__all__'``) is a
131
+ # configuration stub with no standalone retrieval value. Guarded to ONLY fire
132
+ # when there is no ``def`` / nested ``class`` / control flow — real small library
133
+ # classes (e.g. ``class Meta`` with a custom method) must survive.
134
+ #
135
+ # _CLASS_HEADER_RE: first meaningful line is a class declaration header.
136
+ _CLASS_HEADER_RE = re.compile(r"^[\s]*class\s+\w+[^:]*:\s*$")
137
+
138
+ # _ATTR_ASSIGN_RE: a line is an attribute assignment of the form
139
+ # ``identifier = <value>`` or ``identifier: type = <value>`` (Django/dataclass
140
+ # style). Matches any assignment; combined with _COMPLEX_RHS_RE to detect
141
+ # whether the RHS is a "simple" value (not a function call or dotted access).
142
+ _ATTR_ASSIGN_RE = re.compile(r"^[\s]*\w+\s*(?::[^=]+)?\s*=\s*(.+)$")
143
+
144
+ # _COMPLEX_RHS_RE: the right-hand side of an assignment is "complex" (carries
145
+ # real semantic signal) when it contains a function CALL or a dotted ATTRIBUTE
146
+ # access. An assignment like ``username = forms.CharField(...)`` defines a field
147
+ # with a specific type — real code with retrieval value. A "simple" assignment
148
+ # like ``model = Writer``, ``fields = '__all__'``, or a bare tuple/list literal
149
+ # ``fields = ()`` / ``fields = ('name', 'age')`` is a config option, trivial on
150
+ # its own. Two signals (either makes the RHS complex):
151
+ # * a call: a name/closing-bracket immediately followed by ``(``
152
+ # (``CharField(``, ``foo()``) — but NOT a grouping/tuple paren ``= (`` where
153
+ # ``(`` is preceded by whitespace/operator, so ``fields = ('a','b')`` stays
154
+ # simple.
155
+ # * dotted attribute access: an identifier followed by ``.`` (``forms.``,
156
+ # ``models.CASCADE``) — but NOT a numeric literal like ``1.5`` (digit-led).
157
+ _COMPLEX_RHS_RE = re.compile(r"[\w\]\)]\s*\(|[A-Za-z_]\w*\.")
158
+
159
+ # _BARE_CTOR_RHS_RE: the RHS captured by _ATTR_ASSIGN_RE is a bare constructor
160
+ # call — a single call to a PascalCase-named callable (the class-naming
161
+ # convention shared by Python/JS/TS/Java/C#/Go) with simple arguments only (no
162
+ # nested call, no dotted attribute access inside the parens). This is the
163
+ # "declare a module-level instance of an imported type" pattern used by
164
+ # re-export/manifest modules (``request_started = Signal()``,
165
+ # ``pre_init = ModelSignal(use_caching=True)``) — the statement adds no
166
+ # retrieval value beyond the import that already names the type, so a module
167
+ # consisting only of imports and such declarations is navigational, not
168
+ # implementation (UPG-PREFIX-COMPOSE).
169
+ _BARE_CTOR_RHS_RE = re.compile(r"^[A-Z]\w*\([^().]*\)[\s;]*$")
170
+
171
+ # _DECLARED_NAME_RE: an attribute-assignment line captured as (LHS identifier,
172
+ # RHS). Same shape as _ATTR_ASSIGN_RE but also captures the LHS name, so a
173
+ # bare-constructor-manifest line's declared identifier (``request_started`` in
174
+ # ``request_started = Signal()``) can be recovered (UPG-NAV-OVERDEMOTE-DECL).
175
+ _DECLARED_NAME_RE = re.compile(r"^[\s]*(\w+)\s*(?::[^=]+)?\s*=\s*(.+)$")
176
+
177
+ # _LEADING_DOCSTRING_DELIM_RE: a line that OPENS a Python triple-quoted string
178
+ # at its start (module/file docstring convention). Used only to recognise a
179
+ # LEADING module docstring block so it can be skipped when judging whether the
180
+ # rest of a chunk is import-only navigational content — a docstring describes
181
+ # the file, it is not itself implementation or an import (UPG-PREFIX-COMPOSE).
182
+ _LEADING_DOCSTRING_DELIM_RE = re.compile(r'^("""|\'\'\')')
183
+
184
+ # Lines that signal real logic in a class body — if any of these appear in the
185
+ # body, the chunk is NOT an attribute-only stub.
186
+ _HAS_DEF_OR_LOGIC_RE = re.compile(
187
+ r"""^[\s]*(
188
+ (async\s+)?def\s # method definition
189
+ | class\s # nested class
190
+ | (if|elif|else|for|while|try|except|finally|with|raise|return|yield|assert)\b
191
+ )""",
192
+ re.VERBOSE,
193
+ )
194
+
195
+ # _TRIVIAL_ATTR_CLASS_MAX_ATTRS is imported from agent.config (via config.yaml
196
+ # ranking.quality_priors.trivial_attr_class_max_attrs). No inline literal here —
197
+ # the config import at the top of this file already binds the name.
198
+
199
+
200
+ def _meaningful_lines(content: str) -> list[str]:
201
+ """Lines that are neither blank nor comment/context-prefix lines."""
202
+ out: list[str] = []
203
+ for raw in content.splitlines():
204
+ s = raw.strip()
205
+ if not s:
206
+ continue
207
+ if s.startswith(_COMMENT_PREFIXES):
208
+ continue
209
+ out.append(s)
210
+ return out
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Chunk predicates
215
+ # ---------------------------------------------------------------------------
216
+
217
+ def is_trivial_chunk(content: str, language: str = "") -> bool:
218
+ """True if the chunk has no standalone retrieval value (UPG-1.1).
219
+
220
+ A chunk is trivial when, after dropping comments/context lines, it is empty
221
+ or its only meaningful line is pure punctuation, a bare return, a lone block
222
+ keyword, or a single import. These never answer a query on their own.
223
+
224
+ UPG-15.5: HTML/markup and plain-text chunks with ≤ TRIVIAL_DOC_MAX_LINES
225
+ non-blank lines are also trivial. This catches 1–2-line test-fixture
226
+ templates ("Logged out", "{{ form }}", "<h1>Error</h1>") and egg-info TXT
227
+ stubs ("django", "from-my-custom-list") that otherwise flood short
228
+ natural-language queries. Multi-line .txt/.rst documentation (Django's
229
+ docs/howto/*.txt, docs/topics/*.txt) has many more lines and is unaffected.
230
+ """
231
+ raw_nonblank = [l for l in content.splitlines() if l.strip()]
232
+ if not raw_nonblank:
233
+ return True
234
+
235
+ # UPG-15.5: language-aware short-prose rule for HTML/TXT doc languages.
236
+ # Checked on raw_nonblank (not _meaningful_lines) so RST heading underlines
237
+ # (===, ---) and HTML tags count as non-blank content. Any real doc chunk
238
+ # has far more than TRIVIAL_DOC_MAX_LINES non-blank lines; only stub fixtures
239
+ # fall at or below the threshold.
240
+ lang_lower = (language or "").lower()
241
+ if lang_lower in _TRIVIAL_DOC_LANGUAGES and len(raw_nonblank) <= _TRIVIAL_DOC_MAX_LINES:
242
+ return True
243
+
244
+ lines = _meaningful_lines(content)
245
+ if not lines:
246
+ # Only comments/headings, no code/text payload. Trivial only when tiny —
247
+ # a large comment/doc block (e.g. a window over an unparsed file) is real
248
+ # coverage and must be kept.
249
+ return len(raw_nonblank) <= 2
250
+ if len(lines) == 1:
251
+ line = lines[0]
252
+ if _TRIVIAL_LINE_RE.match(line) or _IMPORT_LINE_RE.match(line) or _DECL_LINE_RE.match(line):
253
+ return True
254
+ # A single very short token line (e.g. `bad_extension = true;`) with no
255
+ # structure is also low value.
256
+ if len(line) <= 3:
257
+ return True
258
+ if len(lines) == 2:
259
+ # Declaration header + lone stub body → no retrieval value.
260
+ # e.g. "class Style:\n pass" or "def foo():\n ..."
261
+ # Only fires when: (1) first line is a class/def declaration header,
262
+ # AND (2) second line is a pure stub (pass / ... / raise NotImplementedError).
263
+ # A real second line ("return self.x", "x = 1") is NOT a stub body and
264
+ # keeps the chunk alive.
265
+ if _DECL_HEADER_RE.match(lines[0]) and _STUB_BODY_RE.match(lines[1]):
266
+ return True
267
+
268
+ # UPG-15.9 / F25: attribute-assignment-only class body with SIMPLE values.
269
+ # A class whose body is ONLY attribute assignments with no method definitions,
270
+ # nested classes, control flow, OR complex RHS (function calls, dotted access)
271
+ # is a configuration stub (e.g. Django's inner ``class Meta:`` with
272
+ # ``model=X, fields='__all__'``). 200+ such 3-line chunks exist in Django
273
+ # test files and flood doc-intent queries with zero educational content.
274
+ #
275
+ # Guard conditions (ALL must hold to classify as trivial):
276
+ # 1. First meaningful line is a class declaration header (not a def).
277
+ # 2. The class body has ≤ _TRIVIAL_ATTR_CLASS_MAX_ATTRS meaningful body lines.
278
+ # 3. Every body line is an attribute assignment (matches _ATTR_ASSIGN_RE).
279
+ # 4. NO body line has a complex RHS: no function call ``(`` or dotted ``.``.
280
+ # This preserves real Django form/model classes like
281
+ # ``username = forms.CharField(...)`` (dotted + parens → complex → kept).
282
+ # 5. NO body line contains a method def, nested class, or control-flow keyword.
283
+ #
284
+ # A class with any method, complex field declaration, or more body lines than
285
+ # _TRIVIAL_ATTR_CLASS_MAX_ATTRS is NOT trivial — it may be a real form,
286
+ # dataclass, NamedTuple, or library class.
287
+ if len(lines) >= 2 and _CLASS_HEADER_RE.match(lines[0]):
288
+ body_lines = lines[1:]
289
+ # Require at least 2 body lines: a single-attr-assignment class like
290
+ # ``class Foo:\n x = 1`` is kept by the UPG-15.1 invariant (the
291
+ # two-line stub rule only fires for pass/... stubs; a real assignment
292
+ # body keeps the chunk alive). The UPG-15.9 rule targets the
293
+ # multi-attribute config stubs (class Meta: model=X; fields='__all__').
294
+ if 1 < len(body_lines) <= _TRIVIAL_ATTR_CLASS_MAX_ATTRS:
295
+ # Reject immediately if any line has def/class/control flow
296
+ if not any(_HAS_DEF_OR_LOGIC_RE.match(bl) for bl in body_lines):
297
+ # Accept only if every body line is a simple attribute assignment
298
+ # (RHS has no function call parens or dotted attribute access).
299
+ if all(
300
+ (m := _ATTR_ASSIGN_RE.match(bl)) and not _COMPLEX_RHS_RE.search(m.group(1))
301
+ for bl in body_lines
302
+ ):
303
+ return True
304
+
305
+ return False
306
+
307
+
308
+ def _strip_leading_docstring(lines: list[str]) -> list[str]:
309
+ """Drop a leading Python module/file docstring block (UPG-PREFIX-COMPOSE).
310
+
311
+ A module docstring (``\"\"\"Multi-consumer multi-producer dispatching
312
+ mechanism...\"\"\"``) describes the file; it is not an import and not
313
+ implementation, so it should not by itself block an otherwise
314
+ import-only re-export shim (e.g. a package ``__init__.py`` that opens
315
+ with a docstring and then re-exports names) from being recognised as
316
+ navigational. Only strips a block that starts at ``lines[0]`` — a
317
+ triple-quoted string appearing later (e.g. inside a function body) is
318
+ left untouched.
319
+ """
320
+ if not lines:
321
+ return lines
322
+ m = _LEADING_DOCSTRING_DELIM_RE.match(lines[0])
323
+ if not m:
324
+ return lines
325
+ delim = m.group(1)
326
+ remainder = lines[0][len(delim):]
327
+ if delim in remainder:
328
+ return lines[1:]
329
+ for i in range(1, len(lines)):
330
+ if delim in lines[i]:
331
+ return lines[i + 1:]
332
+ return lines
333
+
334
+
335
+ def is_navigational_chunk(content: str, language: str = "") -> bool:
336
+ """True if every meaningful line is an import / re-export / alias, or a
337
+ bare module-level instantiation of an imported type (UPG-1.2, extended by
338
+ UPG-PREFIX-COMPOSE).
339
+
340
+ These are tables of contents (Rust ``lib.rs`` re-export blocks, Python
341
+ ``__init__`` import aggregators, JS/TS barrels) — they lexically match many
342
+ queries but contain no implementation. The extension also covers Python
343
+ "declaration manifest" modules that do nothing but import a type and
344
+ declare bare module-level instances of it (``request_started = Signal()``)
345
+ — same lack of standalone retrieval value as a pure re-export.
346
+ """
347
+ lines = _meaningful_lines(content)
348
+ if len(lines) < 2:
349
+ # single-line imports are handled by is_trivial_chunk; require a block
350
+ return False
351
+ body = _strip_leading_docstring(lines)
352
+ if not body:
353
+ return False
354
+ nav = 0
355
+ for line in body:
356
+ if _IMPORT_LINE_RE.match(line) or _TRIVIAL_LINE_RE.match(line):
357
+ nav += 1
358
+ elif (m := _ATTR_ASSIGN_RE.match(line)) and _BARE_CTOR_RHS_RE.match(m.group(1).strip()):
359
+ nav += 1
360
+ else:
361
+ return False
362
+ return nav == len(body)
363
+
364
+
365
+ def navigational_declared_identifiers(content: str) -> list[str]:
366
+ """Identifiers declared by bare-constructor-manifest assignment lines.
367
+
368
+ A chunk classified navigational via the bare-constructor-manifest pattern
369
+ (``request_started = Signal()``, UPG-PREFIX-COMPOSE) is not a pure
370
+ re-export — the LHS name of each such line IS the corpus-wide unique
371
+ declaration site of that identifier. quality_score() uses this to check
372
+ whether a query lexically names one of the declared identifiers before
373
+ applying the full navigational demotion (UPG-NAV-OVERDEMOTE-DECL / F59):
374
+ a manifest that IS the answer to "where is X defined" should not be
375
+ buried the same way a plain re-export block is. Returns [] for chunks
376
+ with no such lines (pure import blocks keep the full demotion).
377
+ """
378
+ out: list[str] = []
379
+ for line in _meaningful_lines(content):
380
+ m = _DECLARED_NAME_RE.match(line)
381
+ if m and _BARE_CTOR_RHS_RE.match(m.group(2).strip()):
382
+ out.append(m.group(1))
383
+ return out
384
+
385
+
386
+ # Identifier sub-word tokenizer — duplicated (not imported) from
387
+ # agent.searcher._code_tokenize's identifier-aware splitting, because
388
+ # searcher.py already imports FROM this module (importing back would cycle).
389
+ # Used only to compare a chunk's declared identifier names against the
390
+ # query's own already-tokenized BM25 tokens (lexical-match-gated), never as a
391
+ # standing query-keyword list.
392
+ _IDENT_SPLIT_RE = re.compile(r"[^a-zA-Z0-9]+")
393
+ _CAMEL_IDENT_RE = re.compile(r"([a-z])([A-Z])")
394
+
395
+
396
+ def _identifier_parts(text: str) -> frozenset[str]:
397
+ """Lowercase sub-word tokens of an identifier (camelCase + snake_case split)."""
398
+ expanded = _CAMEL_IDENT_RE.sub(r"\1 \2", text)
399
+ return frozenset(t for t in _IDENT_SPLIT_RE.split(expanded.lower()) if len(t) >= 2)
400
+
401
+
402
+ def is_markdown_heading_only(content: str) -> bool:
403
+ """True if a markdown chunk is essentially just heading(s) + a scrap of body.
404
+
405
+ ``_meaningful_lines`` already strips ``#``-prefixed lines (markdown headings
406
+ read as comments), so what's left is the prose body. A chunk with no body, or
407
+ a heading plus only a tiny fragment of text (e.g. ``### vectr_evict_hint`` /
408
+ ``No distinct content again.``), carries no real retrieval signal (UPG-2.1).
409
+ A genuine one-sentence section (≥40 chars of prose) is real content and kept.
410
+ """
411
+ body = _meaningful_lines(content)
412
+ if not body:
413
+ return True
414
+ return len(body) <= 2 and len(" ".join(body)) < 40
415
+
416
+
417
+ # ---------------------------------------------------------------------------
418
+ # File predicates
419
+ # ---------------------------------------------------------------------------
420
+
421
+ # vectr's own injected IDE-config files — noise in a *code* search index.
422
+ _VECTR_CONFIG_BASENAMES = {
423
+ "claude.md", "agents.md", "gemini.md", "codex.md",
424
+ ".cursorrules", ".mcp.json", "copilot-instructions.md",
425
+ }
426
+ _VECTR_CONFIG_DIRS = {".cursor", ".vscode"}
427
+
428
+ # Machine-generated / vendored file patterns (not hand-authored code).
429
+ _GENERATED_NAME_RES = [
430
+ re.compile(r".*_db\.h$"), # cpython unicodetype_db.h
431
+ re.compile(r".*_metadata\.h$"), # cpython pycore_uop_metadata.h
432
+ re.compile(r".*\.pb\.(go|cc|h|py)$"), # protobuf
433
+ re.compile(r".*_pb2\.pyi?$"), # python protobuf
434
+ re.compile(r".*\.min\.(js|css)$"), # minified
435
+ re.compile(r".*\.generated\..*$"),
436
+ re.compile(r".*\.g\.dart$"),
437
+ ]
438
+ _GENERATED_DIR_PARTS = {"clinic", "generated", "__generated__", "gen", "node_modules"}
439
+
440
+
441
+ def is_vectr_config_file(file_path: str) -> bool:
442
+ """True for vectr-injected IDE-config files that should never be indexed (UPG-1.3)."""
443
+ p = PurePosixPath(file_path.replace("\\", "/"))
444
+ if p.name.lower() in _VECTR_CONFIG_BASENAMES:
445
+ return True
446
+ return any(part in _VECTR_CONFIG_DIRS for part in p.parts)
447
+
448
+
449
+ def is_generated_file(file_path: str) -> bool:
450
+ """True for machine-generated / vendored files (UPG-1.3)."""
451
+ p = PurePosixPath(file_path.replace("\\", "/"))
452
+ name = p.name.lower()
453
+ if any(rx.match(name) for rx in _GENERATED_NAME_RES):
454
+ return True
455
+ return any(part.lower() in _GENERATED_DIR_PARTS for part in p.parts)
456
+
457
+
458
+ def is_build_artifact_file(file_path: str) -> bool:
459
+ """True for files inside build-artifact directories (UPG-15.9).
460
+
461
+ Detects files whose path contains a directory component that ends with one of
462
+ the configured build-artifact dir suffixes (e.g. ``.egg-info``, ``.dist-info``).
463
+ These directories are entirely machine-generated (Python packaging metadata,
464
+ file-path manifests, PKG-INFO) and contain no educational content — they flood
465
+ BM25 on module/command identifiers.
466
+
467
+ Examples that return True:
468
+ ``/project/Django.egg-info/SOURCES.txt``
469
+ ``/project/myapp.egg-info/PKG-INFO``
470
+ ``/project/mylib-1.0.dist-info/RECORD``
471
+
472
+ Real documentation (``docs/howto/*.txt``) is unaffected — ``docs`` does not
473
+ end with any of the configured suffixes.
474
+
475
+ Suffixes are sourced from ``indexing.build_artifact_dir_suffixes`` in
476
+ ``agent/config.yaml`` via ``config.INDEXING_BUILD_ARTIFACT_DIR_SUFFIXES``.
477
+ """
478
+ p = PurePosixPath(file_path.replace("\\", "/"))
479
+ # Check every directory component (not the filename itself).
480
+ for part in p.parts[:-1]:
481
+ part_lower = part.lower()
482
+ for suffix in _BUILD_ARTIFACT_DIR_SUFFIXES:
483
+ if part_lower.endswith(suffix):
484
+ return True
485
+ return False
486
+
487
+
488
+ # A C/C++ file basename that starts with "_test" (e.g. "_testutilmodule.c") —
489
+ # a shipped internal test module, following the leading-underscore-for-
490
+ # internal-module convention some C codebases use for their own test suite
491
+ # files (UPG-RUST-DEF-EVICTION / DEF-A). Distinct from the trailing "_test.c"
492
+ # / "_test.h" pattern below (a name that ENDS in "_test").
493
+ _C_LEADING_TEST_FILE_RE = re.compile(r"^_test.*\.(c|h)$")
494
+
495
+
496
+ def is_test_file(file_path: str) -> bool:
497
+ """True for test files, which should not outrank implementation (UPG-2.3)."""
498
+ p = PurePosixPath(file_path.replace("\\", "/"))
499
+ name = p.name.lower()
500
+ if name.startswith("test_") or name.endswith((
501
+ "_test.py", "_test.go", ".test.ts", ".test.js", ".spec.ts", ".spec.js",
502
+ "_test.c", "_test.h",
503
+ )):
504
+ return True
505
+ if re.match(r"test.*\.(java|kt)$", name):
506
+ return True
507
+ if _C_LEADING_TEST_FILE_RE.match(name):
508
+ return True
509
+ parts = {part.lower() for part in p.parts[:-1]}
510
+ return bool(parts & {"test", "tests", "__tests__", "spec", "testing"})
511
+
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # Content-structural inline-test detection (UPG-RUST-DEF-EVICTION / DEF-A)
515
+ # ---------------------------------------------------------------------------
516
+
517
+ # is_test_file() is purely path-based, so test code co-located INSIDE a
518
+ # production file evades the demotion above: a Rust #[test] fn (or one inside
519
+ # a #[cfg(test)] mod block) living in an otherwise-production .rs file, or a
520
+ # Zig inline `test "…" {` block, never has a test-named path to match. These
521
+ # are AST-emitted structural markers — the same class of signal as the
522
+ # existing _IMPORT_LINE_RE / _TRIVIAL_LINE_RE structural patterns in this
523
+ # module (language syntax, not a tunable vocabulary), gated by the chunk's
524
+ # own recorded `language` field so a coincidental line shape in an unrelated
525
+ # language can never trigger it.
526
+
527
+ # Rust test-attribute family. The chunker prepends a function's leading
528
+ # attribute lines as "comments" (indexer._get_leading_comments treats any
529
+ # `#`-prefixed line as a leading-comment line), so a #[test]-attributed fn's
530
+ # chunk content already starts with this line today — no reindex required.
531
+ _RUST_TEST_ATTR_RE = re.compile(r"^#\[\s*(test|tokio::test|cfg\(test\))\s*\]$")
532
+
533
+ # Zig inline test-block declaration head: `test "name" {` or bare `test {`.
534
+ _ZIG_TEST_DECL_RE = re.compile(r'^test\s*(".*?"\s*)?\{')
535
+
536
+
537
+ def is_content_structural_test_chunk(content: str, language: str = "") -> bool:
538
+ """True if the chunk's own text structurally marks it as test code, even
539
+ when its FILE path is not test-named (UPG-RUST-DEF-EVICTION / DEF-A).
540
+
541
+ Scans only the chunk's HEAD region — the leading attribute/comment lines
542
+ the chunker already prepends before the declaration line, plus the
543
+ declaration line itself — and stops at the first line that is neither an
544
+ attribute/comment nor a recognised test-block head. A string literal or
545
+ comment mentioning "#[test]"/"test {" somewhere INSIDE a function body is
546
+ therefore never matched; only a marker that actually precedes/opens the
547
+ declaration counts.
548
+
549
+ Known gap (not silently patched — honest limitation of this first cut): a
550
+ helper function defined inside the same `#[cfg(test)] mod { ... }` block
551
+ as a `#[test]` fn, but not itself carrying the `#[test]` attribute, has no
552
+ marker in its own chunk head and is not caught here.
553
+ """
554
+ lang = (language or "").lower()
555
+ for raw_line in content.splitlines():
556
+ stripped = raw_line.strip()
557
+ if not stripped:
558
+ continue
559
+ if lang == "rust" and _RUST_TEST_ATTR_RE.match(stripped):
560
+ return True
561
+ if lang == "zig" and _ZIG_TEST_DECL_RE.match(stripped):
562
+ return True
563
+ if not (stripped.startswith(_COMMENT_PREFIXES) or stripped.startswith("@")):
564
+ break
565
+ return False
566
+
567
+
568
+ # ---------------------------------------------------------------------------
569
+ # Quality prior (UPG-2.1)
570
+ # ---------------------------------------------------------------------------
571
+
572
+ # Documentation languages — prose, not implementation. On code-shaped queries the
573
+ # audit found substantive doc prose (blog sections, README walkthroughs, marketing
574
+ # HTML) burying real code, because the embedding model scores natural-language prose
575
+ # highly against natural-language queries. A mild demotion lets near-tied code edge
576
+ # ahead while leaving docs on top when nothing else competes (UPG-2.1).
577
+ _DOC_LANGUAGES = {"markdown", "md", "html", "htm", "rst", "text", "txt", "mdx"}
578
+
579
+ # Languages for which a very short chunk (≤ TRIVIAL_DOC_MAX_LINES non-blank lines)
580
+ # is classified as trivial (UPG-15.5). Covers test-fixture HTML templates and
581
+ # egg-info / requirements TXT stubs that flood short natural-language queries.
582
+ # Markdown is intentionally excluded: is_markdown_heading_only() handles its
583
+ # trivial sub-cases already (a 1-line markdown heading is caught there, and a
584
+ # single prose sentence has real retrieval value).
585
+ _TRIVIAL_DOC_LANGUAGES = {"html", "htm", "text", "txt"}
586
+
587
+
588
+ def is_doc_language(language: str) -> bool:
589
+ """True for documentation/prose languages (vs. implementation code)."""
590
+ return (language or "").lower() in _DOC_LANGUAGES
591
+
592
+
593
+ def is_private_symbol_name(symbol_name: str) -> bool:
594
+ """True for a symbol named by the "internal use" leading-underscore
595
+ convention (a single leading underscore, not a dunder).
596
+
597
+ Language-general, not query- or corpus-specific: Python's PEP 8 marks a
598
+ single leading underscore as "internal use only"; the same idiom appears
599
+ across most C-family/JS/Go/Rust codebases for a private helper that
600
+ supports, but is not itself, the public API. Dunder methods (``__init__``,
601
+ ``__str__``, ...) are excluded — they are public protocol hooks, not
602
+ private implementation detail.
603
+ """
604
+ name = symbol_name or ""
605
+ return name.startswith("_") and not name.startswith("__")
606
+
607
+
608
+ # Multipliers applied to the hybrid similarity score — sourced from
609
+ # agent/config.yaml (ranking.quality_priors) via agent/config.py.
610
+ # The _Q_* aliases are imported at the top of this file so all call sites
611
+ # inside this module continue to work without change (UPG-12.1).
612
+
613
+
614
+ def quality_score(
615
+ content: str,
616
+ file_path: str = "",
617
+ language: str = "",
618
+ node_type: str = "",
619
+ query_tokens: frozenset[str] = frozenset(),
620
+ file_fan_in: int = 0,
621
+ symbol_name: str = "",
622
+ ) -> float:
623
+ """A per-chunk usefulness prior in (0, 1], folded into ranking as a multiplier.
624
+
625
+ Relevance × usefulness: similarity already models relevance; this models
626
+ "is this chunk a good answer at all, regardless of similarity". Cheap and
627
+ language-agnostic. Lower = worse answer.
628
+
629
+ ``query_tokens`` (already-tokenized BM25 query tokens, not a keyword list)
630
+ softens the navigational demotion when the chunk is a bare-constructor-
631
+ manifest whose declared identifier the query names directly
632
+ (UPG-NAV-OVERDEMOTE-DECL / F59). ``file_fan_in`` (corpus-wide unambiguous
633
+ caller-file count, see symbol_graph.file_fan_in) exempts a shipped
634
+ testing-framework file misclassified by its test-named path from the
635
+ test-file demotion (UPG-TESTPATH-FRAMEWORK-MISCLASS / F58) — the SAME
636
+ exemption also covers a chunk classified as test by its own content
637
+ (is_content_structural_test_chunk, DEF-A) rather than its path.
638
+ ``symbol_name`` (the chunk's bare, unqualified symbol leaf) mildly demotes
639
+ a private/internal helper (UPG-16.1 / F30) — see is_private_symbol_name.
640
+ ``symbol_name`` + ``node_type`` together also EXEMPT a symbol-bearing
641
+ definition chunk from the trivial multiplier (UPG-TRIVIAL-DROP-ALIAS-DEFS)
642
+ — see is_definition_chunk.
643
+ """
644
+ if file_path and is_vectr_config_file(file_path):
645
+ return _Q_VECTR_CONFIG
646
+ if node_type == NAVIGATIONAL_NODE_TYPE or is_navigational_chunk(content, language):
647
+ if query_tokens:
648
+ for name in navigational_declared_identifiers(content):
649
+ parts = _identifier_parts(name)
650
+ if parts and parts <= query_tokens:
651
+ return _Q_NAV_DECLARATION_RESCUE
652
+ return _Q_NAVIGATIONAL
653
+ # UPG-TRIVIAL-DROP-ALIAS-DEFS: a chunk that is itself a symbol DEFINITION
654
+ # (real symbol_name + a class/struct/enum/interface/type-alias/function/
655
+ # method node_type, is_definition_chunk) is exempt from the trivial
656
+ # multiplier for the same reason it is exempt from the index-time
657
+ # trivial-DROP in _chunking.py's _postprocess_chunks — a one-line alias
658
+ # class (`class ModelForm(Base, metaclass=Meta): pass`) IS the canonical
659
+ # answer to "where is X defined", so scoring it identically to a bare
660
+ # `return`/lone-import chunk buries it beyond what the importance blend
661
+ # can recover. Falls through to the remaining rules below (test/doc/
662
+ # private-symbol/short-chunk demotions all still apply) rather than
663
+ # returning a special-cased score — a definition chunk is scored like
664
+ # any other real chunk, not given a blanket exemption from every prior.
665
+ if is_trivial_chunk(content, language) and not is_definition_chunk(symbol_name, node_type):
666
+ return _Q_TRIVIAL
667
+ if language == "markdown" and is_markdown_heading_only(content):
668
+ return _Q_HEADING_ONLY
669
+
670
+ score = 1.0
671
+ if file_path and is_generated_file(file_path):
672
+ score *= _Q_GENERATED
673
+ # DEF-A (UPG-RUST-DEF-EVICTION): a chunk is test code either because its
674
+ # FILE path says so (is_test_file, path-based, UPG-2.3) or because its OWN
675
+ # content structurally marks it as such (a Rust #[test] fn / Zig inline
676
+ # `test "…" {` block living in an otherwise-production file, UPG-2.3's
677
+ # path-only check cannot see these). Same demotion tier and same
678
+ # framework-fan-in escape apply regardless of which signal fired.
679
+ is_test_chunk = (
680
+ (file_path and is_test_file(file_path))
681
+ or is_content_structural_test_chunk(content, language)
682
+ )
683
+ if is_test_chunk and file_fan_in < _TEST_FRAMEWORK_FAN_IN_THRESHOLD:
684
+ score *= _Q_TEST_DEPRIORITISED
685
+ if is_doc_language(language):
686
+ score *= _Q_DOC_PROSE
687
+ if is_private_symbol_name(symbol_name):
688
+ score *= _Q_PRIVATE_SYMBOL
689
+
690
+ n_lines = len(_meaningful_lines(content))
691
+ if n_lines <= 2:
692
+ score *= _Q_SHORT_PENALTY
693
+ return score
694
+
695
+
696
+ # ---------------------------------------------------------------------------
697
+ # Class-context extraction (UPG-F4)
698
+ # ---------------------------------------------------------------------------
699
+
700
+ # Regex to detect a CLASS-prefix line injected by the indexer (UPG-F4).
701
+ # The indexer prepends "# class: ClassName\n" to method chunks so the embedding
702
+ # has class context. We extract this at query time to reconstruct the qualified
703
+ # name when symbol_name is a bare leaf.
704
+ _CLASS_PREFIX_RE = re.compile(r"^#\s*class:\s*(\w+)", re.MULTILINE)
705
+
706
+
707
+ def extract_class_from_content(content: str) -> str:
708
+ """Extract the class name from an indexer-injected '# class: X' prefix line.
709
+
710
+ The indexer prepends ``# class: ClassName`` to method chunks so they are
711
+ self-contained for the embedder (indexer.py _collect_chunks_ast). This
712
+ function recovers that class name at query time so we can reconstruct the
713
+ qualified ``ClassName.leaf`` form when ``symbol_name`` was stored as a bare
714
+ leaf (UPG-F4).
715
+
716
+ Returns the class name string, or ``""`` if no prefix is found.
717
+ """
718
+ m = _CLASS_PREFIX_RE.search(content)
719
+ return m.group(1) if m else ""
720
+
721
+
722
+ # ---------------------------------------------------------------------------
723
+ # Type-definition node_type prior (UPG-RUST-DEF-EVICTION / DEF-B)
724
+ # ---------------------------------------------------------------------------
725
+
726
+ # Tree-sitter node_type strings the indexer actually stamps on a chunk
727
+ # (agent/indexer/_chunking.py `_CHUNK_NODE_TYPES` — the node.type recorded on
728
+ # CodeChunk, NOT the symbol graph's presentational "kind" label) when the
729
+ # chunk is a TYPE DEFINITION (struct/enum/trait/class/interface/typedef)
730
+ # rather than a function/method or a type's separate implementation block.
731
+ # Verified empirically against the exact `_CHUNK_NODE_TYPES` dict per
732
+ # language — REACHABLE means this string can actually be found on an indexed
733
+ # chunk's node_type today. All entries below are REACHABLE
734
+ # (UPG-RUST-STRUCT-CHUNK-MISSING closed the last DORMANT gaps: Rust
735
+ # struct_item/trait_item/enum_item, TypeScript interface_declaration /
736
+ # type_alias_declaration / enum_declaration, Go type_declaration, and Java
737
+ # interface_declaration / enum_declaration were all added to
738
+ # `_CHUNK_NODE_TYPES` so each now gets its own top-level chunk instead of
739
+ # being silently swallowed by a sibling chunk or a window fallback):
740
+ # python: class_definition — REACHABLE
741
+ # javascript: class_declaration — REACHABLE
742
+ # typescript: class_declaration, interface_declaration,
743
+ # type_alias_declaration, enum_declaration — REACHABLE
744
+ # java: class_declaration, interface_declaration,
745
+ # enum_declaration — REACHABLE
746
+ # go: type_declaration — REACHABLE (single node
747
+ # type covers struct/interface/alias forms in this grammar)
748
+ # rust: struct_item, trait_item, enum_item — REACHABLE. impl_item
749
+ # is deliberately EXCLUDED from this set: an impl block is an
750
+ # implementation of a type, not the type's own definition
751
+ # (symbol_graph already draws this same distinction, UPG-4.5).
752
+ # c / cpp: struct_specifier, enum_specifier, type_definition — REACHABLE
753
+ # cpp only: class_specifier — REACHABLE
754
+ _TYPE_DEF_NODE_TYPES: frozenset[str] = frozenset({
755
+ "class_definition",
756
+ "class_declaration",
757
+ "interface_declaration",
758
+ "type_alias_declaration",
759
+ "enum_declaration",
760
+ "type_declaration",
761
+ "struct_item",
762
+ "trait_item",
763
+ "enum_item",
764
+ "struct_specifier",
765
+ "enum_specifier",
766
+ "type_definition",
767
+ "class_specifier",
768
+ })
769
+
770
+ # Zig has no distinct type-definition node type: both `pub const Foo = struct
771
+ # {...}` (a genuine type definition) and a plain `pub const x = 5;` (an
772
+ # ordinary constant) parse to the SAME chunk node_type, "variable_declaration"
773
+ # (verified empirically against a live tree-sitter-zig parse and against
774
+ # agent/indexer/_chunking.py `_CHUNK_NODE_TYPES["zig"]`). Matching on
775
+ # node_type alone would therefore boost every top-level Zig constant, not
776
+ # just type factories — so a Zig "variable_declaration" chunk additionally
777
+ # needs this narrow, declaration-HEAD-only content check (never the body) for
778
+ # the `= struct {` / `= enum {` / `= union {` / `= opaque {` factory shape
779
+ # before the type-def prior is granted.
780
+ _ZIG_TYPE_FACTORY_HEAD_RE = re.compile(
781
+ r"^(pub\s+)?const\s+\w+\s*=\s*(struct|enum|union|opaque)\b"
782
+ )
783
+
784
+
785
+ def is_type_definition_chunk(node_type: str, content: str = "", language: str = "") -> bool:
786
+ """True if a chunk defines a TYPE (struct/enum/trait/class/typedef), as
787
+ opposed to a function/method or a type's separate implementation block
788
+ (UPG-RUST-DEF-EVICTION / DEF-B).
789
+
790
+ When a type definition and a usage or test site of the same name compete
791
+ at similar relevance, the definition is the canonical answer to "where is
792
+ X defined" — usages are one vectr_trace call away. This is a chunk-
793
+ PROPERTY check computed only from the chunk's own recorded node_type/
794
+ content/language, never from the query.
795
+ """
796
+ if node_type in _TYPE_DEF_NODE_TYPES:
797
+ return True
798
+ if (language or "").lower() == "zig" and node_type == "variable_declaration":
799
+ first_line = next((l.strip() for l in content.splitlines() if l.strip()), "")
800
+ return bool(_ZIG_TYPE_FACTORY_HEAD_RE.match(first_line))
801
+ return False
802
+
803
+
804
+ # ---------------------------------------------------------------------------
805
+ # Module-level function node_type (UPG-SIBLING-TYPEDEF-CROWDING)
806
+ # ---------------------------------------------------------------------------
807
+
808
+ # Tree-sitter node_type strings the indexer stamps on a FUNCTION chunk (see
809
+ # agent/indexer/_chunking.py `_CHUNK_NODE_TYPES`) across every language with a
810
+ # symbol graph. Deliberately excludes method node_types ("method_definition",
811
+ # "method_declaration") — those are always owned by a class/receiver, even on
812
+ # the rare occasion the "# class: X" context prefix failed to attach, so they
813
+ # must keep going through the existing owning-class attribution path rather
814
+ # than being mistaken for a standalone function.
815
+ _FUNCTION_NODE_TYPES: frozenset[str] = frozenset({
816
+ "function_definition", # python, c, cpp
817
+ "function_declaration", # javascript, typescript, go, zig (go/zig: never a receiver method — see "method_declaration")
818
+ "function_expression", # javascript, typescript
819
+ "arrow_function", # javascript, typescript
820
+ "function_item", # rust (outside an impl_item)
821
+ })
822
+
823
+ # Method node_type strings the indexer stamps on a chunk (see `_CHUNK_NODE_TYPES`
824
+ # in agent/indexer/_chunking.py) for languages whose grammar gives a method its
825
+ # own distinct node type — javascript/typescript ("method_definition"), go/java
826
+ # ("method_declaration"). Python has no separate method node type: a method's
827
+ # `def` parses to the same "function_definition" already in
828
+ # `_FUNCTION_NODE_TYPES` above, distinguished from a module-level function only
829
+ # by its (indentation-derived) class context, not by node_type. Kept apart
830
+ # from `_FUNCTION_NODE_TYPES` because that set's own docstring records a
831
+ # DELIBERATE exclusion of method node types for a different purpose (owning-
832
+ # class importance attribution, UPG-SIBLING-TYPEDEF-CROWDING) — this set is
833
+ # for the orthogonal question of "is this chunk a symbol DEFINITION at all"
834
+ # (UPG-TRIVIAL-DROP-ALIAS-DEFS), where a method is exactly as much a
835
+ # definition as a module-level function.
836
+ _METHOD_NODE_TYPES: frozenset[str] = frozenset({
837
+ "method_definition",
838
+ "method_declaration",
839
+ })
840
+
841
+ # The full symbol-DEFINITION node_type family (UPG-TRIVIAL-DROP-ALIAS-DEFS):
842
+ # type definitions (class/struct/enum/interface/trait/type-alias) plus
843
+ # function/method definitions. A chunk whose node_type is in this set actually
844
+ # DECLARES a named symbol — as opposed to a navigational/window/markdown-
845
+ # section chunk, or an implementation block (Rust `impl_item`) that is not
846
+ # itself the type's own definition site.
847
+ _DEFINITION_NODE_TYPES: frozenset[str] = (
848
+ _TYPE_DEF_NODE_TYPES | _FUNCTION_NODE_TYPES | _METHOD_NODE_TYPES
849
+ )
850
+
851
+
852
+ def is_definition_chunk(symbol_name: str, node_type: str) -> bool:
853
+ """True if a chunk is a real symbol DEFINITION — a named class, struct,
854
+ enum, interface, type-alias, function, or method (UPG-TRIVIAL-DROP-ALIAS-
855
+ DEFS).
856
+
857
+ Used to EXEMPT an otherwise content-trivial chunk (e.g. a one-line
858
+ ``class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass): pass``
859
+ alias, or a single-statement function body) from the UPG-1.1 trivial-drop
860
+ at index time: a symbol-bearing definition is the canonical answer to
861
+ "where is X defined" even when its body is a bare ``pass``/one-liner, so
862
+ dropping it makes the symbol structurally unfindable by search (the
863
+ symbol graph / ``locate`` is unaffected — this is a search-only gap).
864
+ A pure chunk-PROPERTY check (node_type + the chunk's own recorded
865
+ symbol_name), never the query.
866
+ """
867
+ return bool(symbol_name) and node_type in _DEFINITION_NODE_TYPES
868
+
869
+
870
+ def is_module_level_function_chunk(node_type: str, class_ctx: str) -> bool:
871
+ """True if a chunk defines a MODULE-LEVEL function (not a method), as
872
+ opposed to a method chunk whose owning class happened to resolve (or a
873
+ type definition / type's implementation block).
874
+
875
+ A module-level function has its own reference-frequency importance (ARCH-2
876
+ extension, UPG-SIBLING-TYPEDEF-CROWDING) exactly like a class/struct name
877
+ does — a corpus-central function (e.g. one call thousands of sites route
878
+ through) can otherwise lose to a same-file, rarely-referenced sibling
879
+ function at a near-tie base relevance, the same crowding class of problem
880
+ ARCH-2 already fixes for classes.
881
+
882
+ `class_ctx` is the caller's already-extracted
883
+ `extract_class_from_content(content)` result — passed in rather than
884
+ recomputed here since every caller already has it at hand. This is a pure
885
+ chunk-PROPERTY check (node_type + the chunk's own recovered class context),
886
+ never the query.
887
+ """
888
+ return node_type in _FUNCTION_NODE_TYPES and not class_ctx
889
+
890
+
891
+ # ---------------------------------------------------------------------------
892
+ # Purpose-text distillation (ARCH-4 dual-vector pool entry)
893
+ # ---------------------------------------------------------------------------
894
+
895
+ # node_types the chunker stamps that are never symbol definitions — a purpose
896
+ # vector (qualified signature + docstring) only makes sense for a chunk that
897
+ # actually declares a function/method/class/type. Markdown sections, sliding-
898
+ # window fallback chunks, and re-export blocks carry no signature to distil.
899
+ _NON_SYMBOL_NODE_TYPES = {NAVIGATIONAL_NODE_TYPE, "window", "section"}
900
+
901
+ # The "# class: X" context line the indexer prepends to method chunks (see
902
+ # extract_class_from_content) — excluded from the leading-doc scan below so it
903
+ # doesn't get embedded twice (once as the qualified name, once as raw text).
904
+ _CLASS_PREFIX_LINE_RE = re.compile(r"^#\s*class:\s*\w+\s*$")
905
+
906
+ # A declaration line's block-opening terminator: python's trailing ':' or a
907
+ # C-family/Rust/Go/Java/Zig trailing '{'. Trailing whitespace/comment-safe.
908
+ _SIGNATURE_END_RE = re.compile(r"[:{]\s*(//.*|#.*)?$")
909
+
910
+ # First statement of a Python function/class body is a string literal — the
911
+ # docstring convention. Matches from the very start of the (stripped) body
912
+ # text; DOTALL so a multi-line triple-quoted docstring is captured whole.
913
+ _PY_DOCSTRING_RE = re.compile(
914
+ r'^[rRbBuU]{0,2}(?P<q>"""|\'\'\')(?P<body>.*?)(?P=q)', re.DOTALL,
915
+ )
916
+ # One-line plain-quoted docstring (less common but valid Python).
917
+ _PY_DOCSTRING_ONELINE_RE = re.compile(
918
+ r"^[rRbBuU]{0,2}(?P<q>['\"])(?P<body>(?:(?!(?P=q)).)*)(?P=q)\s*$"
919
+ )
920
+
921
+
922
+ def is_symbol_bearing_chunk(symbol_name: str, node_type: str) -> bool:
923
+ """True if a chunk declares a real symbol worth a purpose vector.
924
+
925
+ A symbol chunk has a non-empty `symbol_name` AND a node_type that is an
926
+ actual AST definition node — not a navigational/window/markdown-section
927
+ chunk (those have no signature to distil).
928
+ """
929
+ return bool(symbol_name) and node_type not in _NON_SYMBOL_NODE_TYPES
930
+
931
+
932
+ def _leading_doc_and_code(lines: list[str]) -> tuple[list[str], list[str]]:
933
+ """Split a chunk's lines into (leading doc/decorator lines, remaining code).
934
+
935
+ "Leading" comments/decorators (JSDoc, rustdoc, godoc, `@decorator`) precede
936
+ the declaration for most languages — already prepended to chunk content by
937
+ the chunker's `_get_leading_comments`. The injected "# class: X" context
938
+ line is skipped (it is not documentation prose).
939
+ """
940
+ doc: list[str] = []
941
+ i = 0
942
+ while i < len(lines):
943
+ stripped = lines[i].strip()
944
+ if not stripped:
945
+ i += 1
946
+ continue
947
+ if _CLASS_PREFIX_LINE_RE.match(stripped):
948
+ i += 1
949
+ continue
950
+ if stripped.startswith(_COMMENT_PREFIXES) or stripped.startswith("@"):
951
+ doc.append(stripped)
952
+ i += 1
953
+ continue
954
+ break
955
+ return doc, lines[i:]
956
+
957
+
958
+ def _extract_signature(code_lines: list[str]) -> tuple[list[str], int]:
959
+ """Return (signature lines, index of first body line) from a declaration.
960
+
961
+ Accumulates lines from the start of `code_lines` until one ends the block
962
+ opener (python ':' / brace-family '{'), or `_DV_MAX_SIGNATURE_LINES` is
963
+ reached — bounds pathological multi-line parameter lists.
964
+ """
965
+ sig: list[str] = []
966
+ for i, line in enumerate(code_lines[:_DV_MAX_SIGNATURE_LINES]):
967
+ sig.append(line.strip())
968
+ if _SIGNATURE_END_RE.search(line.rstrip()):
969
+ return sig, i + 1
970
+ return sig, min(len(code_lines), _DV_MAX_SIGNATURE_LINES)
971
+
972
+
973
+ def _first_paragraph(text: str) -> str:
974
+ """The text up to (not including) the first blank line, else the whole text.
975
+
976
+ PEP 257 convention (mirrored by Google/NumPy docstring styles): a
977
+ multi-line docstring is a one-line summary, a blank line, then an
978
+ elaborated description — often a structured block (``Args:``, attribute
979
+ lists, examples). The summary line alone already carries the purpose;
980
+ everything after the first blank line is detail for a human reader, not
981
+ additional intent signal. Keeping it anyway measurably dilutes the
982
+ embedding (see ARCH-4-DEBUG spike evidence), the same class of problem the
983
+ purpose vector exists to defeat — just recurring one level down inside the
984
+ docstring itself for structured multi-paragraph text.
985
+ """
986
+ m = re.search(r"\n[ \t]*\n", text)
987
+ return text[: m.start()] if m else text
988
+
989
+
990
+ def _extract_python_docstring(body_lines: list[str]) -> str:
991
+ """First-statement docstring from a Python function/class body, if any."""
992
+ body_text = "\n".join(body_lines).strip()
993
+ if not body_text:
994
+ return ""
995
+ m = _PY_DOCSTRING_RE.match(body_text) or _PY_DOCSTRING_ONELINE_RE.match(
996
+ body_text.splitlines()[0].strip() if body_text.splitlines() else ""
997
+ )
998
+ if not m:
999
+ return ""
1000
+ doc = _first_paragraph(m.group("body").strip())
1001
+ doc_lines = doc.splitlines()[:_DV_MAX_DOCSTRING_LINES]
1002
+ return "\n".join(doc_lines)[:_DV_MAX_DOCSTRING_CHARS]
1003
+
1004
+
1005
+ def build_purpose_text(
1006
+ content: str, symbol_name: str, node_type: str, language: str = "",
1007
+ ) -> str | None:
1008
+ """Distil a symbol-bearing chunk down to qualified signature + docstring.
1009
+
1010
+ ARCH-4: the STEP-0 spike proved a mechanical implementation body dilutes
1011
+ the intent-bearing tokens (signature + docstring) when mean-pooled into a
1012
+ single body embedding — the canonical chunk can miss dense pool entry
1013
+ entirely even though its own docstring already paraphrases the query. This
1014
+ builds the body-stripped text embedded as the chunk's second "purpose"
1015
+ vector: `ClassName.symbol_name` (class-qualified when the indexer recorded
1016
+ class context) + the raw declaration line(s) (which carry the parameter
1017
+ list) + the docstring (Python: first body statement) or leading
1018
+ comment/decorator block (other languages' pre-declaration doc convention).
1019
+
1020
+ Returns None for non-symbol chunks (`is_symbol_bearing_chunk` False) — no
1021
+ purpose vector is stored for markdown/navigational/window chunks. An
1022
+ undocumented symbol still returns a non-None signature-only text (graceful
1023
+ degradation — no docstring found is not an error).
1024
+ """
1025
+ if not is_symbol_bearing_chunk(symbol_name, node_type):
1026
+ return None
1027
+
1028
+ lines = content.splitlines()
1029
+ class_ctx = extract_class_from_content(content)
1030
+ qualified_name = f"{class_ctx}.{symbol_name}" if class_ctx else symbol_name
1031
+
1032
+ leading_doc, code_lines = _leading_doc_and_code(lines)
1033
+ signature, body_start = _extract_signature(code_lines)
1034
+
1035
+ docstring = ""
1036
+ if (language or "").lower() == "python":
1037
+ docstring = _extract_python_docstring(code_lines[body_start:])
1038
+
1039
+ parts = [qualified_name]
1040
+ if signature:
1041
+ parts.append("\n".join(signature))
1042
+ if leading_doc:
1043
+ # Same size caps as the Python docstring branch (max_docstring_lines /
1044
+ # max_docstring_chars) — a long JSDoc/rustdoc/godoc header block dilutes
1045
+ # the purpose embedding exactly like an untruncated Python docstring
1046
+ # would; this branch was previously uncapped.
1047
+ capped_doc = leading_doc[:_DV_MAX_DOCSTRING_LINES]
1048
+ parts.append("\n".join(capped_doc)[:_DV_MAX_DOCSTRING_CHARS])
1049
+ if docstring:
1050
+ parts.append(docstring)
1051
+ return "\n".join(parts)
1052
+
1053
+
1054
+ # ---------------------------------------------------------------------------
1055
+ # Dedup (UPG-2.2)
1056
+ # ---------------------------------------------------------------------------
1057
+
1058
+ def normalized_content(content: str) -> str:
1059
+ """Whitespace-collapsed lowercase form for exact/near-duplicate detection."""
1060
+ return re.sub(r"\s+", " ", content).strip().lower()
1061
+
1062
+
1063
+ # A leading attribute/decorator line — Python/Java `@decorator`/`@Override`,
1064
+ # Rust/Zig `#[attr]` — is a STRUCTURAL marker, not documentation prose, even
1065
+ # though `_leading_doc_and_code` sweeps it into the same "leading comment"
1066
+ # block as a real doc-comment (both start with `@`/`#`, the same convention
1067
+ # `_get_leading_comments` uses at chunk-creation time). Left unfiltered here,
1068
+ # it shadows the real docstring in two ways: (1) a chunk whose ONLY leading
1069
+ # line is a decorator (e.g. `@abc.abstractmethod` immediately above a Python
1070
+ # method whose docstring is its FIRST body statement, not a leading comment)
1071
+ # makes `doc_text` non-empty, so the Python-docstring fallback below never
1072
+ # runs and the dedup key becomes the decorator text itself (too short/generic
1073
+ # to mean anything, or worse: identical across UNRELATED overrides that merely
1074
+ # share `@abc.abstractmethod`); (2) two DIFFERENT Rust structs sharing only a
1075
+ # `#[derive(Debug, Clone)]` line with no `///` doc would wrongly key on that
1076
+ # shared attribute and collapse together. Filtering these lines out before
1077
+ # keying — and falling through to the doc that follows them, or to no key at
1078
+ # all if there is none — fixes both.
1079
+ _ATTR_DECORATOR_LINE_RE = re.compile(r"^(@\w|#\[)")
1080
+
1081
+
1082
+ def leading_docstring_key(content: str, language: str = "") -> str:
1083
+ """Normalized leading docstring/comment-block key for near-dup collapse
1084
+ (UPG-RUST-DEF-EVICTION / DEF-C).
1085
+
1086
+ Byte-identical full-content dedup (`normalized_content`, UPG-2.2) misses
1087
+ near-duplicate boilerplate: several thin wrapper/test chunks that copy the
1088
+ same rustdoc/JSDoc/docstring header onto otherwise-different bodies still
1089
+ bury the canonical definition under look-alike results. This returns a
1090
+ second, independent dedup key built ONLY from the chunk's own leading
1091
+ doc/comment lines (reusing the same `_leading_doc_and_code` /
1092
+ `_extract_python_docstring` split used for purpose-text distillation),
1093
+ with attribute/decorator lines filtered out (see `_ATTR_DECORATOR_LINE_RE`
1094
+ above), capped at `_DOCSTRING_DEDUP_LINES` lines.
1095
+
1096
+ Returns "" (never a valid dedup key — the caller must treat an empty
1097
+ string as "do not collapse this chunk on this key") when the chunk has no
1098
+ leading doc at all (once attribute/decorator lines are filtered out), or
1099
+ when the normalized header is shorter than `_DOCSTRING_DEDUP_MIN_CHARS` —
1100
+ a trivial/near-empty header must not fold together chunks that merely
1101
+ share a blank or one-word comment (or, per the above, a bare decorator).
1102
+ """
1103
+ lines = content.splitlines()
1104
+ leading_doc, code_lines = _leading_doc_and_code(lines)
1105
+ leading_doc = [l for l in leading_doc if not _ATTR_DECORATOR_LINE_RE.match(l)]
1106
+
1107
+ doc_text = "\n".join(leading_doc)
1108
+ if not doc_text and (language or "").lower() == "python":
1109
+ _, body_start = _extract_signature(code_lines)
1110
+ doc_text = _extract_python_docstring(code_lines[body_start:])
1111
+
1112
+ if not doc_text:
1113
+ return ""
1114
+
1115
+ capped = "\n".join(doc_text.splitlines()[:_DOCSTRING_DEDUP_LINES])
1116
+ normalized = normalized_content(capped)
1117
+ if len(normalized) < _DOCSTRING_DEDUP_MIN_CHARS:
1118
+ return ""
1119
+ return normalized