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/__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)
|