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.
- agent/__init__.py +0 -0
- agent/cartographer.py +428 -0
- agent/chunk_quality.py +1119 -0
- agent/config.py +648 -0
- agent/config.yaml +1153 -0
- agent/eviction_advisor.py +447 -0
- agent/identifier_hint.py +68 -0
- agent/indexer/__init__.py +112 -0
- agent/indexer/_chunking.py +458 -0
- agent/indexer/_constants.py +105 -0
- agent/indexer/_core.py +878 -0
- agent/indexer/_types.py +157 -0
- agent/instance_registry.py +127 -0
- agent/llm_client.py +8 -0
- agent/model_cache.py +101 -0
- agent/prompt_templates.py +31 -0
- agent/searcher.py +848 -0
- agent/strategy_selector.py +299 -0
- agent/symbol_graph/__init__.py +131 -0
- agent/symbol_graph/_constants.py +366 -0
- agent/symbol_graph/_extraction.py +528 -0
- agent/symbol_graph/_graph.py +1878 -0
- agent/symbol_graph/_types.py +35 -0
- agent/templates/claude_md.md +65 -0
- agent/templates/claude_md_search_only.md +27 -0
- agent/templates/cursor_mcp.json.template +7 -0
- agent/templates/cursor_rules_header.txt +5 -0
- agent/templates/hook_no_double_recall.txt +1 -0
- agent/templates/mcp.json.template +8 -0
- agent/templates/session_start_guidance_default.txt +3 -0
- agent/templates/session_start_guidance_hooks_aware.txt +1 -0
- agent/templates/tool_loading_guidance_claude.txt +1 -0
- agent/templates/tool_loading_guidance_claude_search_only.txt +1 -0
- agent/templates/vscode_mcp.json.template +8 -0
- agent/tool_necessity_probe.py +177 -0
- agent/version_stamp.py +58 -0
- agent/watcher.py +515 -0
- agent/working_context_store/__init__.py +76 -0
- agent/working_context_store/_audit.py +46 -0
- agent/working_context_store/_encryption.py +75 -0
- agent/working_context_store/_store.py +1130 -0
- agent/working_context_store/_types.py +50 -0
- api.py +101 -0
- app/__init__.py +0 -0
- app/models.py +368 -0
- app/routes.py +451 -0
- app/service.py +1055 -0
- integrations/__init__.py +0 -0
- integrations/mcp_server/__init__.py +73 -0
- integrations/mcp_server/_dispatch.py +782 -0
- integrations/mcp_server/_schemas.py +484 -0
- integrations/mcp_server/_session.py +86 -0
- integrations/vscode_bridge.py +71 -0
- integrations/workspace_detect.py +259 -0
- main.py +2037 -0
- vectr-1.0.0.dist-info/METADATA +37 -0
- vectr-1.0.0.dist-info/RECORD +61 -0
- vectr-1.0.0.dist-info/WHEEL +5 -0
- vectr-1.0.0.dist-info/entry_points.txt +2 -0
- vectr-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|