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/config.yaml ADDED
@@ -0,0 +1,1153 @@
1
+ # vectr internal configuration — versioned in the repo.
2
+ # All tunables that affect ranking behaviour live here so changes are auditable
3
+ # via git history rather than buried in code.
4
+ #
5
+ # Do NOT set these via environment variables or per-user config files.
6
+ # Runtime code reads this file via importlib.resources so it works for both
7
+ # the repo-local interpreter and the pip-installed global binary.
8
+
9
+ ranking:
10
+ quality_priors:
11
+ # Multipliers applied to the hybrid similarity score. 1.0 = neutral.
12
+ # Lower = de-ranked further. Applied in chunk_quality.quality_score().
13
+
14
+ # Bare stub chunks with almost no content (single-line import, lone brace, etc.)
15
+ trivial: 0.15
16
+
17
+ # Maximum number of non-blank lines a HTML/markup or plain-text chunk may contain
18
+ # and still be classified as trivial by is_trivial_chunk() (UPG-15.5).
19
+ # 1-line and 2-line test-fixture templates ("Logged out", "{{ form }}", top_level.txt)
20
+ # are trivial; any real doc prose chunk has many more lines than this.
21
+ # Must be a small integer (≥1) so that multi-line .txt/.rst documentation
22
+ # (Django docs/howto/*.txt, docs/topics/*.txt) are NOT classified trivial.
23
+ # Conservative default: 2 — catches single-line HTML templates and 1–2-line
24
+ # TXT fixtures without risk of demoting substantive documentation.
25
+ trivial_doc_max_lines: 2
26
+
27
+ # Re-export / import-only "navigational" chunks (no implementation substance)
28
+ navigational: 0.35
29
+
30
+ # UPG-NAV-OVERDEMOTE-DECL (F59): a bare-constructor-manifest chunk (UPG-
31
+ # PREFIX-COMPOSE's ``request_started = Signal()`` pattern) is ALSO the
32
+ # corpus-wide unique declaration site of each name it assigns. For a
33
+ # conceptual query ("signal dispatcher implementation") the manifest is
34
+ # correctly navigational-demoted to `navigational` above (F44/F53's
35
+ # re-export-stub-overrank fix). For a query that names one of the
36
+ # declared identifiers directly ("where is request_started signal
37
+ # defined") — a lexical match, gated on the query's own BM25 tokens, not
38
+ # a keyword list — the chunk IS the singular correct answer and the full
39
+ # navigational demotion buries it (F59: django/core/signals.py absent
40
+ # from top-25 despite being in the candidate pool). This softened value
41
+ # applies only in that lexically-gated case (chunk_quality.quality_score
42
+ # query_tokens argument); every other query for the same chunk still
43
+ # gets the full `navigational` demotion. Measured against the F59 offline
44
+ # replay (benchmarks/acceptance/product_cases.jsonl, django corpus,
45
+ # tmp/vectr-accept-django index): the pool-rank gap needs a value in this
46
+ # range to surface the chunk in top-10 without letting the still-nav-
47
+ # shaped chunk outrank undemoted real implementation code (kept < 1.0).
48
+ navigational_declaration_rescue: 0.75
49
+
50
+ # Markdown chunks that contain only headings (no prose or code body)
51
+ heading_only: 0.40
52
+
53
+ # Machine-generated files (migrations, protobuf output, build artifacts)
54
+ generated: 0.45
55
+
56
+ # vectr's own config files — we never want to surface our own internals
57
+ vectr_config: 0.10
58
+
59
+ # Test files when the query does not explicitly target tests. UPG-PREFIX-
60
+ # COMPOSE: lowered from 0.55 — once the query-prefix fix (UPG-ARCTIC-QUERY-
61
+ # PREFIX) changed dense pool composition, a test-file symbol whose name or
62
+ # docstring happens to restate the query in plain language (e.g. a helper
63
+ # class documented as "track threads and kwargs when signals are
64
+ # dispatched") could out-rank the canonical production symbol it merely
65
+ # name-echoes, because purpose_rank's textual-similarity boost (below) is
66
+ # otherwise blind to quality tier. 0.46 is the largest reduction that still
67
+ # satisfies test_ordering's invariant (test_deprioritised > generated=0.45,
68
+ # tests/test_chunk_quality.py::TestQualityScore::test_ordering) — this value
69
+ # only matters in combination with purpose_rank.lambda below (see there for
70
+ # the joint tuning rationale); it does not fix anything on its own.
71
+ test_deprioritised: 0.46
72
+
73
+ # UPG-TESTPATH-FRAMEWORK-MISCLASS (F58): is_test_file()'s path-segment
74
+ # heuristic (any "test"/"tests"/... directory component) cannot tell a
75
+ # disposable test module apart from a shipped testing-FRAMEWORK subpackage
76
+ # that happens to live at a test-named path (e.g. django/test/utils.py —
77
+ # a stable public API, not a throwaway test). Filtering by "called only
78
+ # from non-test files" was measured and rejected: a testing framework is,
79
+ # by definition, called almost exclusively from test code, so that signal
80
+ # never fires (0/175 measured non-test callers of override_settings on
81
+ # the django corpus) even though the file plainly IS shipped API surface.
82
+ # The signal that DOES discriminate cleanly is corpus-wide unambiguous
83
+ # caller BREADTH: symbol_graph.file_fan_in() counts, per file, the number
84
+ # of DISTINCT calling files across edges whose target symbol name
85
+ # resolves to exactly one defining file corpus-wide (excludes common
86
+ # same-leaf collisions like __init__/setUp that would otherwise dilute
87
+ # the count with unrelated files). Measured on the django corpus:
88
+ # django/test/utils.py=285, django/test/testcases.py=518,
89
+ # django/test/client.py=85, vs. the next-highest genuinely-disposable
90
+ # test file at 42 and a median near 0 — a wide, well-separated gap.
91
+ # A file at or above this threshold is exempted from the
92
+ # test_deprioritised multiplier above regardless of its path.
93
+ test_framework_fan_in_threshold: 60
94
+
95
+ # substantive documentation prose vs implementation code
96
+ doc_prose: 0.70
97
+
98
+ # bodies with very few meaningful lines
99
+ short_penalty: 0.80
100
+
101
+ # A private/internal symbol (single leading underscore, e.g. `_helper`,
102
+ # not a dunder like `__init__`) is a mild demotion vs. its public-API
103
+ # sibling. This is a language-general naming convention (Python's PEP 8
104
+ # "internal use" prefix; the same single-underscore-for-internal idiom
105
+ # appears in Go, JS/TS, and Rust codebases too), not a corpus- or query-
106
+ # specific rule, so it applies uniformly regardless of query text.
107
+ # Root cause it addresses: a keyword-dense private helper's raw text can
108
+ # score higher on cross-encoder + BM25 similarity than the public method
109
+ # a natural-language query is actually asking about, because the helper's
110
+ # body happens to restate more of the query's vocabulary literally (e.g.
111
+ # a queryset-filtering helper's body mentions "queryset"/"filter" several
112
+ # times while the public method it supports has a terse one-line body).
113
+ # The importance/purpose priors (ranking.importance_prior, ranking.
114
+ # purpose_rank) already favor the public symbol's file/class, but a
115
+ # sizeable base-rerank gap from the raw-text-similarity effect above can
116
+ # still outweigh them; this prior corrects the imbalance at its source
117
+ # (django/db/models/query.py QuerySet.filter vs. the private
118
+ # _filter_prefetch_queryset helper in fields/related_descriptors.py is
119
+ # the measured witness case, UPG-16.1/F30). Mild (comparable to
120
+ # short_penalty) because a private helper is still legitimate,
121
+ # sometimes-wanted code — just usually not the intended answer to a
122
+ # concept-level query naming the public operation.
123
+ private_symbol_deprioritised: 0.80
124
+
125
+ # Maximum number of attribute-assignment body lines for an attribute-only
126
+ # class body to be classified as trivial by is_trivial_chunk() (UPG-15.9 / F25).
127
+ # A ``class Meta:`` inner stub with ≤ this many attribute-assignment body lines
128
+ # and NO method definitions / control flow is trivial — it is a configuration
129
+ # stub, not a real class definition. Django test files have 200+ such 3-line
130
+ # stubs (class Meta: / model = X / fields = '__all__') that flood doc-intent
131
+ # queries. Real small library classes with more attributes or any method body
132
+ # are unaffected. Conservative default 3: catches the dominant Meta stub
133
+ # pattern without risk of demoting real dataclass-style classes.
134
+ trivial_attr_class_max_attrs: 3
135
+
136
+ importance_prior:
137
+ # ARCH-1b: file-level PageRank importance (computed at index time by ARCH-1a,
138
+ # stored in the symbol_importance table) is blended into the FINAL search sort
139
+ # as a relevance-gated multiplicative prior:
140
+ # final_score = base_rerank_score * quality_score * (1 + lambda * importance)
141
+ # where importance ∈ [0,1] is the normalized file-level PageRank of the chunk's
142
+ # file (0 when unknown). The multiply by base_rerank_score keeps it relevance-
143
+ # gated — an irrelevant high-importance file (base ≈ 0) is not lifted. A global
144
+ # standing prior (canonical = most-referenced), NOT a query-keyword heuristic.
145
+ # lambda=0 disables it (behaviour identical to pre-ARCH-1b). Conservative default
146
+ # 0.25: reorders near-ties toward canonical files without overriding clear
147
+ # relevance. Live F22/F23 tuning is tracked in tasks.md ARCH-1b.
148
+ lambda: 0.25
149
+
150
+ class_importance:
151
+ # ARCH-2: class/symbol-name "canonical = most-referenced" importance
152
+ # (whole-word reference frequency of the name across the corpus, computed
153
+ # at index time by SymbolGraph._compute_and_store_class_importance and
154
+ # stored in the class_importance table) is blended into the FINAL search
155
+ # sort as a second relevance-gated multiplicative prior, alongside
156
+ # ARCH-1b's file-level prior:
157
+ # final_score = base_rerank_score * quality_score
158
+ # * (1 + lambda_file * file_importance)
159
+ # * (1 + lambda_class * class_importance)
160
+ # where class_importance ∈ [0,1] is the normalized reference frequency of
161
+ # the relevant name. This is the lever for same-leaf collisions (two
162
+ # unrelated classes that both define a method with the same name; a
163
+ # canonical type's own definition chunk crowded by a same-file, narrower
164
+ # sibling type; a corpus-central module-level function crowded by a
165
+ # rarely-referenced sibling function — UPG-SIBLING-TYPEDEF-CROWDING) that
166
+ # file-level importance cannot discriminate — a symbol's own body often IS
167
+ # the file, so file-level PageRank has no separating signal.
168
+ #
169
+ # UPG-SIBLING-TYPEDEF-CROWDING extended attribution beyond methods: a
170
+ # METHOD chunk still uses its owning class's name (unchanged); a
171
+ # TYPE-DEFINITION chunk (struct/enum/trait/class/interface/typedef) or a
172
+ # MODULE-LEVEL FUNCTION chunk is instead attributed to its OWN name —
173
+ # previously stuck at class_importance=0 forever since neither has an
174
+ # owning-class context to key off. See agent/searcher.py
175
+ # (is_type_definition_chunk / is_module_level_function_chunk gated
176
+ # lookup).
177
+ #
178
+ # lambda=0 disables it (behaviour identical to pre-ARCH-2). Conservative
179
+ # default 0.25, matching ARCH-1b's default: reorders same-leaf look-alikes
180
+ # toward the canonical symbol without overriding clear relevance — the
181
+ # multiplicative gate bounds the factor to [1, 1+lambda] regardless of how
182
+ # skewed the importance map is, so a modest lambda here cannot let even a
183
+ # maximum-importance rival overturn a clear cross-encoder relevance lead.
184
+ # Live F22/F23 tuning is tracked in tasks.md ARCH-2.
185
+ lambda: 0.25
186
+
187
+ purpose_rank:
188
+ # ARCH-4b: carries the ARCH-4 dual-vector purpose similarity (STEP-0/ARCH-4
189
+ # gated it into POOL ENTRY only) into the FINAL search sort as a third
190
+ # relevance-gated multiplicative prior, alongside ARCH-1b's file-level and
191
+ # ARCH-2's class-level priors:
192
+ # final_score = base_rerank_score * quality_score
193
+ # * (1 + lambda_file * file_importance)
194
+ # * (1 + lambda_class * class_importance)
195
+ # * (1 + lambda_purpose * purpose_sim)
196
+ # where purpose_sim ∈ [0,1] is the chunk's own body-vs-purpose-vector cosine
197
+ # similarity to the query (0 when the chunk has no purpose vector, e.g. a
198
+ # doc/config chunk, or dual_vector is disabled). Without this, a symbol whose
199
+ # signature+docstring is the single best purpose match in the corpus can still
200
+ # be buried under its own subclasses/near-duplicates in the final MCP ranking,
201
+ # because the cross-encoder rerank that dominates base_rerank_score only sees
202
+ # BODY text and has no visibility into the purpose signal that got the chunk
203
+ # into the pool in the first place. lambda=0 disables it (behaviour identical
204
+ # to pre-ARCH-4b). Default 0.9 (raised from ARCH-4b's original 0.5 by
205
+ # UPG-PREFIX-COMPOSE, see below), higher than ARCH-1b/ARCH-2's 0.25: those
206
+ # two priors are typically fed near-binary importance scores (0 or ~1), so a
207
+ # one-rank near-tie gap clears at 0.25; purpose_sim is a continuous cosine
208
+ # similarity that rarely reaches 1.0 even for a strong match and must also
209
+ # recover deeper rerank-pool burial (observed: purpose-rank-1 chunk landing
210
+ # at final rank 16 in a ~60-candidate pool) — algebra on the multiplicative
211
+ # gate shows 0.25 only clears a 1-of-10-rank gap, not a 15-of-60-rank gap.
212
+ # See TestPurposeRankPrior in tests/test_indexer_searcher.py for the worked
213
+ # cases.
214
+ #
215
+ # UPG-PREFIX-COMPOSE: purpose_sim measures textual similarity between the
216
+ # query and a chunk's own qualified-name+docstring independent of the
217
+ # chunk's quality tier, so a quality-demoted chunk (a test helper whose
218
+ # docstring happens to restate the query in plain language) can carry a
219
+ # HIGHER purpose_sim than the canonical production symbol it name-echoes —
220
+ # letting a shallow textual echo outrank the real implementation no matter
221
+ # how large lambda is scaled, since raising lambda amplifies the demoted
222
+ # chunk's edge by the same factor (a case witnessed on the django corpus:
223
+ # a test-only helper class's docstring out-scored the production Signal
224
+ # class's own docstring on raw cosine similarity). Raising this lambda
225
+ # alone (against the un-weighted prior) to clear that gap also
226
+ # disproportionately boosts every OTHER demoted-but-purpose-matching chunk
227
+ # against non-symbol content that has no purpose vector at all (prose/doc
228
+ # chunks default purpose_sim to 0 — see above), which was observed to
229
+ # invert an unrelated doc-vs-test-code ranking on the same corpus. The
230
+ # searcher now weights this factor by the chunk's own quality_score
231
+ # (1 + lambda * purpose_sim * quality_score, in
232
+ # agent/searcher.py._apply_quality_and_dedup) so a demoted chunk's textual
233
+ # echo can no longer buy back more rank than an undemoted chunk's smaller
234
+ # one, while an undemoted (quality=1.0) candidate keeps the full boost.
235
+ # With that weighting, lambda=0.9 (paired with test_deprioritised=0.46
236
+ # above) is stable across a lambda range of roughly [0.7, 1.1] on the
237
+ # verified case above — not a knife-edge value — and the full django
238
+ # offline acceptance replay (benchmarks/acceptance/product_cases.jsonl)
239
+ # shows zero other case flipped by this change.
240
+ lambda: 0.9
241
+
242
+ type_def_prior:
243
+ # UPG-RUST-DEF-EVICTION / DEF-B: a fourth relevance-gated multiplicative
244
+ # prior, same shape as file_importance/class_importance/purpose_rank
245
+ # above:
246
+ # final_score = base_rerank_score * quality_score
247
+ # * (1 + lambda_file * file_importance)
248
+ # * (1 + lambda_class * class_importance)
249
+ # * (1 + lambda_purpose * purpose_sim * quality_score)
250
+ # * (1 + lambda_def * is_type_definition)
251
+ # where is_type_definition is 1 when the chunk's own node_type is a TYPE
252
+ # DEFINITION (struct/enum/trait/class/interface — see
253
+ # chunk_quality.is_type_definition_chunk), else 0. Cross-language witness:
254
+ # a struct/type's canonical definition chunk repeatedly loses to same-name
255
+ # test code and usage sites at comparable rerank scores because the
256
+ # cross-encoder has no visibility into "this chunk IS the definition" —
257
+ # only the AST does. lambda=0 disables it (pre-DEF-B behaviour). Default
258
+ # 0.15, smaller than class_importance/file_importance's 0.25: this prior
259
+ # fires on every type-def chunk unconditionally (not gated by corpus-wide
260
+ # reference frequency), so a larger lambda would systematically favour
261
+ # definitions over well-matched usage sites even when the usage IS the
262
+ # better answer to the query.
263
+ lambda: 0.15
264
+
265
+ docstring_dedup:
266
+ # UPG-RUST-DEF-EVICTION / DEF-C: a second, independent dedup key
267
+ # (alongside the existing byte-normalized full-content key, UPG-2.2)
268
+ # keyed on a chunk's normalized LEADING docstring/comment block only.
269
+ # Near-duplicate boilerplate headers (the same rustdoc/JSDoc/docstring
270
+ # copy-pasted across several thin wrapper/test chunks with otherwise
271
+ # different bodies) evade the full-content key but still bury the
272
+ # canonical definition under several near-identical-looking results.
273
+ # `lines` bounds how many leading doc lines are compared (matches the
274
+ # dual-vector purpose-text docstring cap's spirit — a header is a header,
275
+ # extra lines rarely add distinguishing signal). `min_chars` is a floor
276
+ # on the normalized header's length below which a chunk is NEVER folded
277
+ # into this key (chunks with a trivial/near-empty leading header, or none
278
+ # at all, keep every occurrence — collapsing on near-nothing would fold
279
+ # together unrelated chunks that merely share a blank or 1-word header).
280
+ lines: 3
281
+ min_chars: 20
282
+
283
+ locate_ranking:
284
+ # UPG-15.10: scope-depth + line-span signals for locate ranking.
285
+ # These penalise class/function definitions that are buried inside function
286
+ # bodies (inner test-scope defs) and reward large, substantive definitions.
287
+
288
+ # Line-span threshold (in lines) above which a symbol definition is considered
289
+ # "large" — typically a canonical library class or function. Symbols with
290
+ # end_line - start_line >= this value get the best span bucket (0).
291
+ # Django's Model base class has ~1400 lines; real functions are ≥10 lines.
292
+ # Test-inner stubs are typically 2–5 lines.
293
+ large_span_threshold: 50
294
+
295
+ # Line-span threshold below which a symbol definition is considered "tiny" —
296
+ # a stub or inner test class. Symbols with span < this value get the worst
297
+ # span bucket (2); spans in [small, large) get bucket 1.
298
+ small_span_threshold: 10
299
+
300
+ rerank:
301
+ # Cross-encoder reranker model (HuggingFace id). The VECTR_RERANKER_MODEL
302
+ # env var overrides this key (test fixtures rely on that); an empty value
303
+ # disables reranking entirely. Swapping this model needs no reindex —
304
+ # the reranker scores (query, chunk) pairs at query time only.
305
+ model: "BAAI/bge-reranker-base"
306
+
307
+ # rerank this many hybrid candidates before trimming to n_results
308
+ top_k: 40
309
+
310
+ # When no language filter is set, doc prose dominates the shallow hybrid pool and
311
+ # real implementation chunks fall outside it (audit: extractor.py / chunk_file never
312
+ # fetched). Fetch a deeper pool in that case so the quality prior has code to surface.
313
+ top_k_unfiltered: 60
314
+
315
+ # Over-fetch depth for the pool-entry trivial filter (UPG-15.7).
316
+ # Before trimming to top_k_unfiltered, the hybrid retrieval fetches this many raw
317
+ # candidates, drops any that are trivial (is_trivial_chunk == True) and not
318
+ # forced-inclusion candidates, THEN trims to top_k_unfiltered. This ensures the
319
+ # rerank pool of ~60 is filled with real code even on fixture-heavy repos
320
+ # (a large web codebase can carry a hundred-plus 1–2-line HTML/TXT template
321
+ # chunks that otherwise crowd out code).
322
+ # Rule: pre_filter_fetch_k must be >= top_k_unfiltered. ~200 covers such
323
+ # fixture blocks while remaining a tiny fraction of a 40k-chunk corpus.
324
+ # The cross-encoder reranker only sees ~top_k_unfiltered (~60) candidates, so latency
325
+ # is unaffected by this wider initial fetch.
326
+ pre_filter_fetch_k: 200
327
+
328
+ notfound_floor:
329
+ # UPG-NOTFOUND-FLOOR (F46): the score shown to the caller is the FINAL
330
+ # rank-based composite (searcher._apply_quality_and_dedup: base = 1 -
331
+ # rank/n, then quality/importance multipliers) — it is re-derived from
332
+ # each query's own candidate ORDER, so the top result of any query always
333
+ # displays close to 1.0 even when every candidate is irrelevant. A signal
334
+ # independent of that per-query rescaling is needed to tell the caller
335
+ # the whole result set may be a guess.
336
+ #
337
+ # UPG-NOTFOUND-FLOOR-2: the first version of this signal gated on the raw
338
+ # pre-rerank vector-search cosine similarity (the one score in the
339
+ # pipeline NOT re-normalized per query) against an absolute constant
340
+ # (dense_score_floor). Measured against the production embedder on a
341
+ # large real corpus, that constant never fires: an absent-topic query's
342
+ # best pool cosine and a genuinely on-topic query's best pool cosine
343
+ # overlap the same ~0.60-0.86 band (one nonsense string measured HIGHER
344
+ # than several real on-topic queries) — every sentence embedding this
345
+ # model produces lands close to the corpus centroid regardless of
346
+ # relevance, so no absolute cosine constant can separate the two
347
+ # classes. (One parenthetical data point from the django benchmark
348
+ # corpus: the query "CORS handling implementation" — a protocol
349
+ # Django's own source never touches — still measured a pool-max cosine
350
+ # of 0.75, comfortably inside the on-topic band.) The defect was the
351
+ # signal, not the threshold.
352
+ #
353
+ # The replacement is distributional rather than absolute: does every
354
+ # content word in the query occur, even once, anywhere in the corpus's
355
+ # indexed vocabulary? A query naming a concept the codebase genuinely
356
+ # does not contain is built from at least one word that has literally
357
+ # zero document frequency across the whole corpus (not just the
358
+ # query's fetched candidate pool); a query about something the
359
+ # codebase does contain, however badly it is then ranked, is built
360
+ # entirely from words that really do appear in it. This generalizes
361
+ # past absolute-cosine floors to any embedder vectr ships, because it
362
+ # never looks at a cosine value at all — only at whether the corpus's
363
+ # own vocabulary contains the query's words.
364
+ enabled: true
365
+
366
+ # A query token shorter than this is excluded from the zero-document-
367
+ # frequency check — very short tokens (2-letter abbreviations, stray
368
+ # punctuation fragments) are too ambiguous to serve as a reliable
369
+ # vocabulary anchor in either direction.
370
+ min_content_token_length: 3
371
+
372
+ # Generic English/query-scaffolding words excluded from the zero-
373
+ # document-frequency check. These are near-universal across any real
374
+ # codebase's comments and docstrings (so they would essentially never
375
+ # have zero document frequency) — excluding them only avoids wasted
376
+ # per-query token lookups, it does not change which queries get
377
+ # flagged.
378
+ stopwords:
379
+ - the
380
+ - a
381
+ - an
382
+ - of
383
+ - to
384
+ - in
385
+ # Quoted: bare "on"/"off"/"yes"/"no" are YAML 1.1 boolean literals — an
386
+ # unquoted "on" here is parsed as the Python bool True, not the string
387
+ # "on", corrupting this list (agent/config.py loads with pyyaml's
388
+ # default YAML 1.1 resolver).
389
+ - "on"
390
+ - for
391
+ - and
392
+ - or
393
+ - is
394
+ - are
395
+ - how
396
+ - what
397
+ - does
398
+ - do
399
+ - implementation
400
+ - code
401
+ - framework
402
+ - using
403
+ - use
404
+
405
+ # Minimum number of query content tokens with zero corpus-wide document
406
+ # frequency required to flag the result set low_confidence. 1 (default)
407
+ # flags as soon as a single content word never appears anywhere in the
408
+ # indexed corpus. Set arbitrarily high (or set enabled: false) to
409
+ # disable — the floor never fires and low_confidence is always false,
410
+ # i.e. an exact no-op restoring pre-UPG-NOTFOUND-FLOOR behaviour.
411
+ min_zero_df_tokens: 1
412
+
413
+ # UPG-SCORE-DISPLAY-FLAT: a second, independent low-confidence sub-signal —
414
+ # the top result's own displayed relevance is below this absolute floor.
415
+ # Gated on the cross-encoder relevance score specifically (ce_relevance),
416
+ # never on the raw dense-cosine fallback: the UPG-NOTFOUND-FLOOR-2
417
+ # measurement above already showed a bi-encoder cosine cannot separate
418
+ # absent-topic from on-topic queries, so flooring on it here would
419
+ # reintroduce that same defect. A cross-encoder score is a different,
420
+ # calibrated relevance judgment (trained to predict query-doc relevance
421
+ # directly), so it is safe to floor on. A no-op (this sub-signal never
422
+ # fires) whenever reranking didn't run (rerank=False, or the reranker
423
+ # model failed to load) — low_confidence then falls back to the
424
+ # zero-document-frequency check above alone.
425
+ min_top_relevance: 0.30
426
+
427
+ # Text prepended to the MCP vectr_search response when the floor fires.
428
+ # Results are still shown in full below it — the caller LLM may still
429
+ # salvage a lead; this only adds a signal it currently has no way to see.
430
+ banner: "No strong match for this query — the results below scored low on absolute relevance and may be unrelated. Try vectr_locate with the exact symbol name, rephrase the query, or fall back to grep/reading the file directly."
431
+
432
+ # UPG-CLI-SEARCH-FLOOR: separate CLI-surface banner, printed by `vectr
433
+ # search` when SearchResponse.low_confidence is true. The MCP banner
434
+ # above names `vectr_locate` — meaningless at a shell prompt, since no
435
+ # `vectr locate` subcommand exists. Same signal, phrasing for a human
436
+ # terminal reader instead of the editor's LLM.
437
+ banner_cli: "No strong match for this query — the results below scored low on absolute relevance and may be unrelated. Try an exact symbol name, rephrase the query, or fall back to grep/reading the file directly."
438
+
439
+ strategy:
440
+ # Fallback hybrid-search weights used before the first index-time codebase
441
+ # fingerprint has run (agent.strategy_selector.select_strategy). Once a
442
+ # workspace is indexed these are superseded by the fingerprint-derived
443
+ # RetrievalStrategy; until then `status` and `search` use this pair so
444
+ # retrieval behaviour — and its reporting — is deterministic from the very
445
+ # first call rather than silently absent (UPG-8.2). Must sum to 1.0.
446
+ default_semantic_weight: 0.70
447
+ default_bm25_weight: 0.30
448
+ retrieval:
449
+ dual_vector:
450
+ # ARCH-4: per-symbol dual-vector pool entry. STEP-0 spike proved a mechanical
451
+ # implementation body dilutes the intent-bearing signature/docstring tokens
452
+ # when mean-pooled into a single embedding — a documented method can miss
453
+ # dense pool entry entirely even though its own docstring paraphrases the
454
+ # query. Fix: embed a second, body-stripped "purpose" text (qualified
455
+ # signature + docstring/leading comment) per symbol-bearing chunk and query
456
+ # both vector spaces; the two are merged uniformly for every query, never
457
+ # gated on query content. Non-symbol chunks (docs/markdown/config) are
458
+ # unaffected — no purpose vector is stored for them.
459
+ enabled: true
460
+
461
+ # How a chunk's two similarity scores (body, purpose) combine into the one
462
+ # dense score consumed by pool entry and the hybrid merge:
463
+ # "max" — dense_score = max(body_sim, purpose_sim). A chunk enters the
464
+ # pool if EITHER vector matches; never scores worse than a
465
+ # body-only search would. This is the evidence-gated choice:
466
+ # the spike showed purpose_sim clears pool-entry threshold
467
+ # while body_sim does not (0.601 vs 0.706 for one phrasing),
468
+ # so averaging the two would still leave the chunk short —
469
+ # only a non-averaging max recovers pool entry.
470
+ # "weighted" — dense_score = (1 - blend_weight) * body_sim + blend_weight *
471
+ # purpose_sim. Kept as an evaluated alternative for future
472
+ # tuning; not the default because averaging re-introduces the
473
+ # dilution the mechanism exists to defeat.
474
+ blend_mode: "max"
475
+
476
+ # Purpose-vector weight when blend_mode is "weighted" (ignored under "max").
477
+ blend_weight: 0.5
478
+
479
+ # Maximum lines captured as a chunk's "signature" when distilling the
480
+ # purpose text — the declaration line(s) up to (and including) the line that
481
+ # opens the body (python ':' / brace-block '{'). Bounds pathologically long
482
+ # multi-line parameter lists.
483
+ max_signature_lines: 8
484
+
485
+ # Maximum lines captured from a Python docstring (or a non-Python leading
486
+ # doc-comment block) when building the purpose text.
487
+ max_docstring_lines: 12
488
+
489
+ # Maximum characters kept from the captured docstring/leading-comment text —
490
+ # a hard cap so a very long module-style docstring doesn't itself dilute the
491
+ # purpose embedding the same way a long body would.
492
+ max_docstring_chars: 600
493
+
494
+ search:
495
+ identifier_hint:
496
+ # UPG-QUERYTYPE-REROUTE: additive, high-precision symbol-graph hint
497
+ # appended below vectr_search's L3 results. Replaces the deleted
498
+ # agent/query_router.py regex query-classification layer, which
499
+ # misrouted innocent NL questions containing bare nouns like
500
+ # "dependency"/"override"/"subclass" into a CALL_GRAPH intent — silently
501
+ # overriding the fingerprint-derived semantic weight and guessing a
502
+ # same-named homonym via the query's first non-stopword word, with no
503
+ # ranking tie to the query at all. This mechanism does no intent
504
+ # classification: it tokenizes the RAW query for identifier-SHAPED
505
+ # tokens (CamelCase, snake_case, dotted Class.method — a structural
506
+ # transform, never a keyword/phrase match) and appends a section only
507
+ # when at least one token resolves to an EXACT symbol-graph hit (no
508
+ # fuzzy/prefix/substring). A query with no identifier-shaped token, or
509
+ # one whose plain words merely coincide with a symbol name, gets no
510
+ # section and no weight override.
511
+ enabled: true
512
+
513
+ # Maximum number of distinct identifier-shaped tokens (query order,
514
+ # deduplicated) attempted against the symbol graph per search.
515
+ max_identifiers: 2
516
+
517
+ # Maximum resolved symbol locations shown per identifier.
518
+ max_locations: 3
519
+
520
+ # UPG-NEARMISS-SYMBOL-NAMES: when an identifier-shaped token fails EXACT
521
+ # resolution, additively surface the nearest existing symbol NAMES so the
522
+ # caller LLM learns it misremembered a name rather than seeing nothing.
523
+ # Sourced entirely from the symbol graph's own deterministic partial-match
524
+ # machinery (locate_l2's non-exact strategies, plus one bounded prefix-
525
+ # containment check — see SymbolGraph.nearest_symbol_names) — never a
526
+ # semantic guess, and always labeled inexact in the rendered section.
527
+ nearmiss_enabled: true
528
+
529
+ # Maximum distinct near-miss symbol names shown, TOTAL across the whole
530
+ # response (not per token) — keeps the addition small even when more
531
+ # than one identifier-shaped token in the query fails to resolve exactly.
532
+ nearmiss_max: 3
533
+
534
+ # Minimum length (characters) an existing symbol's name must have to be
535
+ # offered as a prefix-containment near-miss candidate (the token "starts
536
+ # with" that shorter, real symbol name). Prevents a short, generic symbol
537
+ # name (e.g. a 2-3 char helper) from spuriously "matching" as a prefix of
538
+ # many unrelated tokens.
539
+ nearmiss_min_prefix_len: 6
540
+
541
+ indexing:
542
+ # hard cap — prevents single huge chunks diluting embeddings
543
+ max_chunk_lines: 150
544
+
545
+ # lines kept for class-level chunk (sig + docstring + attrs)
546
+ class_header_lines: 40
547
+
548
+ # Build-artifact directories excluded from indexing at file-walk time (UPG-15.9).
549
+ # Any path component that ENDS WITH one of these suffixes is treated as a build
550
+ # artifact directory and every file inside it is excluded. This catches Python
551
+ # egg-info directories (e.g. Django.egg-info/, mypackage.egg-info/) whose
552
+ # SOURCES.txt / PKG-INFO / *.txt files are plain file-path listings with no
553
+ # educational content but high BM25 IDF on module/command identifiers.
554
+ # Rule: suffix match on each path part — "Django.egg-info" ends with ".egg-info".
555
+ # Do NOT widen to cover real documentation dirs (docs/, howto/) — only build
556
+ # artifacts whose entire directory is machine-generated have no index value.
557
+ build_artifact_dir_suffixes:
558
+ - ".egg-info"
559
+ - ".dist-info"
560
+
561
+ # UPG-JSFLOW-SYMBOLS: Flow-typed `.js` detection. tree-sitter-javascript
562
+ # treats Flow type syntax (`@flow` pragma, `import type {...}`, `: Type`
563
+ # annotations, generics) as ERROR nodes and desyncs the symbol walk —
564
+ # canonical functions go missing and keyword tokens get misattributed as
565
+ # symbol names. Files that signal Flow are parsed with the typescript/tsx
566
+ # grammar instead (which understands type annotations); plain `.js` is
567
+ # unaffected. Detection is a cheap header scan (first `scan_head_bytes`
568
+ # bytes), done once per file parse — never per AST node.
569
+ flow_detection:
570
+ # Bytes scanned from the start of the file when looking for a Flow signal.
571
+ scan_head_bytes: 2000
572
+ # Primary signal: the `@flow` pragma, conventionally in the file's leading comment.
573
+ pragma: "@flow"
574
+ # Secondary signal: Flow-only import syntax, checked within the same head window.
575
+ secondary_markers:
576
+ - "import type "
577
+ - "export type "
578
+
579
+ output:
580
+ # Number of lines returned as a snippet with each symbol location.
581
+ # Enough for the AI to understand signature + first few lines of the body.
582
+ snippet_lines: 12
583
+
584
+ behavior:
585
+ remember_nudge:
586
+ # After this many vectr tool calls without a vectr_remember, the next
587
+ # vectr_search / vectr_locate / vectr_trace response appends an imperative
588
+ # reminder. The counter resets on every vectr_remember call.
589
+ threshold: 10
590
+
591
+ # After the threshold fires, re-fires every this many calls so a single
592
+ # dismissal cannot silence it for the rest of the session.
593
+ cooldown: 5
594
+
595
+ eviction:
596
+ # Minimum accumulated retrieved-token estimate (since the last auto-eviction
597
+ # hint or session start) required before auto_eviction_hint() will emit the
598
+ # ACTION REQUIRED block. Gates on real context pressure rather than raw
599
+ # call count — a burst of tiny searches (7-15 line methods, ~30-60 tokens
600
+ # each) does not trigger the hint until meaningful content has been retrieved.
601
+ # Token estimate: len(content) // 4. At ~100 chars/line, 4000 tokens ≈ 10
602
+ # small methods or 2-3 medium functions. The explicit vectr_evict_hint tool
603
+ # and /v1/evict endpoint remain ungated.
604
+ retrieved_token_gate: 4000
605
+
606
+ # UPG-EVICT-SESSION-SCOPE: maximum number of chunk ids listed as re-fetch
607
+ # keys (`vectr_fetch(ids=[...])`) in the eviction_hint() render. Bounds the
608
+ # hint's own token cost — a session with hundreds of retrieved chunks still
609
+ # gets a short, actionable list rather than every id it has ever seen.
610
+ hint_max_ids: 5
611
+
612
+ # UPG-EVICT-SESSION-SCOPE: each MCP session gets its own EvictionAdvisor so
613
+ # one session never sees chunks retrieved by another. Bounds the daemon-
614
+ # wide registry of per-session advisors — oldest session is dropped (LRU)
615
+ # once this many concurrent sessions have been tracked, so a long-running
616
+ # daemon serving many short-lived sessions can't grow this without bound.
617
+ max_tracked_sessions: 50
618
+
619
+ # UPG-REMEMBER-BANNER-FATIGUE: auto_eviction_hint()'s escalated
620
+ # "ACTION REQUIRED: call vectr_remember NOW" directive only fires once
621
+ # this many NEW chunks have been retrieved since the caller's last
622
+ # vectr_remember call (or since session start, if it has never called
623
+ # vectr_remember). Right after a vectr_remember, repeating the same
624
+ # urgent directive on the very next retrieval trains the caller to
625
+ # ignore it — the counter gives it a chance to actually comply before
626
+ # escalating again. Below this count, MCP dispatch's existing
627
+ # turn-count soft nudge (behavior.remember_nudge, or nothing) takes
628
+ # over instead; see _should_nudge_remember/_remember_nudge_text.
629
+ # Set in the same order of magnitude as rearm_retrieval_calls (4) above.
630
+ remember_escalation_chunks: 3
631
+
632
+ # UPG-EVICT-ESCALATION-GATE-TOO-LOW: companion gate to remember_escalation_chunks,
633
+ # required in ADDITION to it (both must hold). remember_escalation_chunks alone is
634
+ # smaller than a single search's default n_results (5), so on large-chunk corpora
635
+ # one vectr_search call can add >4000 tokens in a single burst — tripping both the
636
+ # chunk-count gate AND the existing retrieved_token_gate at once, right after the
637
+ # caller just complied with vectr_remember. Gating on accumulated TOKENS since the
638
+ # last vectr_remember (not just since the last hint) means a single typical search
639
+ # (~4000 tokens on a large-chunk corpus, per retrieved_token_gate's own comment)
640
+ # cannot cross this threshold alone — it takes roughly two such searches, giving the
641
+ # caller a real chance to act on vectr_remember before repeat pressure builds back up.
642
+ remember_escalation_tokens: 8000
643
+
644
+ boot:
645
+ # SessionStart boot recall (UPG-9.2): unconditional directive notes are
646
+ # capped defensively so a workspace with unusually many standing rules
647
+ # doesn't inject an unbounded block every session. Directives are
648
+ # ordered oldest-first (standing rules stay in a stable order).
649
+ max_directive_notes: 100
650
+
651
+ # UPG-TASK-NOTE-INJECTION-RECENCY: kind=task notes are current-work
652
+ # state, not relevance-ranked learnings — they are ordered strictly
653
+ # newest-first (ignoring author_trust_score/decay_score) and capped to
654
+ # this many so the boot set surfaces only the freshest checkpoints, not
655
+ # the full task history.
656
+ max_task_notes: 5
657
+
658
+ ingest_traces:
659
+ # Maximum number of "caller/callee not found in the indexed symbol graph"
660
+ # example strings surfaced in the vectr_ingest_traces response text (UPG-7.3).
661
+ # The edge is still ingested — an unresolved name is common and expected
662
+ # (external library, dynamic dispatch target added at runtime, or a symbol
663
+ # kind the static extractor doesn't capture) — this only bounds how many
664
+ # examples are echoed back so a large trace batch doesn't flood the response.
665
+ max_unresolved_examples: 10
666
+
667
+ # UPG-TRACE-GRAPH-INCOMPLETE (F40-class, reproduced on vectr_trace): the MCP
668
+ # schema's required parameter is "name", but every symbol-name-bearing tool
669
+ # description has read like a positional example, training callers to guess
670
+ # a different key. Rather than rely on descriptions alone, the dispatch
671
+ # layer accepts these as drop-in aliases for "name" on vectr_locate and
672
+ # vectr_trace — a call using any of them succeeds instead of erroring.
673
+ symbol_name_param_aliases:
674
+ - symbol
675
+ - symbol_name
676
+
677
+ symbol_graph:
678
+ # UPG-JSFLOW-SYMBOLS: language keywords that must never be minted as a
679
+ # symbol name or call-edge target. A desynced parse (e.g. Flow syntax
680
+ # hitting the plain javascript grammar, or any other ERROR-node recovery)
681
+ # can misattribute a keyword token as an identifier — these sets are the
682
+ # global reject-list, checked alongside the per-node ERROR-ancestor guard.
683
+ # Grouped per language (most C-descended languages share a keyword core,
684
+ # but each set is kept explicit rather than merged so a language's grammar
685
+ # can evolve independently).
686
+ reserved_keywords:
687
+ python:
688
+ - if
689
+ - elif
690
+ - else
691
+ - for
692
+ - while
693
+ - return
694
+ - def
695
+ - class
696
+ - import
697
+ - from
698
+ - try
699
+ - except
700
+ - finally
701
+ - with
702
+ - as
703
+ - pass
704
+ - break
705
+ - continue
706
+ - lambda
707
+ - yield
708
+ - global
709
+ - nonlocal
710
+ - assert
711
+ - raise
712
+ - del
713
+ - in
714
+ - is
715
+ - not
716
+ - and
717
+ - or
718
+ - async
719
+ - await
720
+ - None
721
+ - True
722
+ - False
723
+ # Not a real keyword, but a pre-existing noise guard: `print(...)` call
724
+ # sites were already suppressed as call-edge targets before this list
725
+ # existed — kept here so the exclusion stays config-driven.
726
+ - print
727
+ javascript:
728
+ - if
729
+ - else
730
+ - for
731
+ - while
732
+ - do
733
+ - switch
734
+ - case
735
+ - default
736
+ - break
737
+ - continue
738
+ - return
739
+ - function
740
+ - class
741
+ - const
742
+ - let
743
+ - var
744
+ - new
745
+ - typeof
746
+ - instanceof
747
+ - in
748
+ - of
749
+ - void
750
+ - delete
751
+ - yield
752
+ - async
753
+ - await
754
+ - export
755
+ - import
756
+ - extends
757
+ - super
758
+ - this
759
+ - static
760
+ - get
761
+ - set
762
+ - try
763
+ - catch
764
+ - finally
765
+ - throw
766
+ typescript:
767
+ - if
768
+ - else
769
+ - for
770
+ - while
771
+ - do
772
+ - switch
773
+ - case
774
+ - default
775
+ - break
776
+ - continue
777
+ - return
778
+ - function
779
+ - class
780
+ - const
781
+ - let
782
+ - var
783
+ - new
784
+ - typeof
785
+ - instanceof
786
+ - in
787
+ - of
788
+ - void
789
+ - delete
790
+ - yield
791
+ - async
792
+ - await
793
+ - export
794
+ - import
795
+ - extends
796
+ - super
797
+ - this
798
+ - static
799
+ - get
800
+ - set
801
+ - try
802
+ - catch
803
+ - finally
804
+ - throw
805
+ - interface
806
+ - type
807
+ - enum
808
+ - implements
809
+ - namespace
810
+ - declare
811
+ - as
812
+ go:
813
+ - if
814
+ - else
815
+ - for
816
+ - switch
817
+ - case
818
+ - default
819
+ - break
820
+ - continue
821
+ - return
822
+ - func
823
+ - package
824
+ - import
825
+ - defer
826
+ - go
827
+ - select
828
+ - chan
829
+ - range
830
+ - var
831
+ - const
832
+ - type
833
+ - struct
834
+ - interface
835
+ - map
836
+ - goto
837
+ - fallthrough
838
+ rust:
839
+ - if
840
+ - else
841
+ - for
842
+ - while
843
+ - loop
844
+ - match
845
+ - break
846
+ - continue
847
+ - return
848
+ - fn
849
+ - let
850
+ - mut
851
+ - struct
852
+ - enum
853
+ - impl
854
+ - trait
855
+ - use
856
+ - mod
857
+ - pub
858
+ - self
859
+ - Self
860
+ - in
861
+ - unsafe
862
+ - async
863
+ - await
864
+ - move
865
+ - ref
866
+ - where
867
+ - dyn
868
+ - static
869
+ - const
870
+ - type
871
+ java:
872
+ - if
873
+ - else
874
+ - for
875
+ - while
876
+ - do
877
+ - switch
878
+ - case
879
+ - default
880
+ - break
881
+ - continue
882
+ - return
883
+ - class
884
+ - interface
885
+ - extends
886
+ - implements
887
+ - public
888
+ - private
889
+ - protected
890
+ - static
891
+ - void
892
+ - this
893
+ - super
894
+ - new
895
+ - try
896
+ - catch
897
+ - finally
898
+ - throw
899
+ - throws
900
+ - import
901
+ - package
902
+ - enum
903
+ zig:
904
+ - if
905
+ - else
906
+ - for
907
+ - while
908
+ - switch
909
+ - break
910
+ - continue
911
+ - return
912
+ - fn
913
+ - const
914
+ - var
915
+ - pub
916
+ - struct
917
+ - enum
918
+ - union
919
+ - defer
920
+ - errdefer
921
+ - try
922
+ - catch
923
+ - comptime
924
+ - async
925
+ - await
926
+ - orelse
927
+ c:
928
+ - if
929
+ - else
930
+ - for
931
+ - while
932
+ - do
933
+ - switch
934
+ - case
935
+ - default
936
+ - break
937
+ - continue
938
+ - return
939
+ - goto
940
+ - sizeof
941
+ - typedef
942
+ - struct
943
+ - union
944
+ - enum
945
+ - static
946
+ - const
947
+ - void
948
+ - extern
949
+ - volatile
950
+ cpp:
951
+ - if
952
+ - else
953
+ - for
954
+ - while
955
+ - do
956
+ - switch
957
+ - case
958
+ - default
959
+ - break
960
+ - continue
961
+ - return
962
+ - goto
963
+ - sizeof
964
+ - typedef
965
+ - struct
966
+ - union
967
+ - enum
968
+ - class
969
+ - namespace
970
+ - template
971
+ - static
972
+ - const
973
+ - void
974
+ - new
975
+ - delete
976
+ - try
977
+ - catch
978
+ - throw
979
+ - public
980
+ - private
981
+ - protected
982
+ - virtual
983
+ - using
984
+
985
+ # UPG-REACT-TSX-FUNCTION-DECL-DROP: recovery for a grammar desync that
986
+ # swallows an unrelated region of siblings into one opaque, mis-typed node
987
+ # (e.g. a single unparseable construct hundreds of lines earlier can wreck
988
+ # the parser's recovery for the rest of the file). Language-agnostic — any
989
+ # tree-sitter grammar's error-recovery path can produce this shape, not
990
+ # just Flow/tsx. When the walker hits a node that is not itself a
991
+ # recognized symbol/call construct but has_error is set and its own span
992
+ # is large, it is worth an isolated reparse of just that byte range: the
993
+ # parser often resyncs cleanly once it starts fresh without the earlier
994
+ # desync's carried-over state.
995
+ error_recovery:
996
+ # Minimum line-span (end_line - start_line) an opaque errored node must
997
+ # cover before an isolated reparse is attempted. A short span IS the
998
+ # error (no swallowed siblings to recover) — reparsing it wastes a parse
999
+ # pass for nothing.
1000
+ min_span_lines: 8
1001
+ # Reparse attempts budget per file. Bounds worst-case parse cost when a
1002
+ # file has many/deeply-nested desynced regions.
1003
+ max_reparse_attempts_per_file: 200
1004
+ # Per-attempt cap on how many original siblings a single recovery reparse
1005
+ # may absorb while growing past an arbitrary mid-declaration cut (see
1006
+ # error_recovery above). Without its own cap, one badly-cut region can
1007
+ # burn the entire per-file budget growing sibling-by-sibling and starve
1008
+ # every other desynced region in the same file.
1009
+ max_extend_steps_per_attempt: 20
1010
+
1011
+ cli:
1012
+ # UPG-CLI-START-READY-RACE: `vectr start`/`restart` spawn the daemon as a
1013
+ # detached background process and used to print a success block
1014
+ # immediately after Popen() returned — before the daemon had bound its
1015
+ # port or finished loading the embedder in its FastAPI lifespan. A
1016
+ # `vectr status` run right after start's "success" message could still see
1017
+ # a connection refused, reading as "start lied." `_do_start` now polls
1018
+ # /v1/status until the daemon actually answers (or this timeout elapses)
1019
+ # before printing anything, so the message reflects reality.
1020
+ start_ready_poll_timeout_s: 20.0
1021
+ # How often to retry the readiness probe while waiting (above).
1022
+ start_ready_poll_interval_s: 0.3
1023
+ # Per-attempt HTTP timeout for a single readiness probe during the poll
1024
+ # loop above — short, since a probe that hangs this long just means "not
1025
+ # ready yet," and we want to retry rather than block on one slow attempt.
1026
+ start_ready_probe_timeout_s: 1.0
1027
+ # UPG-CLI-DAEMON-VERSION-SKEW: HTTP timeout for the standalone /v1/status
1028
+ # probe a daemon-talking subcommand issues to compare its local version
1029
+ # stamp against the running daemon's. Short and best-effort — a slow/failed
1030
+ # probe just means the check is skipped this invocation, never a blocked
1031
+ # command.
1032
+ version_skew_probe_timeout_s: 1.0
1033
+
1034
+ workspace:
1035
+ # Directory names written into a FRESH .vectrignore on first `vectr start` /
1036
+ # `vectr init` when the workspace has none yet (UPG-13.2). An existing
1037
+ # .vectrignore is never overwritten. These mirror the built-in indexing
1038
+ # excludes (agent.indexer.EXCLUDED_DIRS) plus common ecosystem cache/build
1039
+ # dirs, so a new user sees sensible excludes without hand-authoring them.
1040
+ default_vectrignore_dirs:
1041
+ - node_modules
1042
+ - .venv
1043
+ - venv
1044
+ - env
1045
+ - __pycache__
1046
+ - .git
1047
+ - dist
1048
+ - build
1049
+ - target
1050
+ - .mypy_cache
1051
+ - .pytest_cache
1052
+ - .ruff_cache
1053
+ - htmlcov
1054
+ - coverage
1055
+ - .tox
1056
+ - .cache
1057
+ - tmp
1058
+ - vendor
1059
+ - .next
1060
+ - .nuxt
1061
+ - out
1062
+
1063
+ watcher:
1064
+ # How often (seconds) CodeWatcher shallow-scans each workspace root's
1065
+ # TOP-LEVEL entries only (UPG-13.1/13.3). The watcher never schedules a
1066
+ # native OS watch on the workspace root itself — on macOS, FSEvents
1067
+ # delivers every event under a watched path's *entire* subtree to the
1068
+ # Python emitter regardless of the `recursive` flag passed to watchdog, so
1069
+ # watching the root non-recursively gives zero protection against a large
1070
+ # excluded sibling directory (confirmed empirically: churn inside an
1071
+ # unwatched excluded dir produces zero native events; churn inside the
1072
+ # same dir under a "non-recursive" root watch delivers 100% of events).
1073
+ # Only per-included-top-level-directory watches actually stop the flood.
1074
+ # This interval is how quickly a brand-new top-level directory, or an edit
1075
+ # to a top-level loose file, is picked up instead — the shallow scan stats
1076
+ # only the root's direct children, never the whole tree, so it stays cheap
1077
+ # regardless of repo size.
1078
+ top_level_rescan_interval_s: 5.0
1079
+
1080
+ # UPG-WATCHER-PRESSURE-GOVERNOR: burst coalescing. A sustained multi-file
1081
+ # edit stream (AI coding agents editing many files in quick succession) hits
1082
+ # the per-file debounce independently for every path, so the embed pipeline
1083
+ # never goes idle — no global ceiling on concurrent re-embed work. When more
1084
+ # than this many distinct paths are pending debounce SIMULTANEOUSLY, every
1085
+ # per-file timer is cancelled and every pending path collapses into one
1086
+ # deferred batch re-index instead — an edit storm then costs one batch pass
1087
+ # over N files, not N independent re-embeds.
1088
+ burst_files_threshold: 8
1089
+ # Seconds of repo-wide silence (no new watched-file event) required, once
1090
+ # burst coalescing has kicked in, before the collapsed batch actually runs.
1091
+ # Keeps the batch from firing mid-storm and having to run again seconds
1092
+ # later for the next wave of edits from the same agent turn.
1093
+ burst_quiet_seconds: 15.0
1094
+ # Optional self-limit (MB) on this process's own resident memory before a
1095
+ # watcher-triggered batch re-index is allowed to run; 0 disables the check
1096
+ # (default — the watcher never inspects memory unless a laptop operator
1097
+ # opts in). When set and exceeded, the batch is deferred to the next quiet
1098
+ # window and ONE warning line is logged instead of running — the collected
1099
+ # paths are never dropped, just retried later. Reads this process's peak
1100
+ # resident set size (stdlib `resource.getrusage`, no extra dependency);
1101
+ # peak RSS is monotonically non-decreasing for the life of the process, so
1102
+ # once tripped this acts as a standing "this process has run hot before,
1103
+ # stop feeding it more embed work" brake rather than an instantaneous
1104
+ # reading — deliberately conservative for a resource-safety guard.
1105
+ max_rss_mb: 0
1106
+
1107
+ hooks:
1108
+ # UPG-HOOK-INJECT-OBSERVABILITY: when true, every hook-driven recall that
1109
+ # actually injects notes (SessionStart/UserPromptSubmit/PreToolUse) appends
1110
+ # one line to ~/.vectr/logs/<workspace-hash>.hooks.log — timestamp, hook
1111
+ # kind, and an approximate token count of what was injected. Off by
1112
+ # default: the per-hook-kind counters in `vectr status` already answer the
1113
+ # common "is this actually firing" question without writing to disk; turn
1114
+ # this on only when auditing exact injection timing/volume.
1115
+ log_injections: false
1116
+
1117
+ # Divisor used to turn the injected notes' character count into an
1118
+ # approximate token count for the optional injection log above. 4 is the
1119
+ # commonly-cited chars-per-token ratio for English/code text; this is a
1120
+ # rough sizing signal for the log, not a billing-accurate count.
1121
+ log_chars_per_token: 4
1122
+
1123
+ embedding:
1124
+ # Default local (sentence-transformers) embedding model for the L3 content
1125
+ # index (UPG-EMBEDDER-SWAP-GRANITE). Dimension-compatible drop-in for the
1126
+ # prior default (Snowflake/snowflake-arctic-embed-m-v1.5 — both 768-dim),
1127
+ # but a DIFFERENT vector space: a workspace's collection must never mix
1128
+ # vectors from the two (see CodeIndexer's embed-model stamp, which forces
1129
+ # a full rebuild on a stamp mismatch rather than letting old and new
1130
+ # vectors coexist). Query-prompt handling stays model-driven, never
1131
+ # hardcoded to either model's specific prefix convention: LocalEmbedProvider
1132
+ # detects a registered "query" prompt on the loaded model itself
1133
+ # (`model.prompts`) and only applies it when one exists, so a model with no
1134
+ # such registration (like this default) embeds queries and documents
1135
+ # identically, unchanged.
1136
+ default_model: ibm-granite/granite-embedding-english-r2
1137
+ # Hard cap on the encoder's token sequence length for local (sentence-
1138
+ # transformers) embedding, applied on top of whatever the model's own
1139
+ # config declares. Long-context embedders (this default declares 8192)
1140
+ # otherwise encode every long chunk at full length — quadratic-attention
1141
+ # cost on CPU, observed to stall a full-corpus index for hours — while
1142
+ # short-context models (512) are unaffected. 512 matches the prior
1143
+ # default's behavior; raise deliberately, with indexing throughput in mind.
1144
+ max_seq_length: 512
1145
+
1146
+ fetch:
1147
+ # UPG-CTX-EVICT: deterministic re-fetch-by-chunk-id contract. Every chunk
1148
+ # already carries a stable id (`file_path:start_line-end_line`, the literal
1149
+ # ChromaDB id) — vectr_fetch/POST /v1/fetch/`vectr fetch` restore any of
1150
+ # them verbatim in one call, no embedding or rerank involved. This caps how
1151
+ # many ids a single call may request, so a caller can't turn the re-fetch
1152
+ # surface into an unbounded bulk-export of the whole index.
1153
+ max_ids_per_call: 20