vigil-codeintel 0.1.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.
- vigil_codeintel-0.1.0.dist-info/METADATA +780 -0
- vigil_codeintel-0.1.0.dist-info/RECORD +131 -0
- vigil_codeintel-0.1.0.dist-info/WHEEL +5 -0
- vigil_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- vigil_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- vigil_codeintel-0.1.0.dist-info/top_level.txt +3 -0
- vigil_forensic/__init__.py +224 -0
- vigil_forensic/_git_utils.py +178 -0
- vigil_forensic/_shared.py +510 -0
- vigil_forensic/_stubs.py +156 -0
- vigil_forensic/gate_checks/__init__.py +1 -0
- vigil_forensic/gate_checks/_ast_helpers.py +629 -0
- vigil_forensic/gate_checks/_deployment_detector.py +573 -0
- vigil_forensic/gate_checks/atomic_write_checks.py +1143 -0
- vigil_forensic/gate_checks/authority_checks.py +95 -0
- vigil_forensic/gate_checks/boundary_breach_checks.py +202 -0
- vigil_forensic/gate_checks/broad_except_checks.py +301 -0
- vigil_forensic/gate_checks/broad_except_hidden_sentinel_checks.py +365 -0
- vigil_forensic/gate_checks/common.py +253 -0
- vigil_forensic/gate_checks/config_safety_checks.py +704 -0
- vigil_forensic/gate_checks/config_ssot_checks.py +78 -0
- vigil_forensic/gate_checks/conflict_checks.py +193 -0
- vigil_forensic/gate_checks/context_fallback_checks.py +697 -0
- vigil_forensic/gate_checks/context_health_checks.py +289 -0
- vigil_forensic/gate_checks/contract_shape_drift_checks.py +459 -0
- vigil_forensic/gate_checks/dirty_baseline_check.py +274 -0
- vigil_forensic/gate_checks/duplication_checks.py +387 -0
- vigil_forensic/gate_checks/embedded_string_checks.py +123 -0
- vigil_forensic/gate_checks/empty_output_checks.py +87 -0
- vigil_forensic/gate_checks/encoding_checks.py +847 -0
- vigil_forensic/gate_checks/export_completeness_checks.py +156 -0
- vigil_forensic/gate_checks/fallback_checks.py +41 -0
- vigil_forensic/gate_checks/file_proliferation_checks.py +171 -0
- vigil_forensic/gate_checks/fix_without_test_checks.py +69 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/__init__.py +9 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/_helpers.py +71 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/advanced_checks.py +322 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/core.py +273 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/integrity_checks.py +203 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/quality_checks.py +666 -0
- vigil_forensic/gate_checks/forensic_clusters/__init__.py +193 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist.py +426 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist_writer.py +302 -0
- vigil_forensic/gate_checks/forensic_clusters/api_protocol.py +231 -0
- vigil_forensic/gate_checks/forensic_clusters/async_quality.py +1156 -0
- vigil_forensic/gate_checks/forensic_clusters/code_style.py +808 -0
- vigil_forensic/gate_checks/forensic_clusters/core.py +319 -0
- vigil_forensic/gate_checks/forensic_clusters/data_quality.py +763 -0
- vigil_forensic/gate_checks/forensic_clusters/dead_code.py +480 -0
- vigil_forensic/gate_checks/forensic_clusters/edit_mutation.py +842 -0
- vigil_forensic/gate_checks/forensic_clusters/exception_boundary.py +240 -0
- vigil_forensic/gate_checks/forensic_clusters/legacy_debt.py +556 -0
- vigil_forensic/gate_checks/forensic_clusters/static_analysis.py +834 -0
- vigil_forensic/gate_checks/forensic_clusters/structural_quality.py +298 -0
- vigil_forensic/gate_checks/god_object_zones_checks.py +173 -0
- vigil_forensic/gate_checks/hallucination_checks.py +566 -0
- vigil_forensic/gate_checks/hunter_artifact_completeness_check.py +139 -0
- vigil_forensic/gate_checks/implementation_overfit_checks.py +380 -0
- vigil_forensic/gate_checks/import_integrity_checks.py +233 -0
- vigil_forensic/gate_checks/imports_in_function_checks.py +283 -0
- vigil_forensic/gate_checks/ml_checks.py +318 -0
- vigil_forensic/gate_checks/performance_checks.py +106 -0
- vigil_forensic/gate_checks/project_specific_runner.py +691 -0
- vigil_forensic/gate_checks/provider_capability_checks.py +73 -0
- vigil_forensic/gate_checks/refactor_completeness_checks.py +274 -0
- vigil_forensic/gate_checks/reliability_checks.py +389 -0
- vigil_forensic/gate_checks/reporting_checks.py +55 -0
- vigil_forensic/gate_checks/runtime_behavior_checks.py +220 -0
- vigil_forensic/gate_checks/security_injection_checks.py +332 -0
- vigil_forensic/gate_checks/semantic_intent_checks.py +139 -0
- vigil_forensic/gate_checks/size_complexity_checks.py +336 -0
- vigil_forensic/gate_checks/stuck_feature_flag_checks.py +354 -0
- vigil_forensic/gate_checks/syntax_validity_checks.py +217 -0
- vigil_forensic/gate_checks/temporal_freshness_checks.py +79 -0
- vigil_forensic/gate_checks/test_quality_checks.py +946 -0
- vigil_forensic/gate_checks/testing_checks.py +149 -0
- vigil_forensic/gate_checks/toctou_checks.py +367 -0
- vigil_forensic/gate_checks/type_checking_checks.py +316 -0
- vigil_forensic/gate_models.py +392 -0
- vigil_forensic/gate_packs/__init__.py +1 -0
- vigil_forensic/gate_packs/universal.py +179 -0
- vigil_forensic/gate_profile.json +31 -0
- vigil_forensic/gate_registry.py +21 -0
- vigil_forensic/language_profiles.py +219 -0
- vigil_forensic/meta_findings.py +207 -0
- vigil_forensic/self_audit.py +725 -0
- vigil_forensic/source_analysis.py +175 -0
- vigil_mapper/__init__.py +103 -0
- vigil_mapper/_ast_helpers_minimal.py +229 -0
- vigil_mapper/_extract_imports_impl.py +123 -0
- vigil_mapper/_file_count_guard.py +129 -0
- vigil_mapper/_git_utils.py +178 -0
- vigil_mapper/_runtime_ast.py +438 -0
- vigil_mapper/_runtime_dispatch.py +137 -0
- vigil_mapper/_seed_helpers.py +82 -0
- vigil_mapper/authority_builder.py +1102 -0
- vigil_mapper/cli_entry.py +731 -0
- vigil_mapper/conflict_builder.py +818 -0
- vigil_mapper/data_contract_builder.py +446 -0
- vigil_mapper/findings_builder.py +716 -0
- vigil_mapper/fingerprint.py +53 -0
- vigil_mapper/hotspot_builder.py +539 -0
- vigil_mapper/map_common.py +449 -0
- vigil_mapper/map_errors.py +55 -0
- vigil_mapper/map_models.py +431 -0
- vigil_mapper/map_models_ext.py +206 -0
- vigil_mapper/map_models_findings.py +130 -0
- vigil_mapper/map_storage.py +455 -0
- vigil_mapper/parse_cache.py +795 -0
- vigil_mapper/refactor_boundary_builder.py +266 -0
- vigil_mapper/runtime_builder.py +527 -0
- vigil_mapper/runtime_tracer.py +243 -0
- vigil_mapper/runtime_tracer_entry.py +199 -0
- vigil_mapper/semantic_diff.py +71 -0
- vigil_mapper/source_adapters/__init__.py +109 -0
- vigil_mapper/source_adapters/_base.py +264 -0
- vigil_mapper/source_adapters/_ir.py +156 -0
- vigil_mapper/source_adapters/_lexer.py +309 -0
- vigil_mapper/source_adapters/_patterns.py +212 -0
- vigil_mapper/source_adapters/_treesitter.py +182 -0
- vigil_mapper/source_adapters/go.py +553 -0
- vigil_mapper/source_adapters/java.py +541 -0
- vigil_mapper/source_adapters/javascript.py +626 -0
- vigil_mapper/source_adapters/python.py +325 -0
- vigil_mapper/source_adapters/typescript.py +749 -0
- vigil_mapper/structural_builder.py +586 -0
- vigil_mcp/__init__.py +1 -0
- vigil_mcp/_jobs.py +587 -0
- vigil_mcp/_paths.py +93 -0
- vigil_mcp/forensic_server.py +419 -0
- vigil_mcp/map_server.py +452 -0
vigil_mcp/map_server.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""FastMCP stdio server: code-map
|
|
2
|
+
|
|
3
|
+
Wraps vigil_mapper.run_map_build + load_repo_maps behind a
|
|
4
|
+
background-job poll API. Resource constraints:
|
|
5
|
+
- At most 2 concurrent jobs (enforced by _jobs.JobRegistry).
|
|
6
|
+
- One thread per job (no pool).
|
|
7
|
+
- Output truncated/paginated to OUTPUT_CHAR_LIMIT chars to stay within
|
|
8
|
+
MCP token limits (~25 k tokens ≈ ~100 k chars; we use 80 k to be safe).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from mcp.server.fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
from vigil_mcp import _jobs
|
|
19
|
+
from vigil_mcp import _paths
|
|
20
|
+
from vigil_mapper import run_map_build, load_repo_maps
|
|
21
|
+
|
|
22
|
+
_INSTRUCTIONS = """\
|
|
23
|
+
code-map - builds structural maps of a codebase across Python/Go/Java/JS/TS:
|
|
24
|
+
imports/dependencies, defined symbols, runtime entry points, data contracts,
|
|
25
|
+
authority/write sites, risk hotspots, and refactor boundaries. Static analysis
|
|
26
|
+
(tree-sitter/AST) - it never runs the project.
|
|
27
|
+
|
|
28
|
+
WHEN TO USE: when the user wants to understand an unfamiliar codebase's architecture,
|
|
29
|
+
find what imports/depends on what, locate runtime entry points or risk hotspots, or
|
|
30
|
+
scope a refactor. Not a code-quality auditor (use forensic-audit for bugs/smells).
|
|
31
|
+
|
|
32
|
+
WORKFLOW (background job + poll):
|
|
33
|
+
1. start_code_map(path="", map="all") -> leave path empty to auto-detect the project
|
|
34
|
+
root from the current directory; returns {job_id, resolved_path}.
|
|
35
|
+
2. get_code_map_status(job_id) -> poll until status == "done".
|
|
36
|
+
3. get_code_map_results(job_id) -> COMPACT SUMMARY by default (per-map-type
|
|
37
|
+
counts + top entries). Read this FIRST; it fits the context budget.
|
|
38
|
+
4. Drill in: get_code_map_results(job_id, map="structural") for one full map type,
|
|
39
|
+
or view="full" for every map (both paginated via page=).
|
|
40
|
+
Also: load_code_map_by_path(path) re-reads maps built in an earlier session (no job).
|
|
41
|
+
|
|
42
|
+
SEEDS (optional refinement): 6 maps need no config. Three can be refined by a JSON
|
|
43
|
+
seed under <project>/.cortex/map_seeds/:
|
|
44
|
+
- authority_domains.json: group write sites into named domains (without it, each
|
|
45
|
+
writer is auto-surfaced on its own).
|
|
46
|
+
- runtime_seed.json: declare extra runtime nodes beyond auto-discovered entrypoints.
|
|
47
|
+
- refactor_boundaries.json: define the refactor goal/boundaries (refactor_boundary
|
|
48
|
+
is seed-driven and needs a goal).
|
|
49
|
+
See docs/usage/code-map.* (section "Seeds") for the JSON format and templates.
|
|
50
|
+
|
|
51
|
+
HUGE REPOS (anti-hang): if the collected file count exceeds max_files (default 800),
|
|
52
|
+
the build is SKIPPED and get_code_map_results returns skipped_reason="too_many_files"
|
|
53
|
+
with top_subdirs + a suggestion - build a submodule (start_code_map(path='<dir>/<subdir>'))
|
|
54
|
+
or pass a larger max_files to force a full build.
|
|
55
|
+
|
|
56
|
+
NOTE: output is summary-first to stay within the context budget; maps are cached on
|
|
57
|
+
disk under <project>/.cortex/ so re-runs are cheap.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
mcp = FastMCP("code-map", instructions=_INSTRUCTIONS)
|
|
61
|
+
|
|
62
|
+
# ~80 k chars keeps well under the 25 k token MCP output limit.
|
|
63
|
+
OUTPUT_CHAR_LIMIT = 80_000
|
|
64
|
+
|
|
65
|
+
# Map list keys present in the serialisable maps dict.
|
|
66
|
+
_MAP_TYPES = (
|
|
67
|
+
"structural",
|
|
68
|
+
"runtime",
|
|
69
|
+
"data_contract",
|
|
70
|
+
"authority",
|
|
71
|
+
"conflict",
|
|
72
|
+
"hotspot",
|
|
73
|
+
"refactor_boundary",
|
|
74
|
+
"findings",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Per-map-type cap for entries shown in the compact summary view.
|
|
78
|
+
_TOP_ENTRIES_CAP = 10
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Serialisation helpers
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def _entry_to_dict(entry: Any) -> Any:
|
|
86
|
+
"""Convert a dataclass entry to a plain dict via to_dict() if available."""
|
|
87
|
+
if hasattr(entry, "to_dict"):
|
|
88
|
+
return entry.to_dict()
|
|
89
|
+
if hasattr(entry, "__dict__"):
|
|
90
|
+
return vars(entry)
|
|
91
|
+
return str(entry)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _repo_maps_to_serialisable(repo_maps: Any) -> dict:
|
|
95
|
+
"""Flatten a RepoMaps instance into a plain dict of lists."""
|
|
96
|
+
if repo_maps is None:
|
|
97
|
+
return {"missing": True}
|
|
98
|
+
if getattr(repo_maps, "missing", False):
|
|
99
|
+
return {
|
|
100
|
+
"missing": True,
|
|
101
|
+
"note": "maps directory not found - run start_code_map first",
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
"missing": False,
|
|
105
|
+
"schema_version": getattr(repo_maps, "schema_version", "unknown"),
|
|
106
|
+
"structural": [_entry_to_dict(e) for e in (repo_maps.structural or ())],
|
|
107
|
+
"runtime": [_entry_to_dict(e) for e in (repo_maps.runtime or ())],
|
|
108
|
+
"data_contract": [_entry_to_dict(e) for e in (repo_maps.data_contract or ())],
|
|
109
|
+
"authority": [_entry_to_dict(e) for e in (repo_maps.authority or ())],
|
|
110
|
+
"conflict": [_entry_to_dict(e) for e in (repo_maps.conflict or ())],
|
|
111
|
+
"hotspot": [_entry_to_dict(e) for e in (repo_maps.hotspot or ())],
|
|
112
|
+
"refactor_boundary": [
|
|
113
|
+
_entry_to_dict(e) for e in (repo_maps.refactor_boundary or ())
|
|
114
|
+
],
|
|
115
|
+
"findings": [_entry_to_dict(e) for e in (repo_maps.findings or ())],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _paginate_json(data: Any, page: int, page_size_chars: int) -> dict:
|
|
120
|
+
"""Serialise *data* to JSON and return a page slice with metadata."""
|
|
121
|
+
full_json = json.dumps(data, default=str, indent=2)
|
|
122
|
+
total = len(full_json)
|
|
123
|
+
start_char = page * page_size_chars
|
|
124
|
+
end_char = start_char + page_size_chars
|
|
125
|
+
return {
|
|
126
|
+
"payload": full_json[start_char:end_char],
|
|
127
|
+
"truncated": end_char < total,
|
|
128
|
+
"total_chars": total,
|
|
129
|
+
"page": page,
|
|
130
|
+
"total_pages": (total + page_size_chars - 1) // page_size_chars,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Summary builder (Feature 2 - summary-first map results)
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def _compact_map_entry(entry: Any) -> dict:
|
|
139
|
+
"""Project a map entry down to a few compact fields for the summary.
|
|
140
|
+
|
|
141
|
+
Map types use different field names for an entry's identity:
|
|
142
|
+
structural->file, data_contract->entity, hotspot->target, runtime->node,
|
|
143
|
+
conflict->subject, findings->title, refactor_boundary->boundary_id. Pull the
|
|
144
|
+
first present so the summary is MEANINGFUL for every map (not all-null).
|
|
145
|
+
"""
|
|
146
|
+
if not isinstance(entry, dict):
|
|
147
|
+
return {"value": str(entry)}
|
|
148
|
+
name = (
|
|
149
|
+
entry.get("name") or entry.get("entity") or entry.get("target")
|
|
150
|
+
or entry.get("node") or entry.get("subject") or entry.get("title")
|
|
151
|
+
or entry.get("boundary_id") or entry.get("conflict_id")
|
|
152
|
+
or entry.get("finding_id") or entry.get("authority_domain")
|
|
153
|
+
)
|
|
154
|
+
file = (
|
|
155
|
+
entry.get("file") or entry.get("defined_in")
|
|
156
|
+
or entry.get("canonical_schema") or entry.get("canonical_owner")
|
|
157
|
+
or entry.get("path")
|
|
158
|
+
)
|
|
159
|
+
compact: dict[str, Any] = {"name": name, "file": file}
|
|
160
|
+
if entry.get("line") is not None:
|
|
161
|
+
compact["line"] = entry.get("line")
|
|
162
|
+
# One signal metric if present (varies by map type).
|
|
163
|
+
for metric in ("hotspot_score", "severity", "size", "complexity", "score", "count"):
|
|
164
|
+
if entry.get(metric) is not None:
|
|
165
|
+
compact[metric] = entry[metric]
|
|
166
|
+
break
|
|
167
|
+
return compact
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _build_map_summary(maps_data: dict) -> dict:
|
|
171
|
+
"""Build a compact summary of serialised repo maps.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
maps_data: Output of ``_repo_maps_to_serialisable`` (a dict with one
|
|
175
|
+
list per map type).
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A dict with ``by_map_type`` (per-type counts), ``top_entries``
|
|
179
|
+
(up to ``_TOP_ENTRIES_CAP`` compact entries per non-empty type),
|
|
180
|
+
``schema_version``, ``missing`` and a ``hint``.
|
|
181
|
+
"""
|
|
182
|
+
by_map_type: dict[str, int] = {}
|
|
183
|
+
top_entries: dict[str, list[dict]] = {}
|
|
184
|
+
for map_type in _MAP_TYPES:
|
|
185
|
+
entries = maps_data.get(map_type) or []
|
|
186
|
+
by_map_type[map_type] = len(entries)
|
|
187
|
+
if entries:
|
|
188
|
+
top_entries[map_type] = [
|
|
189
|
+
_compact_map_entry(e) for e in entries[:_TOP_ENTRIES_CAP]
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
hint = (
|
|
193
|
+
"Compact map summary. For all entries of one map call "
|
|
194
|
+
"get_code_map_results with map='<type>' (e.g. 'structural'), or "
|
|
195
|
+
"view='full' for every map. Paging via page=."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
"missing": bool(maps_data.get("missing", False)),
|
|
200
|
+
"schema_version": maps_data.get("schema_version", "unknown"),
|
|
201
|
+
"by_map_type": by_map_type,
|
|
202
|
+
"top_entries": top_entries,
|
|
203
|
+
"hint": hint,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Internal: path-preserving wrapper around run_map_build
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def _run_map_build_with_path(path: str, map: str = "all", max_files: int = 800) -> dict:
|
|
212
|
+
"""Run the map build and bundle the project path into the return value
|
|
213
|
+
so that get_code_map_results can call load_repo_maps without extra args.
|
|
214
|
+
|
|
215
|
+
Anti-hang file-COUNT guard (mirrors the forensic guard): the map build does
|
|
216
|
+
per-file AST/tree-sitter work that scales with file count, so a repo with
|
|
217
|
+
thousands of files can hang the machine. ``run_map_build`` returns a bare int
|
|
218
|
+
exit code, so the guard is threaded HERE (the MCP wrapper that already
|
|
219
|
+
returns a dict consumed by ``get_code_map_results``) rather than mutating
|
|
220
|
+
that int contract. When the collected source-file count exceeds ``max_files``
|
|
221
|
+
we do NOT build maps; we return a FAST structured ``too_many_files`` dict
|
|
222
|
+
(count + top sub-dirs + a suggestion to narrow scope).
|
|
223
|
+
"""
|
|
224
|
+
from vigil_mapper.map_common import iter_source_files
|
|
225
|
+
from vigil_mapper._file_count_guard import build_too_many_files_meta
|
|
226
|
+
|
|
227
|
+
project_dir = Path(path)
|
|
228
|
+
# Cheap pass: collect the same source files the build would, but only to
|
|
229
|
+
# COUNT + group by top-level sub-dir. No parsing happens here.
|
|
230
|
+
root = project_dir.resolve()
|
|
231
|
+
rel_paths: list[str] = []
|
|
232
|
+
for f in iter_source_files(project_dir):
|
|
233
|
+
try:
|
|
234
|
+
rel_paths.append(f.resolve().relative_to(root).as_posix())
|
|
235
|
+
except ValueError:
|
|
236
|
+
rel_paths.append(f.name)
|
|
237
|
+
|
|
238
|
+
if len(rel_paths) > max_files:
|
|
239
|
+
meta = build_too_many_files_meta(
|
|
240
|
+
rel_paths, max_files, entry_call="start_code_map"
|
|
241
|
+
)
|
|
242
|
+
# exit_code 0: skipping is a controlled outcome, not an error.
|
|
243
|
+
return {"exit_code": 0, "_path": path, **meta}
|
|
244
|
+
|
|
245
|
+
exit_code = run_map_build(project_dir, map=map, timeout_s=300)
|
|
246
|
+
return {"exit_code": exit_code, "_path": path}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# MCP tools
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
@mcp.tool()
|
|
254
|
+
def start_code_map(path: str = "", map: str = "all", max_files: int = 800) -> dict:
|
|
255
|
+
"""Start a background code-map build job for the given project path.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
path: Absolute path to the project root directory. When empty/omitted
|
|
259
|
+
the project root is auto-detected by walking up from the current
|
|
260
|
+
working directory for a ``.git`` / ``pyproject.toml`` /
|
|
261
|
+
``package.json`` marker (falling back to cwd). The chosen
|
|
262
|
+
directory is returned as ``resolved_path``.
|
|
263
|
+
map: Map type to build - "all" (default) or a specific map name
|
|
264
|
+
recognised by vigil_mapper (e.g. "structural").
|
|
265
|
+
max_files: Anti-hang ceiling on the collected source-file count
|
|
266
|
+
(default 800). Above it the build is SKIPPED and
|
|
267
|
+
get_code_map_results reports skipped_reason="too_many_files" with
|
|
268
|
+
top_subdirs + a suggestion to scan a submodule; raise it to force
|
|
269
|
+
a full build of a huge repo.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
{"job_id": str | None, "status": "running" | "busy",
|
|
273
|
+
"resolved_path": str, ...}
|
|
274
|
+
When status is "busy" the server is at max concurrent jobs; retry later.
|
|
275
|
+
"""
|
|
276
|
+
# Auto-target: resolve only when no explicit path was given.
|
|
277
|
+
if path:
|
|
278
|
+
resolved_path = path
|
|
279
|
+
else:
|
|
280
|
+
resolved_path = _paths._resolve_project_root(None)
|
|
281
|
+
|
|
282
|
+
# project_dir enables disk-backed persistence so results survive a server
|
|
283
|
+
# restart; get_code_map_status/results then resolve by job_id from disk.
|
|
284
|
+
started = _jobs.start(
|
|
285
|
+
_run_map_build_with_path, resolved_path, map=map, max_files=max_files,
|
|
286
|
+
project_dir=resolved_path,
|
|
287
|
+
)
|
|
288
|
+
started["resolved_path"] = resolved_path
|
|
289
|
+
return started
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@mcp.tool()
|
|
293
|
+
def get_code_map_status(job_id: str) -> dict:
|
|
294
|
+
"""Poll the status of a code-map build job.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
job_id: Job ID returned by start_code_map.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
{"job_id": str, "status": "running" | "done" | "error" | "cancelled" | "not_found"}
|
|
301
|
+
"""
|
|
302
|
+
return _jobs.status(job_id)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@mcp.tool()
|
|
306
|
+
def get_code_map_results(
|
|
307
|
+
job_id: str,
|
|
308
|
+
view: str = "summary",
|
|
309
|
+
map: str = "",
|
|
310
|
+
page: int = 0,
|
|
311
|
+
page_size_chars: int = OUTPUT_CHAR_LIMIT,
|
|
312
|
+
) -> dict:
|
|
313
|
+
"""Retrieve results of a completed code-map build.
|
|
314
|
+
|
|
315
|
+
Three modes (loads maps written to disk by run_map_build):
|
|
316
|
+
* ``view='summary'`` (default) - per-map-type counts + the top entries
|
|
317
|
+
per map. Compact; fits the MCP context budget. Use this first.
|
|
318
|
+
* ``map='<type>'`` - every entry of a single map (e.g. 'structural'),
|
|
319
|
+
paginated. Takes precedence over ``view``.
|
|
320
|
+
* ``view='full'`` - every entry of every map, paginated.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
job_id: Job ID returned by start_code_map.
|
|
324
|
+
view: "summary" (default) or "full".
|
|
325
|
+
map: A single map type to return in full (e.g.
|
|
326
|
+
"structural"). Empty = honour ``view``.
|
|
327
|
+
page: Zero-based page index (each page ≈ page_size_chars chars).
|
|
328
|
+
page_size_chars: Max chars per page (default 80 000 ≈ 25 k tokens).
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
dict with "job_id", "status", "view", "exit_code", "payload"
|
|
332
|
+
(JSON string), "truncated" (bool), "total_chars", "page", "total_pages".
|
|
333
|
+
"""
|
|
334
|
+
r = _jobs.result(job_id)
|
|
335
|
+
status = r.get("status")
|
|
336
|
+
|
|
337
|
+
if status in ("running", "not_found"):
|
|
338
|
+
return {
|
|
339
|
+
"job_id": job_id, "status": status,
|
|
340
|
+
"payload": None, "truncated": False, "total_chars": 0,
|
|
341
|
+
}
|
|
342
|
+
if status == "cancelled":
|
|
343
|
+
return {
|
|
344
|
+
"job_id": job_id, "status": "cancelled",
|
|
345
|
+
"payload": None, "truncated": False, "total_chars": 0,
|
|
346
|
+
}
|
|
347
|
+
if status == "error":
|
|
348
|
+
return {
|
|
349
|
+
"job_id": job_id, "status": "error", "error": r.get("error"),
|
|
350
|
+
"payload": None, "truncated": False, "total_chars": 0,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# status == "done"
|
|
354
|
+
inner = r.get("result") or {}
|
|
355
|
+
exit_code = inner.get("exit_code") if isinstance(inner, dict) else inner
|
|
356
|
+
path_str = inner.get("_path") if isinstance(inner, dict) else None
|
|
357
|
+
|
|
358
|
+
# Anti-hang guard fired: the build was skipped because the repo has too many
|
|
359
|
+
# files. Surface the structured skip (no maps were built) instead of an
|
|
360
|
+
# empty/"missing" maps payload, so the caller learns to narrow scope.
|
|
361
|
+
if isinstance(inner, dict) and inner.get("skipped_reason") == "too_many_files":
|
|
362
|
+
skip_payload = {
|
|
363
|
+
"exit_code": exit_code,
|
|
364
|
+
"skipped_reason": inner.get("skipped_reason"),
|
|
365
|
+
"file_count": inner.get("file_count"),
|
|
366
|
+
"max_files": inner.get("max_files"),
|
|
367
|
+
"top_subdirs": inner.get("top_subdirs"),
|
|
368
|
+
"suggestion": inner.get("suggestion"),
|
|
369
|
+
}
|
|
370
|
+
page_data = _paginate_json(skip_payload, page=page, page_size_chars=page_size_chars)
|
|
371
|
+
return {
|
|
372
|
+
"job_id": job_id, "status": "done", "view": "skipped",
|
|
373
|
+
"exit_code": exit_code, **page_data,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if path_str:
|
|
377
|
+
try:
|
|
378
|
+
repo_maps = load_repo_maps(Path(path_str))
|
|
379
|
+
maps_data = _repo_maps_to_serialisable(repo_maps)
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
maps_data = {"error": str(exc)}
|
|
382
|
+
else:
|
|
383
|
+
# _repo_maps_to_serialisable is monkeypatchable; call it so tests that
|
|
384
|
+
# patch it can inject fake maps even when no path is available.
|
|
385
|
+
maps_data = _repo_maps_to_serialisable(None)
|
|
386
|
+
|
|
387
|
+
# Decide the projection of maps_data based on map / view.
|
|
388
|
+
effective_view = "summary"
|
|
389
|
+
if map and map != "all":
|
|
390
|
+
# Single-map view: only the requested map type's entries.
|
|
391
|
+
rendered_maps: dict = {map: maps_data.get(map, [])}
|
|
392
|
+
effective_view = f"map:{map}"
|
|
393
|
+
elif view == "full":
|
|
394
|
+
rendered_maps = maps_data
|
|
395
|
+
effective_view = "full"
|
|
396
|
+
else:
|
|
397
|
+
rendered_maps = _build_map_summary(maps_data)
|
|
398
|
+
effective_view = "summary"
|
|
399
|
+
|
|
400
|
+
full_data = {"exit_code": exit_code, "maps": rendered_maps}
|
|
401
|
+
page_data = _paginate_json(full_data, page=page, page_size_chars=page_size_chars)
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"job_id": job_id, "status": "done", "view": effective_view,
|
|
405
|
+
"exit_code": exit_code, **page_data,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@mcp.tool()
|
|
410
|
+
def load_code_map_by_path(
|
|
411
|
+
path: str,
|
|
412
|
+
page: int = 0,
|
|
413
|
+
page_size_chars: int = OUTPUT_CHAR_LIMIT,
|
|
414
|
+
) -> dict:
|
|
415
|
+
"""Load previously built maps from disk for a given project path (no job needed).
|
|
416
|
+
|
|
417
|
+
Useful when maps were built in a prior session, or to re-read results.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
path: Absolute path to the project root.
|
|
421
|
+
page: Zero-based page index.
|
|
422
|
+
page_size_chars: Max chars per page.
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
repo_maps = load_repo_maps(Path(path))
|
|
426
|
+
maps_data = _repo_maps_to_serialisable(repo_maps)
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
return {"status": "error", "error": str(exc)}
|
|
429
|
+
|
|
430
|
+
page_data = _paginate_json(maps_data, page=page, page_size_chars=page_size_chars)
|
|
431
|
+
return {"status": "ok", **page_data}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@mcp.tool()
|
|
435
|
+
def cancel_code_map(job_id: str) -> dict:
|
|
436
|
+
"""Cancel a running code-map build job.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
job_id: Job ID returned by start_code_map.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
{"job_id": str, "cancelled": bool, ...}
|
|
443
|
+
"""
|
|
444
|
+
return _jobs.cancel(job_id)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def main() -> None:
|
|
448
|
+
mcp.run()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == "__main__":
|
|
452
|
+
main()
|