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/__init__.py ADDED
File without changes
agent/cartographer.py ADDED
@@ -0,0 +1,428 @@
1
+ """
2
+ CartographerAgent — builds a persistent codebase passport.
3
+
4
+ Design principle: vectr never calls an LLM internally. The AI editor (Claude Code,
5
+ Cursor, etc.) IS the LLM. Vectr collects raw structural metadata and returns it;
6
+ the AI synthesises the passport and writes it back via vectr_map_save. Vectr stores
7
+ and serves it cheaply from then on.
8
+
9
+ vectr_map flow:
10
+ - Passport exists → return cached AI-written summary (~300 tokens, instant)
11
+ - No passport yet → return raw structural metadata + instruct AI to call vectr_map_save
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import time
19
+ from pathlib import Path
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _build_directory_sketch(workspace_root: str, max_files: int = 120) -> dict:
25
+ """
26
+ Walk up to 3 levels deep and collect representative file listing.
27
+ Returns a dict with dirs, file samples, and detected signals — not a string.
28
+ """
29
+ from agent.indexer import EXCLUDED_DIRS
30
+
31
+ root = Path(workspace_root)
32
+ excluded = EXCLUDED_DIRS
33
+ structure: dict[str, list[str]] = {} # rel_dir → [filenames]
34
+ file_count = 0
35
+
36
+ for dirpath, dirnames, filenames in os.walk(root):
37
+ depth = len(Path(dirpath).relative_to(root).parts)
38
+ if depth > 3:
39
+ dirnames.clear()
40
+ continue
41
+ dirnames[:] = sorted(d for d in dirnames if d not in excluded and not d.startswith("."))
42
+
43
+ rel = str(Path(dirpath).relative_to(root)) or "."
44
+ sample = sorted(filenames)[:15]
45
+ if sample:
46
+ structure[rel] = sample
47
+ file_count += sum(1 for _ in filenames)
48
+ if file_count >= max_files:
49
+ break
50
+
51
+ return structure
52
+
53
+
54
+ def _read_readme_snippet(workspace_root: str, max_chars: int = 1500) -> str:
55
+ """Return first 1500 chars of README if present, empty string otherwise."""
56
+ for name in ("README.md", "README.rst", "README.txt", "readme.md"):
57
+ p = Path(workspace_root) / name
58
+ if p.exists():
59
+ text = p.read_text(encoding="utf-8", errors="ignore")[:max_chars]
60
+ logger.debug("Cartographer: found README at %s (%d chars)", p, len(text))
61
+ return text
62
+ return ""
63
+
64
+
65
+ # Display names for the lowercase language keys the indexer stores in chunk
66
+ # metadata (agent/indexer/_constants.py LANG_BY_EXT) — used to render
67
+ # `indexed_language_stats()` results as human-readable passport text.
68
+ _LANG_DISPLAY_NAMES: dict[str, str] = {
69
+ "python": "Python", "javascript": "JavaScript", "typescript": "TypeScript",
70
+ "go": "Go", "rust": "Rust", "java": "Java", "zig": "Zig", "c": "C",
71
+ "cpp": "C++", "markdown": "Markdown", "html": "HTML", "txt": "Text",
72
+ "rst": "reStructuredText",
73
+ }
74
+
75
+
76
+ def _languages_from_index(language_stats: dict[str, dict[str, int]]) -> list[str]:
77
+ """Derive display-name languages from the indexer's real per-language
78
+ coverage (`CodeIndexer.indexed_language_stats()`), ordered by file count
79
+ descending. This is the ground truth for what vectr actually indexed —
80
+ it already applies .vectrignore/.gitignore/EXCLUDED_DIRS and every other
81
+ indexing-time exclusion, so it can only diverge from a raw directory walk
82
+ by being MORE accurate (UPG-6.1)."""
83
+ ordered = sorted(language_stats.items(), key=lambda kv: -kv[1]["files"])
84
+ return [_LANG_DISPLAY_NAMES.get(lang, lang) for lang, _ in ordered]
85
+
86
+
87
+ def _detect_languages(structure: dict[str, list[str]]) -> list[str]:
88
+ """Infer languages from file extensions in the directory sketch.
89
+
90
+ Fallback only — used when the workspace has not been indexed yet (no
91
+ real coverage data available), e.g. the very first `vectr_map` call
92
+ before background indexing completes. Once real index data exists,
93
+ `_languages_from_index` is authoritative (UPG-6.1).
94
+ """
95
+ ext_map = {
96
+ ".py": "Python", ".ts": "TypeScript", ".tsx": "TypeScript",
97
+ ".js": "JavaScript", ".jsx": "JavaScript", ".go": "Go",
98
+ ".java": "Java", ".rs": "Rust", ".rb": "Ruby",
99
+ ".cs": "C#", ".cpp": "C++", ".c": "C", ".kt": "Kotlin",
100
+ ".swift": "Swift", ".php": "PHP", ".scala": "Scala",
101
+ }
102
+ seen: dict[str, int] = {}
103
+ for files in structure.values():
104
+ for f in files:
105
+ ext = Path(f).suffix.lower()
106
+ if ext in ext_map:
107
+ lang = ext_map[ext]
108
+ seen[lang] = seen.get(lang, 0) + 1
109
+ return sorted(seen, key=lambda l: -seen[l])
110
+
111
+
112
+ def _detect_frameworks(workspace_root: str, structure: dict[str, list[str]]) -> list[str]:
113
+ """Detect frameworks from config files and directory names."""
114
+ root = Path(workspace_root)
115
+ signals: list[str] = []
116
+
117
+ config_signals = {
118
+ "pyproject.toml": "Python/pyproject", "requirements.txt": "Python",
119
+ "package.json": "Node.js", "go.mod": "Go modules",
120
+ "Cargo.toml": "Rust/Cargo", "pom.xml": "Java/Maven",
121
+ "build.gradle": "Java/Gradle", "Gemfile": "Ruby/Bundler",
122
+ "docker-compose.yml": "Docker", "Dockerfile": "Docker",
123
+ ".proto": "gRPC",
124
+ }
125
+ all_files = [f for files in structure.values() for f in files]
126
+ for fname in all_files:
127
+ for key, label in config_signals.items():
128
+ if fname == key or fname.endswith(key):
129
+ if label not in signals:
130
+ signals.append(label)
131
+
132
+ # check root-level dirs
133
+ dir_signals = {
134
+ "tests": "testing", "test": "testing", "__tests__": "testing",
135
+ "docs": "documentation", "proto": "gRPC", "migrations": "database migrations",
136
+ }
137
+ top_dirs = [d for d in structure.get(".", []) if (root / d).is_dir()]
138
+ for d in top_dirs:
139
+ if d in dir_signals and dir_signals[d] not in signals:
140
+ signals.append(dir_signals[d])
141
+
142
+ return signals
143
+
144
+
145
+ def _build_import_graph(workspace_root: str, indexed_files: list[str] | None = None) -> "dict[str, set[str]]":
146
+ """
147
+ Build a file-to-file import graph by parsing import statements.
148
+ Returns {file_path: {imported_file_path, ...}} — edges only to files in the workspace.
149
+ """
150
+ import re as _re
151
+ root = Path(workspace_root)
152
+
153
+ # Collect all Python/JS/TS files in workspace
154
+ if indexed_files:
155
+ candidate_files = [Path(f) for f in indexed_files
156
+ if Path(f).suffix.lower() in {".py", ".js", ".ts", ".jsx", ".tsx"}]
157
+ else:
158
+ candidate_files = []
159
+ for f in root.rglob("*.py"):
160
+ candidate_files.append(f)
161
+
162
+ # Build a name → path index for quick resolution
163
+ name_index: dict[str, Path] = {}
164
+ for f in candidate_files:
165
+ # index by stem and by relative path without extension
166
+ stem = f.stem
167
+ name_index[stem] = f
168
+ try:
169
+ rel = f.relative_to(root)
170
+ name_index[str(rel).replace(os.sep, ".").removesuffix(f.suffix)] = f
171
+ except ValueError:
172
+ pass
173
+
174
+ _PY_IMPORT = _re.compile(r'^\s*(?:from|import)\s+([\w.]+)', _re.MULTILINE)
175
+ graph: dict[str, set[str]] = {}
176
+
177
+ for f in candidate_files:
178
+ try:
179
+ src = f.read_text(encoding="utf-8", errors="ignore")
180
+ except OSError:
181
+ continue
182
+
183
+ deps: set[str] = set()
184
+ for m in _PY_IMPORT.finditer(src):
185
+ mod = m.group(1).split(".")[0] # top-level module name
186
+ if mod in name_index:
187
+ target = str(name_index[mod])
188
+ if target != str(f):
189
+ deps.add(target)
190
+
191
+ graph[str(f)] = deps
192
+
193
+ return graph
194
+
195
+
196
+ def detect_module_communities(
197
+ workspace_root: str,
198
+ indexed_files: list[str] | None = None,
199
+ min_community_size: int = 2,
200
+ ) -> list[dict]:
201
+ """
202
+ Detect module communities using Louvain-style greedy modularity
203
+ maximisation (via networkx). Returns a list of community dicts:
204
+ [{"id": 0, "label": "auth", "files": ["auth/models.py", ...], "size": 3}, ...]
205
+
206
+ Community label is the most common top-level directory among member files.
207
+ Falls back gracefully if networkx is unavailable.
208
+ """
209
+ import_graph = _build_import_graph(workspace_root, indexed_files)
210
+ if not import_graph:
211
+ return []
212
+
213
+ try:
214
+ import networkx as nx
215
+ from networkx.algorithms.community import greedy_modularity_communities
216
+
217
+ G = nx.Graph()
218
+ for src, dsts in import_graph.items():
219
+ G.add_node(src)
220
+ for dst in dsts:
221
+ G.add_edge(src, dst)
222
+
223
+ if G.number_of_nodes() < 2:
224
+ return []
225
+
226
+ raw_communities = list(greedy_modularity_communities(G))
227
+ root = Path(workspace_root)
228
+ communities: list[dict] = []
229
+
230
+ for i, members in enumerate(raw_communities):
231
+ files = sorted(members)
232
+ if len(files) < min_community_size:
233
+ continue
234
+
235
+ # Derive label from the most common top-level directory
236
+ top_dirs: dict[str, int] = {}
237
+ for f in files:
238
+ try:
239
+ rel = Path(f).relative_to(root)
240
+ top = rel.parts[0] if len(rel.parts) > 1 else rel.stem
241
+ except ValueError:
242
+ top = Path(f).stem
243
+ top_dirs[top] = top_dirs.get(top, 0) + 1
244
+ label = max(top_dirs, key=top_dirs.get) if top_dirs else f"cluster_{i}"
245
+
246
+ communities.append({
247
+ "id": i,
248
+ "label": label,
249
+ "files": files,
250
+ "size": len(files),
251
+ })
252
+
253
+ return sorted(communities, key=lambda c: -c["size"])
254
+
255
+ except Exception as exc:
256
+ logger.debug("Louvain community detection skipped: %s", exc)
257
+ return []
258
+
259
+
260
+ def collect_raw_metadata(
261
+ workspace_root: str,
262
+ indexed_files: list[str] | None = None,
263
+ language_stats: dict[str, dict[str, int]] | None = None,
264
+ ) -> dict:
265
+ """
266
+ Collect raw structural metadata about the workspace.
267
+ No LLM call — pure file system inspection.
268
+ Returns a dict the AI can read and summarise into a passport.
269
+
270
+ `language_stats` (from `CodeIndexer.indexed_language_stats()`) is the real
271
+ per-language coverage of what vectr actually indexed. When provided and
272
+ non-empty it is authoritative for the `languages` field (UPG-6.1); the
273
+ directory-walk extension guess (`_detect_languages`) is only a fallback
274
+ for the pre-index case.
275
+ """
276
+ logger.info("Cartographer: collecting raw metadata for %s", workspace_root)
277
+ structure = _build_directory_sketch(workspace_root)
278
+ readme = _read_readme_snippet(workspace_root)
279
+ languages = (
280
+ _languages_from_index(language_stats) if language_stats else _detect_languages(structure)
281
+ )
282
+ frameworks = _detect_frameworks(workspace_root, structure)
283
+
284
+ # auto-cluster modules via Louvain community detection
285
+ communities = detect_module_communities(workspace_root, indexed_files)
286
+
287
+ metadata = {
288
+ "workspace_name": Path(workspace_root).name,
289
+ "languages": languages,
290
+ "frameworks": frameworks,
291
+ "structure": structure,
292
+ "readme_excerpt": readme,
293
+ "module_communities": communities,
294
+ "collected_at": time.time(),
295
+ }
296
+ logger.debug(
297
+ "Cartographer: collected metadata — languages=%s, frameworks=%s, communities=%d",
298
+ languages, frameworks, len(communities),
299
+ )
300
+ return metadata
301
+
302
+
303
+ def format_raw_metadata_for_llm(metadata: dict) -> str:
304
+ """
305
+ Format raw metadata as a readable string for the AI.
306
+ The AI reads this, synthesises a summary, then calls vectr_map_save.
307
+ """
308
+ parts = [
309
+ f"# Codebase: {metadata.get('workspace_name', 'unknown')}",
310
+ "",
311
+ "No passport cached yet. Read the metadata below, then call vectr_map_save",
312
+ "with a concise (~300 token) plain-English summary so future sessions get",
313
+ "an instant codebase overview without re-reading files.",
314
+ "",
315
+ ]
316
+
317
+ if metadata.get("languages"):
318
+ from agent.symbol_graph import supports_symbols
319
+ parts.append(f"Languages: {', '.join(metadata['languages'])}")
320
+ with_syms = [l for l in metadata["languages"] if supports_symbols(l)]
321
+ if with_syms:
322
+ parts.append(
323
+ f" locate/trace available for: {', '.join(with_syms)} "
324
+ "(other languages are search-only)"
325
+ )
326
+ if metadata.get("frameworks"):
327
+ parts.append(f"Detected: {', '.join(metadata['frameworks'])}")
328
+
329
+ parts.append("\nDirectory structure (up to 3 levels):")
330
+ for rel_dir, files in sorted(metadata.get("structure", {}).items()):
331
+ parts.append(f" {rel_dir}/")
332
+ for f in files[:8]:
333
+ parts.append(f" {f}")
334
+
335
+ if metadata.get("readme_excerpt"):
336
+ parts.append(f"\nREADME excerpt:\n{metadata['readme_excerpt'][:800]}")
337
+
338
+ # include module community clusters if detected
339
+ communities = metadata.get("module_communities", [])
340
+ if communities:
341
+ parts.append("\nModule communities (auto-clustered by import relationships):")
342
+ for c in communities[:8]: # show top 8 communities
343
+ file_sample = ", ".join(Path(f).name for f in c["files"][:4])
344
+ if len(c["files"]) > 4:
345
+ file_sample += f", +{len(c['files']) - 4} more"
346
+ parts.append(f" [{c['label']}] ({c['size']} files) {file_sample}")
347
+
348
+ return "\n".join(parts)
349
+
350
+
351
+ class PassportStore:
352
+ """Persist and retrieve the AI-written codebase passport."""
353
+
354
+ def __init__(self, db_dir: str) -> None:
355
+ self._path = Path(db_dir) / "passport.json"
356
+ logger.debug("PassportStore: db path = %s", self._path)
357
+
358
+ def load(self) -> dict | None:
359
+ if self._path.exists():
360
+ try:
361
+ data = json.loads(self._path.read_text(encoding="utf-8"))
362
+ logger.debug("PassportStore: loaded passport (workspace=%s)", data.get("_workspace"))
363
+ return data
364
+ except Exception as exc:
365
+ logger.warning("PassportStore: failed to load passport — %s", exc)
366
+ return None
367
+ return None
368
+
369
+ def save(self, passport: dict) -> None:
370
+ """Save a full passport dict (backward-compat for any existing callers)."""
371
+ self._path.write_text(json.dumps(passport, indent=2), encoding="utf-8")
372
+ logger.info("PassportStore: passport saved")
373
+
374
+ def save_summary(self, summary: str, workspace_root: str) -> None:
375
+ """
376
+ Persist an AI-written passport summary.
377
+ Called via vectr_map_save — the AI has already synthesised the passport.
378
+ """
379
+ passport = {
380
+ "summary": summary,
381
+ "_generated_at": time.time(),
382
+ "_workspace": workspace_root,
383
+ "_source": "ai_editor",
384
+ }
385
+ self.save(passport)
386
+ logger.info("PassportStore: AI-written passport saved for %s (%d chars)", workspace_root, len(summary))
387
+
388
+ def exists(self) -> bool:
389
+ return self._path.exists()
390
+
391
+ def format_for_llm(
392
+ self,
393
+ workspace_root: str,
394
+ language_stats: dict[str, dict[str, int]] | None = None,
395
+ ) -> str:
396
+ """
397
+ Return passport for AI consumption.
398
+ If cached: return the stored summary.
399
+ If not: collect raw metadata and prompt the AI to call vectr_map_save.
400
+
401
+ `language_stats` (real indexer coverage) is forwarded to
402
+ `collect_raw_metadata` so the raw-metadata path reports the languages
403
+ vectr actually indexed rather than a directory-walk guess (UPG-6.1).
404
+ """
405
+ p = self.load()
406
+ if p and p.get("summary"):
407
+ summary = p["summary"]
408
+ logger.debug("PassportStore: returning cached passport (%d chars)", len(summary))
409
+ name = Path(p.get("_workspace", workspace_root)).name
410
+ # UPG-6.2: only a passport written via vectr_map_save (an AI editor
411
+ # that actually read and synthesised the codebase) is trusted at
412
+ # face value. Anything else — e.g. a passport written through the
413
+ # PassportStore.save() backward-compat path by a non-editor caller —
414
+ # is labelled unverified so the reading AI applies appropriate
415
+ # scepticism instead of treating it as reviewed ground truth.
416
+ if p.get("_source") == "ai_editor":
417
+ header = f"# Codebase Passport — {name}"
418
+ else:
419
+ header = (
420
+ f"# Codebase Passport — {name} [UNVERIFIED — machine-generated, "
421
+ "not reviewed by an AI editor]"
422
+ )
423
+ return f"{header}\n\n{summary}"
424
+
425
+ # No passport yet — return raw metadata so the AI can synthesise one
426
+ logger.info("PassportStore: no passport found, returning raw metadata for %s", workspace_root)
427
+ metadata = collect_raw_metadata(workspace_root, language_stats=language_stats)
428
+ return format_raw_metadata_for_llm(metadata)