code-atlas-forge 1.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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
code_atlas/mcp.py
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"""Servidor MCP sobre stdio — JSON-RPC 2.0 ndjson, zero-dep (lf-0042).
|
|
2
|
+
|
|
3
|
+
Transporte: newline-delimited JSON (MCP stdio). Sem HTTP neste ciclo.
|
|
4
|
+
Read-only: nenhuma tool destrutiva/escrita; inputs validados, limites
|
|
5
|
+
impostos. Delega aos motores reais — nunca stubs.
|
|
6
|
+
|
|
7
|
+
ADR de transporte: JSON-RPC próprio sobre stdio, mesmo padrão do
|
|
8
|
+
decoder SCIP (ADR-017) — o SDK oficial puxaria deps pesadas.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sqlite3
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, TextIO
|
|
19
|
+
|
|
20
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
21
|
+
SERVER_NAME = "code-atlas-forge"
|
|
22
|
+
|
|
23
|
+
_ERR_METHOD = -32601
|
|
24
|
+
_ERR_PARAMS = -32602
|
|
25
|
+
_ERR_PARSE = -32700
|
|
26
|
+
_ERR_INTERNAL = -32603
|
|
27
|
+
|
|
28
|
+
_TOOLS: dict[str, dict[str, Any]] = {
|
|
29
|
+
"atlas_search": {
|
|
30
|
+
"description": "busca híbrida/lexical/graph/vector por símbolo",
|
|
31
|
+
"inputSchema": {"type": "object", "properties": {
|
|
32
|
+
"query": {"type": "string"},
|
|
33
|
+
"strategy": {"type": "string", "enum": [
|
|
34
|
+
"lexical", "structural", "graph", "hybrid", "vector"]},
|
|
35
|
+
"root": {"type": "string"},
|
|
36
|
+
"limit": {"type": "integer", "maximum": 200},
|
|
37
|
+
}, "required": ["query"], "additionalProperties": False},
|
|
38
|
+
},
|
|
39
|
+
"atlas_symbol": {
|
|
40
|
+
"description": "detalhe de um símbolo (qualified_name)",
|
|
41
|
+
"inputSchema": {"type": "object", "properties": {
|
|
42
|
+
"name": {"type": "string"}, "root": {"type": "string"}},
|
|
43
|
+
"required": ["name"], "additionalProperties": False},
|
|
44
|
+
},
|
|
45
|
+
"atlas_references": {
|
|
46
|
+
"description": "relações que tocam um símbolo (callers/callees)",
|
|
47
|
+
"inputSchema": {"type": "object", "properties": {
|
|
48
|
+
"name": {"type": "string"}, "root": {"type": "string"}},
|
|
49
|
+
"required": ["name"], "additionalProperties": False},
|
|
50
|
+
},
|
|
51
|
+
"atlas_dependencies": {
|
|
52
|
+
"description": "dependências comprovadas do snapshot",
|
|
53
|
+
"inputSchema": {"type": "object", "properties": {
|
|
54
|
+
"root": {"type": "string"},
|
|
55
|
+
"relation_type": {"type": "string"}},
|
|
56
|
+
"additionalProperties": False},
|
|
57
|
+
},
|
|
58
|
+
"atlas_context": {
|
|
59
|
+
"description": "context pack sob budget (orientation|focus|deep)",
|
|
60
|
+
"inputSchema": {"type": "object", "properties": {
|
|
61
|
+
"task": {"type": "string"},
|
|
62
|
+
"level": {"type": "string",
|
|
63
|
+
"enum": ["orientation", "focus", "deep"]},
|
|
64
|
+
"budget": {"type": "integer", "maximum": 100000},
|
|
65
|
+
"root": {"type": "string"}},
|
|
66
|
+
"required": ["task"], "additionalProperties": False},
|
|
67
|
+
},
|
|
68
|
+
"atlas_evidence": {
|
|
69
|
+
"description": "evidências + proveniência de uma aresta/símbolo",
|
|
70
|
+
"inputSchema": {"type": "object", "properties": {
|
|
71
|
+
"name": {"type": "string"}, "root": {"type": "string"}},
|
|
72
|
+
"required": ["name"], "additionalProperties": False},
|
|
73
|
+
},
|
|
74
|
+
"atlas_status": {
|
|
75
|
+
"description": "estado do repositório/snapshot/capabilities",
|
|
76
|
+
"inputSchema": {"type": "object", "properties": {
|
|
77
|
+
"root": {"type": "string"}}, "additionalProperties": False},
|
|
78
|
+
},
|
|
79
|
+
"atlas_expand": {
|
|
80
|
+
"description": "expansão progressiva de símbolo no snapshot",
|
|
81
|
+
"inputSchema": {"type": "object", "properties": {
|
|
82
|
+
"name": {"type": "string"}, "root": {"type": "string"},
|
|
83
|
+
"delivered": {"type": "string"}},
|
|
84
|
+
"required": ["name"], "additionalProperties": False},
|
|
85
|
+
},
|
|
86
|
+
"atlas_impact": {
|
|
87
|
+
"description": "análise de impacto entre revisões/snapshots",
|
|
88
|
+
"inputSchema": {"type": "object", "properties": {
|
|
89
|
+
"base": {"type": "string"}, "target": {"type": "string"},
|
|
90
|
+
"symbol": {"type": "string"},
|
|
91
|
+
"root": {"type": "string"},
|
|
92
|
+
"known_only": {"type": "boolean"},
|
|
93
|
+
"max_paths": {"type": "integer", "maximum": 200}},
|
|
94
|
+
"additionalProperties": False},
|
|
95
|
+
},
|
|
96
|
+
"atlas_diff": {
|
|
97
|
+
"description": "ChangeSet git read-only entre revisões",
|
|
98
|
+
"inputSchema": {"type": "object", "properties": {
|
|
99
|
+
"base": {"type": "string"}, "target": {"type": "string"},
|
|
100
|
+
"root": {"type": "string"}},
|
|
101
|
+
"required": ["base"], "additionalProperties": False},
|
|
102
|
+
},
|
|
103
|
+
"atlas_test_impact": {
|
|
104
|
+
"description": "testes candidatos relacionados às mudanças",
|
|
105
|
+
"inputSchema": {"type": "object", "properties": {
|
|
106
|
+
"base": {"type": "string"}, "target": {"type": "string"},
|
|
107
|
+
"root": {"type": "string"}},
|
|
108
|
+
"additionalProperties": False},
|
|
109
|
+
},
|
|
110
|
+
"atlas_domains": {
|
|
111
|
+
"description": "adapters de domínio registrados "
|
|
112
|
+
"(opt-in — listar não extrai)",
|
|
113
|
+
"inputSchema": {"type": "object", "properties": {},
|
|
114
|
+
"additionalProperties": False},
|
|
115
|
+
},
|
|
116
|
+
"atlas_domain_entity": {
|
|
117
|
+
"description": "cartão de entidade de domínio: identidade, "
|
|
118
|
+
"ocorrências, relações e evidência do snapshot",
|
|
119
|
+
"inputSchema": {"type": "object", "properties": {
|
|
120
|
+
"key": {"type": "string",
|
|
121
|
+
"description": "identity_key 'dom:…' | id | "
|
|
122
|
+
"qualified_name"},
|
|
123
|
+
"root": {"type": "string"}},
|
|
124
|
+
"required": ["key"], "additionalProperties": False},
|
|
125
|
+
},
|
|
126
|
+
"atlas_domain_lineage": {
|
|
127
|
+
"description": "linhagem READS/WRITES limitada (bounded BFS) "
|
|
128
|
+
"de uma entidade de dados",
|
|
129
|
+
"inputSchema": {"type": "object", "properties": {
|
|
130
|
+
"entity": {"type": "string",
|
|
131
|
+
"description": "identity_key | id | "
|
|
132
|
+
"qualified_name"},
|
|
133
|
+
"direction": {"type": "string",
|
|
134
|
+
"enum": ["up", "down", "both"]},
|
|
135
|
+
"depth": {"type": "integer", "maximum": 8},
|
|
136
|
+
"limit": {"type": "integer", "maximum": 500},
|
|
137
|
+
"root": {"type": "string"}},
|
|
138
|
+
"required": ["entity"], "additionalProperties": False},
|
|
139
|
+
},
|
|
140
|
+
"atlas_federation_view": {
|
|
141
|
+
"description": "view federada de um workspace "
|
|
142
|
+
"(repo→snapshot resolvido; read-only)",
|
|
143
|
+
"inputSchema": {"type": "object", "properties": {
|
|
144
|
+
"workspace": {"type": "string",
|
|
145
|
+
"description": "nome ou id do workspace"},
|
|
146
|
+
"pins": {"type": "object",
|
|
147
|
+
"description": "{repository_id: snapshot_id} "
|
|
148
|
+
"overrides de pin"}},
|
|
149
|
+
"required": ["workspace"], "additionalProperties": False},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Tools que não ancoram num repo/snapshot (lf-0123): resolvem sem _ctx.
|
|
154
|
+
_GLOBAL_TOOLS = frozenset({"atlas_domains", "atlas_federation_view"})
|
|
155
|
+
|
|
156
|
+
_RESOURCES = (
|
|
157
|
+
("atlas://repository", "repositório indexado"),
|
|
158
|
+
("atlas://snapshot", "snapshot READY atual"),
|
|
159
|
+
("atlas://capabilities", "capabilities do backend de busca"),
|
|
160
|
+
("atlas://graph", "resumo do grafo (counts por tipo de aresta)"),
|
|
161
|
+
("atlas://context", "context pack do snapshot (template)"),
|
|
162
|
+
("atlas://symbols", "símbolos do snapshot (primeira página)"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
_PROMPTS: dict[str, str] = {
|
|
166
|
+
"understand-component": (
|
|
167
|
+
"Entenda {component}: 1) atlas_search pelo nome; 2) "
|
|
168
|
+
"atlas_symbol no melhor hit; 3) atlas_context com a tarefa; "
|
|
169
|
+
"4) cite evidence e qualified_names reais — nunca invente."),
|
|
170
|
+
"locate-implementation": (
|
|
171
|
+
"Localize {name}: atlas_search estratégia hybrid; depois "
|
|
172
|
+
"atlas_symbol no candidato; reporte path+span da ocorrência."),
|
|
173
|
+
"explore-dependencies": (
|
|
174
|
+
"Dependências de {symbol}: atlas_references + "
|
|
175
|
+
"atlas_dependencies; distinga resolved/ambiguous/unresolved."),
|
|
176
|
+
"prepare-safe-change": (
|
|
177
|
+
"Antes de mudar {symbol}: atlas_context level=deep; liste "
|
|
178
|
+
"impactados via atlas_references; preserve evidências."),
|
|
179
|
+
"explain-code-path": (
|
|
180
|
+
"Explique {path}: atlas_expand progressivo; cite arestas "
|
|
181
|
+
"comprovadas; marque o que é inferido vs resolvido."),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class _Ctx:
|
|
187
|
+
conn: sqlite3.Connection
|
|
188
|
+
root: str
|
|
189
|
+
repo_id: int
|
|
190
|
+
snapshot_id: int
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class AtlasMcpServer:
|
|
194
|
+
"""JSON-RPC 2.0 ndjson sobre stdio — read-only."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, db_path: str | None = None) -> None:
|
|
197
|
+
self._db_path = db_path
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------- transporte
|
|
200
|
+
def serve(self, in_f: TextIO, out_f: TextIO) -> int:
|
|
201
|
+
for line in in_f:
|
|
202
|
+
line = line.strip()
|
|
203
|
+
if not line:
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
msg = json.loads(line)
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
self._send(out_f, self._err(None, _ERR_PARSE,
|
|
209
|
+
"JSON inválido"))
|
|
210
|
+
continue
|
|
211
|
+
resp = self._dispatch(msg)
|
|
212
|
+
if resp is not None:
|
|
213
|
+
self._send(out_f, resp)
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
def _send(self, out: TextIO, msg: dict[str, Any]) -> None:
|
|
217
|
+
out.write(json.dumps(msg) + "\n")
|
|
218
|
+
out.flush()
|
|
219
|
+
|
|
220
|
+
def _err(self, rid: Any, code: int, msg: str) -> dict[str, Any]:
|
|
221
|
+
return {"jsonrpc": "2.0", "id": rid,
|
|
222
|
+
"error": {"code": code, "message": msg}}
|
|
223
|
+
|
|
224
|
+
def _ok(self, rid: Any, result: Any) -> dict[str, Any]:
|
|
225
|
+
return {"jsonrpc": "2.0", "id": rid, "result": result}
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------- dispatch
|
|
228
|
+
def _dispatch(self, msg: dict[str, Any]) -> dict[str, Any] | None:
|
|
229
|
+
rid = msg.get("id")
|
|
230
|
+
method = msg.get("method", "")
|
|
231
|
+
params = msg.get("params") or {}
|
|
232
|
+
if method == "initialize":
|
|
233
|
+
return self._ok(rid, {
|
|
234
|
+
"protocolVersion": params.get(
|
|
235
|
+
"protocolVersion", PROTOCOL_VERSION),
|
|
236
|
+
"capabilities": {
|
|
237
|
+
"tools": {}, "resources": {}, "prompts": {}},
|
|
238
|
+
"serverInfo": {"name": SERVER_NAME,
|
|
239
|
+
"version": self._version()}})
|
|
240
|
+
if method == "notifications/initialized":
|
|
241
|
+
return None
|
|
242
|
+
if method == "ping":
|
|
243
|
+
return self._ok(rid, {})
|
|
244
|
+
if method == "tools/list":
|
|
245
|
+
return self._ok(rid, {"tools": [
|
|
246
|
+
{"name": n, **t} for n, t in sorted(_TOOLS.items())]})
|
|
247
|
+
if method == "tools/call":
|
|
248
|
+
return self._tools_call(rid, params)
|
|
249
|
+
if method in ("resources/list", "resources/read",
|
|
250
|
+
"prompts/list", "prompts/get"):
|
|
251
|
+
return self._dispatch_meta(rid, method, params)
|
|
252
|
+
return self._err(rid, _ERR_METHOD, f"método desconhecido: {method}")
|
|
253
|
+
|
|
254
|
+
def _dispatch_meta(
|
|
255
|
+
self, rid: Any, method: str, params: dict[str, Any]
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
if method == "resources/list":
|
|
258
|
+
return self._ok(rid, {"resources": [
|
|
259
|
+
{"uri": u, "name": n, "mimeType": "application/json"}
|
|
260
|
+
for u, n in _RESOURCES]})
|
|
261
|
+
if method == "resources/read":
|
|
262
|
+
return self._resources_read(rid, params)
|
|
263
|
+
if method == "prompts/list":
|
|
264
|
+
return self._ok(rid, {"prompts": [
|
|
265
|
+
{"name": n, "description": t.split(":")[0]}
|
|
266
|
+
for n, t in sorted(_PROMPTS.items())]})
|
|
267
|
+
if method == "prompts/get":
|
|
268
|
+
name = params.get("name", "")
|
|
269
|
+
if name not in _PROMPTS:
|
|
270
|
+
return self._err(rid, _ERR_PARAMS, f"prompt '{name}'?")
|
|
271
|
+
return self._ok(rid, {
|
|
272
|
+
"description": name,
|
|
273
|
+
"messages": [{"role": "user", "content": {
|
|
274
|
+
"type": "text", "text": _PROMPTS[name]}}]})
|
|
275
|
+
return self._err(rid, _ERR_METHOD, f"método desconhecido: {method}")
|
|
276
|
+
|
|
277
|
+
def _ctx(self, params: dict[str, Any]) -> _Ctx:
|
|
278
|
+
from code_atlas.cli import _open_db
|
|
279
|
+
from code_atlas.config import resolve_root
|
|
280
|
+
from code_atlas.search.structural import StructuralSearch
|
|
281
|
+
|
|
282
|
+
# lf-0054: `or "."` sombreava CODE_ATLAS_ROOT — env deve ser
|
|
283
|
+
# consultada antes do cwd (resolve_root já faz essa cadeia).
|
|
284
|
+
root = str(resolve_root(params.get("root")))
|
|
285
|
+
conn = _open_db()
|
|
286
|
+
conn.row_factory = sqlite3.Row
|
|
287
|
+
st = StructuralSearch(conn)
|
|
288
|
+
repo_id = st.repo_for_root(root)
|
|
289
|
+
if repo_id is None:
|
|
290
|
+
conn.close()
|
|
291
|
+
raise LookupError(f"repositório não indexado: {root}")
|
|
292
|
+
snap, err = st.resolve_snapshot(repo_id, None)
|
|
293
|
+
if snap is None or snap.id is None:
|
|
294
|
+
conn.close()
|
|
295
|
+
raise LookupError(err or "nenhum snapshot READY")
|
|
296
|
+
return _Ctx(conn, root, repo_id, int(snap.id))
|
|
297
|
+
|
|
298
|
+
def _version(self) -> str:
|
|
299
|
+
from code_atlas import __version__
|
|
300
|
+
|
|
301
|
+
return __version__
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------- tools
|
|
304
|
+
def _tools_call(
|
|
305
|
+
self, rid: Any, params: dict[str, Any]
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
name = params.get("name", "")
|
|
308
|
+
args = params.get("arguments") or {}
|
|
309
|
+
if name not in _TOOLS:
|
|
310
|
+
return self._err(rid, _ERR_PARAMS, f"tool '{name}'?")
|
|
311
|
+
if not isinstance(args, dict):
|
|
312
|
+
return self._err(rid, _ERR_PARAMS, "arguments deve ser objeto")
|
|
313
|
+
try:
|
|
314
|
+
data = self._run_tool(name, args)
|
|
315
|
+
except LookupError as exc:
|
|
316
|
+
return self._err(rid, _ERR_PARAMS, str(exc))
|
|
317
|
+
except (ValueError, TypeError) as exc:
|
|
318
|
+
return self._err(rid, _ERR_PARAMS, f"parâmetros: {exc}")
|
|
319
|
+
except sqlite3.Error as exc:
|
|
320
|
+
# lf-0054: erro de storage NUNCA pode derrubar o stdio server
|
|
321
|
+
return self._err(rid, _ERR_INTERNAL, f"storage: {exc}")
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
return self._err(rid, _ERR_INTERNAL, f"interno: {exc}")
|
|
324
|
+
return self._ok(rid, {
|
|
325
|
+
"content": [{"type": "text",
|
|
326
|
+
"text": json.dumps(data, default=str)}],
|
|
327
|
+
"structuredContent": data})
|
|
328
|
+
|
|
329
|
+
def _run_tool(self, name: str, args: dict[str, Any]) -> Any:
|
|
330
|
+
from code_atlas.search import SearchEngine, SearchRequest
|
|
331
|
+
|
|
332
|
+
# Tools sem escopo de repo: federação atravessa repositórios e a
|
|
333
|
+
# listagem de adapters nem toca o banco (lf-0123, Phase 15).
|
|
334
|
+
if name in _GLOBAL_TOOLS:
|
|
335
|
+
return self._run_global_tool(name, args)
|
|
336
|
+
|
|
337
|
+
ctx = self._ctx(args)
|
|
338
|
+
try:
|
|
339
|
+
if name == "atlas_search":
|
|
340
|
+
res = SearchEngine(ctx.conn).search(SearchRequest(
|
|
341
|
+
query=str(args["query"]),
|
|
342
|
+
strategy=str(args.get("strategy", "hybrid")),
|
|
343
|
+
repository_root=ctx.root,
|
|
344
|
+
snapshot_id=ctx.snapshot_id,
|
|
345
|
+
limit=min(int(args.get("limit", 20)), 200)))
|
|
346
|
+
return res.to_dict()
|
|
347
|
+
if name == "atlas_context":
|
|
348
|
+
from code_atlas.context import ContextEngine, ContextRequest
|
|
349
|
+
|
|
350
|
+
pack = ContextEngine(ctx.conn).build(ContextRequest(
|
|
351
|
+
task=str(args["task"]),
|
|
352
|
+
repository_root=ctx.root,
|
|
353
|
+
snapshot_id=ctx.snapshot_id,
|
|
354
|
+
level=str(args.get("level", "focus")), # type: ignore[arg-type]
|
|
355
|
+
budget_tokens=min(int(args.get("budget", 4000)),
|
|
356
|
+
100000)))
|
|
357
|
+
return pack.to_dict()
|
|
358
|
+
if name == "atlas_expand":
|
|
359
|
+
import dataclasses
|
|
360
|
+
|
|
361
|
+
from code_atlas.context import ContextExpander
|
|
362
|
+
|
|
363
|
+
exp = ContextExpander(ctx.conn).expand(
|
|
364
|
+
ctx.snapshot_id, str(args["name"]),
|
|
365
|
+
delivered=frozenset(
|
|
366
|
+
s for s in str(
|
|
367
|
+
args.get("delivered", "")).split(",") if s))
|
|
368
|
+
return dataclasses.asdict(exp)
|
|
369
|
+
if name == "atlas_status":
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"root": ctx.root, "repo_id": ctx.repo_id,
|
|
373
|
+
"snapshot_id": ctx.snapshot_id,
|
|
374
|
+
"capabilities": dataclasses_asdict_safe(
|
|
375
|
+
SearchEngine(ctx.conn).capabilities()),
|
|
376
|
+
}
|
|
377
|
+
return self._run_symbol_tool(ctx, name, args)
|
|
378
|
+
finally:
|
|
379
|
+
ctx.conn.close()
|
|
380
|
+
raise LookupError(f"tool '{name}' não implementada")
|
|
381
|
+
|
|
382
|
+
def _run_global_tool(self, name: str, args: dict[str, Any]) -> Any:
|
|
383
|
+
"""Tools que não ancoram num repo/snapshot do caller (lf-0123)."""
|
|
384
|
+
if name == "atlas_domains":
|
|
385
|
+
from code_atlas.domains.cli_surface import (
|
|
386
|
+
NOTE_OPT_IN,
|
|
387
|
+
adapters_report,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return {"adapters": adapters_report(), "note": NOTE_OPT_IN}
|
|
391
|
+
return self._federation_view(args)
|
|
392
|
+
|
|
393
|
+
def _run_symbol_tool(
|
|
394
|
+
self, ctx: _Ctx, name: str, args: dict[str, Any],
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
"""Despacho das tools por qualified_name — cai nas de domínio."""
|
|
397
|
+
if name == "atlas_symbol":
|
|
398
|
+
return self._symbol(ctx, str(args["name"]))
|
|
399
|
+
if name == "atlas_references":
|
|
400
|
+
return self._references(ctx, str(args["name"]))
|
|
401
|
+
if name == "atlas_dependencies":
|
|
402
|
+
return self._dependencies(
|
|
403
|
+
ctx, str(args.get("relation_type") or ""))
|
|
404
|
+
if name == "atlas_evidence":
|
|
405
|
+
return self._evidence(ctx, str(args["name"]))
|
|
406
|
+
return self._run_domain_tool(ctx, name, args)
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------- domains (lf-0123)
|
|
409
|
+
def _run_domain_tool(
|
|
410
|
+
self, ctx: _Ctx, name: str, args: dict[str, Any],
|
|
411
|
+
) -> dict[str, Any]:
|
|
412
|
+
"""Despacho das tools de domínio — cai no impacto como último."""
|
|
413
|
+
if name == "atlas_domain_entity":
|
|
414
|
+
return self._domain_entity(ctx, str(args["key"]))
|
|
415
|
+
if name == "atlas_domain_lineage":
|
|
416
|
+
return self._domain_lineage(ctx, args)
|
|
417
|
+
return self._run_impact_tool(ctx, name, args)
|
|
418
|
+
|
|
419
|
+
def _domain_entity(self, ctx: _Ctx, key: str) -> dict[str, Any]:
|
|
420
|
+
from code_atlas.domains.cli_surface import (
|
|
421
|
+
entity_detail,
|
|
422
|
+
resolve_single_entity,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
entity, err = resolve_single_entity(ctx.conn, ctx.repo_id, key)
|
|
426
|
+
if entity is None:
|
|
427
|
+
raise LookupError(err)
|
|
428
|
+
return entity_detail(ctx.conn, entity, ctx.snapshot_id)
|
|
429
|
+
|
|
430
|
+
def _domain_lineage(
|
|
431
|
+
self, ctx: _Ctx, args: dict[str, Any]
|
|
432
|
+
) -> dict[str, Any]:
|
|
433
|
+
from code_atlas.domains.cli_surface import (
|
|
434
|
+
lineage,
|
|
435
|
+
resolve_single_entity,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
entity, err = resolve_single_entity(
|
|
439
|
+
ctx.conn, ctx.repo_id, str(args["entity"]))
|
|
440
|
+
if entity is None:
|
|
441
|
+
raise LookupError(err)
|
|
442
|
+
return lineage(
|
|
443
|
+
ctx.conn, entity, ctx.snapshot_id,
|
|
444
|
+
direction=str(args.get("direction") or "both"),
|
|
445
|
+
depth=int(args.get("depth") or 2),
|
|
446
|
+
limit=min(500, int(args.get("limit") or 200)))
|
|
447
|
+
|
|
448
|
+
def _federation_view(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
449
|
+
"""``atlas_federation_view`` — view repo→snapshot do workspace."""
|
|
450
|
+
from code_atlas.cli import _open_db
|
|
451
|
+
from code_atlas.federation import Federation, FederationError
|
|
452
|
+
|
|
453
|
+
conn = _open_db()
|
|
454
|
+
try:
|
|
455
|
+
fed = Federation(conn)
|
|
456
|
+
raw = args.get("pins") or {}
|
|
457
|
+
pins: dict[int, int] = {}
|
|
458
|
+
if isinstance(raw, dict):
|
|
459
|
+
pins = {int(k): int(v) for k, v in raw.items()}
|
|
460
|
+
elif isinstance(raw, str) and raw.strip():
|
|
461
|
+
for seg in raw.split(","):
|
|
462
|
+
repo, sep, snap = seg.partition(":")
|
|
463
|
+
if sep and repo.isdigit() and snap.isdigit():
|
|
464
|
+
pins[int(repo)] = int(snap)
|
|
465
|
+
ws_name = str(args.get("workspace") or "")
|
|
466
|
+
if not ws_name:
|
|
467
|
+
raise LookupError("workspace obrigatório")
|
|
468
|
+
try:
|
|
469
|
+
ws = fed.get_workspace(ws_name)
|
|
470
|
+
return fed.resolve_view(ws, pins).to_dict()
|
|
471
|
+
except FederationError as exc:
|
|
472
|
+
raise LookupError(str(exc)) from exc
|
|
473
|
+
finally:
|
|
474
|
+
conn.close()
|
|
475
|
+
|
|
476
|
+
def _symbol(self, ctx: _Ctx, name: str) -> dict[str, Any]:
|
|
477
|
+
rows = ctx.conn.execute(
|
|
478
|
+
"SELECT s.qualified_name, s.kind, s.language, s.id"
|
|
479
|
+
" FROM symbols s WHERE s.repository_id = ? AND"
|
|
480
|
+
" (s.qualified_name = ? OR s.qualified_name LIKE ?)"
|
|
481
|
+
" ORDER BY s.qualified_name LIMIT 20",
|
|
482
|
+
(ctx.repo_id, name, f"%.{name}")).fetchall()
|
|
483
|
+
return {"symbols": [dict(r) for r in rows],
|
|
484
|
+
"snapshot_id": ctx.snapshot_id}
|
|
485
|
+
|
|
486
|
+
def _references(self, ctx: _Ctx, name: str) -> dict[str, Any]:
|
|
487
|
+
rows = ctx.conn.execute(
|
|
488
|
+
"SELECT sa.qualified_name AS src, r.type,"
|
|
489
|
+
" sb.qualified_name AS dst, r.resolution_status,"
|
|
490
|
+
" r.resolver FROM relations r"
|
|
491
|
+
" JOIN symbol_occurrences a ON a.id = r.from_id"
|
|
492
|
+
" LEFT JOIN symbol_occurrences b ON b.id = r.to_id"
|
|
493
|
+
" JOIN symbols sa ON sa.id = a.logical_symbol_id"
|
|
494
|
+
" LEFT JOIN symbols sb ON sb.id = b.logical_symbol_id"
|
|
495
|
+
" WHERE r.snapshot_id = ? AND (sa.qualified_name = ?"
|
|
496
|
+
" OR sb.qualified_name = ?) LIMIT 100",
|
|
497
|
+
(ctx.snapshot_id, name, name)).fetchall()
|
|
498
|
+
return {"references": [dict(r) for r in rows],
|
|
499
|
+
"snapshot_id": ctx.snapshot_id}
|
|
500
|
+
|
|
501
|
+
def _dependencies(
|
|
502
|
+
self, ctx: _Ctx, relation_type: str
|
|
503
|
+
) -> dict[str, Any]:
|
|
504
|
+
base = (
|
|
505
|
+
"SELECT sa.qualified_name AS src, r.type,"
|
|
506
|
+
" sb.qualified_name AS dst FROM relations r"
|
|
507
|
+
" JOIN symbol_occurrences a ON a.id = r.from_id"
|
|
508
|
+
" LEFT JOIN symbol_occurrences b ON b.id = r.to_id"
|
|
509
|
+
" JOIN symbols sa ON sa.id = a.logical_symbol_id"
|
|
510
|
+
" LEFT JOIN symbols sb ON sb.id = b.logical_symbol_id"
|
|
511
|
+
" WHERE r.snapshot_id = ?")
|
|
512
|
+
params: list[Any] = [ctx.snapshot_id]
|
|
513
|
+
if relation_type:
|
|
514
|
+
sql = base + " AND r.type = ? LIMIT 200"
|
|
515
|
+
params.append(relation_type)
|
|
516
|
+
else:
|
|
517
|
+
sql = base + " LIMIT 200"
|
|
518
|
+
rows = ctx.conn.execute(sql, params).fetchall()
|
|
519
|
+
return {"dependencies": [dict(r) for r in rows],
|
|
520
|
+
"snapshot_id": ctx.snapshot_id}
|
|
521
|
+
|
|
522
|
+
def _evidence(self, ctx: _Ctx, name: str) -> dict[str, Any]:
|
|
523
|
+
# lf-0054: colunas reais da tabela evidence (path/span/hash +
|
|
524
|
+
# proveniência v6) — e.kind/e.detail nunca existiram e o erro
|
|
525
|
+
# não-tratado derrubava o processo do servidor.
|
|
526
|
+
# lf-0051: evidência escopada ao símbolo pedido (arquivos com
|
|
527
|
+
# ocorrências dele), nunca o snapshot inteiro sob o nome.
|
|
528
|
+
rows = ctx.conn.execute(
|
|
529
|
+
"SELECT e.id, e.path, e.span, e.content_hash, e.extractor,"
|
|
530
|
+
" e.notes, e.resolver_id, e.evidence_tier,"
|
|
531
|
+
" e.validation_status FROM evidence e"
|
|
532
|
+
" WHERE e.snapshot_id = ? AND e.source_file_id IN ("
|
|
533
|
+
" SELECT DISTINCT o.source_file_id FROM symbol_occurrences o"
|
|
534
|
+
" JOIN symbols s ON s.id = o.logical_symbol_id"
|
|
535
|
+
" WHERE o.snapshot_id = ? AND s.qualified_name = ?)"
|
|
536
|
+
" ORDER BY e.id LIMIT 200",
|
|
537
|
+
(ctx.snapshot_id, ctx.snapshot_id, name)).fetchall()
|
|
538
|
+
# refs canônicas grounded por occurrence do símbolo (lf-0051)
|
|
539
|
+
from code_atlas.search.refs import EvidenceGrounder
|
|
540
|
+
|
|
541
|
+
grounder = EvidenceGrounder(ctx.conn)
|
|
542
|
+
occ_ids = [
|
|
543
|
+
int(r["id"]) for r in ctx.conn.execute(
|
|
544
|
+
"SELECT o.id FROM symbol_occurrences o JOIN symbols s"
|
|
545
|
+
" ON s.id = o.logical_symbol_id"
|
|
546
|
+
" WHERE o.snapshot_id = ? AND s.qualified_name = ?"
|
|
547
|
+
" LIMIT 25",
|
|
548
|
+
(ctx.snapshot_id, name)).fetchall()
|
|
549
|
+
]
|
|
550
|
+
refs: list[dict[str, Any]] = []
|
|
551
|
+
for oid in occ_ids:
|
|
552
|
+
occ = grounder.occurrence_ref(ctx.repo_id, ctx.snapshot_id,
|
|
553
|
+
oid)
|
|
554
|
+
if occ is not None:
|
|
555
|
+
refs.append({"kind": "syntactic-occurrence",
|
|
556
|
+
"repository_id": occ.repository_id,
|
|
557
|
+
"snapshot_id": occ.snapshot_id,
|
|
558
|
+
"occurrence_id": occ.occurrence_id,
|
|
559
|
+
"qualified_name": occ.qualified_name,
|
|
560
|
+
"path": occ.source_path,
|
|
561
|
+
"span": [occ.span_start, occ.span_end],
|
|
562
|
+
"content_hash": occ.content_hash})
|
|
563
|
+
return {"evidence": [dict(r) for r in rows],
|
|
564
|
+
"refs": refs,
|
|
565
|
+
"snapshot_id": ctx.snapshot_id, "for": name}
|
|
566
|
+
|
|
567
|
+
def _run_impact_tool(
|
|
568
|
+
self, ctx: _Ctx, name: str, args: dict[str, Any],
|
|
569
|
+
) -> dict[str, Any]:
|
|
570
|
+
"""Despacho das tools de impacto (Cycle 5)."""
|
|
571
|
+
if name in ("atlas_impact", "atlas_test_impact"):
|
|
572
|
+
return self._impact(
|
|
573
|
+
ctx, args, tests_only=name == "atlas_test_impact")
|
|
574
|
+
if name == "atlas_diff":
|
|
575
|
+
return self._diff(ctx, args)
|
|
576
|
+
raise LookupError(f"tool '{name}' não implementada")
|
|
577
|
+
|
|
578
|
+
def _impact(
|
|
579
|
+
self, ctx: _Ctx, args: dict[str, Any], *, tests_only: bool,
|
|
580
|
+
) -> dict[str, Any]:
|
|
581
|
+
"""atlas_impact/atlas_test_impact — mesmo ImpactEngine da CLI."""
|
|
582
|
+
from code_atlas.impact.contracts import (
|
|
583
|
+
ImpactLimits,
|
|
584
|
+
ImpactRequest,
|
|
585
|
+
)
|
|
586
|
+
from code_atlas.impact.engine import ImpactEngine
|
|
587
|
+
from code_atlas.impact.report import _jsonable
|
|
588
|
+
|
|
589
|
+
req = ImpactRequest(
|
|
590
|
+
repository_root=ctx.root,
|
|
591
|
+
base_revision=str(args.get("base") or ""),
|
|
592
|
+
target_revision=str(args.get("target") or ""),
|
|
593
|
+
symbol=str(args["symbol"]) if args.get("symbol") else None,
|
|
594
|
+
known_only=bool(args.get("known_only")),
|
|
595
|
+
limits=ImpactLimits(
|
|
596
|
+
max_paths=min(200, int(args.get("max_paths") or 50))))
|
|
597
|
+
res = ImpactEngine(ctx.conn).analyze(req)
|
|
598
|
+
if tests_only:
|
|
599
|
+
res = replace(res, symbol_changes=(), contract_changes=(),
|
|
600
|
+
impacts=())
|
|
601
|
+
return _jsonable(res) # type: ignore[return-value]
|
|
602
|
+
|
|
603
|
+
def _diff(
|
|
604
|
+
self, ctx: _Ctx, args: dict[str, Any],
|
|
605
|
+
) -> dict[str, Any]:
|
|
606
|
+
"""atlas_diff — ChangeSet git read-only (sem tocar workspace)."""
|
|
607
|
+
from code_atlas.impact import diff_reader
|
|
608
|
+
from code_atlas.impact.report import _jsonable
|
|
609
|
+
|
|
610
|
+
cs, diags = diff_reader.read_diff(
|
|
611
|
+
Path(ctx.root), str(args["base"]),
|
|
612
|
+
str(args["target"]) if args.get("target") else None)
|
|
613
|
+
return {
|
|
614
|
+
"change_set": _jsonable(cs),
|
|
615
|
+
"diagnostics": [_jsonable(d) for d in diags],
|
|
616
|
+
"snapshot_id": ctx.snapshot_id}
|
|
617
|
+
|
|
618
|
+
# ---------------------------------------------------- resources
|
|
619
|
+
def _resources_read(
|
|
620
|
+
self, rid: Any, params: dict[str, Any]
|
|
621
|
+
) -> dict[str, Any]:
|
|
622
|
+
uri = str(params.get("uri", ""))
|
|
623
|
+
try:
|
|
624
|
+
ctx = self._ctx({})
|
|
625
|
+
except LookupError as exc:
|
|
626
|
+
return self._err(rid, _ERR_PARAMS, str(exc))
|
|
627
|
+
try:
|
|
628
|
+
if uri == "atlas://repository":
|
|
629
|
+
data: Any = {"root": ctx.root, "repo_id": ctx.repo_id}
|
|
630
|
+
elif uri == "atlas://snapshot":
|
|
631
|
+
data = {"snapshot_id": ctx.snapshot_id}
|
|
632
|
+
elif uri == "atlas://capabilities":
|
|
633
|
+
import dataclasses
|
|
634
|
+
|
|
635
|
+
from code_atlas.search import SearchEngine
|
|
636
|
+
|
|
637
|
+
data = dataclasses.asdict(
|
|
638
|
+
SearchEngine(ctx.conn).capabilities())
|
|
639
|
+
elif uri == "atlas://graph":
|
|
640
|
+
row = ctx.conn.execute(
|
|
641
|
+
"SELECT type, COUNT(*) AS c FROM relations"
|
|
642
|
+
" WHERE snapshot_id = ? GROUP BY type",
|
|
643
|
+
(ctx.snapshot_id,)).fetchall()
|
|
644
|
+
data = {"edges_by_type": {r["type"]: r["c"]
|
|
645
|
+
for r in row},
|
|
646
|
+
"snapshot_id": ctx.snapshot_id}
|
|
647
|
+
elif uri == "atlas://symbols":
|
|
648
|
+
rows = ctx.conn.execute(
|
|
649
|
+
"SELECT qualified_name, kind FROM symbols"
|
|
650
|
+
" WHERE repository_id = ? ORDER BY qualified_name"
|
|
651
|
+
" LIMIT 100", (ctx.repo_id,)).fetchall()
|
|
652
|
+
data = {"symbols": [dict(r) for r in rows]}
|
|
653
|
+
elif uri == "atlas://context":
|
|
654
|
+
data = {"hint": "use tools/call atlas_context",
|
|
655
|
+
"snapshot_id": ctx.snapshot_id}
|
|
656
|
+
else:
|
|
657
|
+
return self._err(rid, _ERR_PARAMS,
|
|
658
|
+
f"uri desconhecida: {uri}")
|
|
659
|
+
finally:
|
|
660
|
+
ctx.conn.close()
|
|
661
|
+
return self._ok(rid, {"contents": [{
|
|
662
|
+
"uri": uri, "mimeType": "application/json",
|
|
663
|
+
"text": json.dumps(data, default=str)}]})
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def dataclasses_asdict_safe(obj: Any) -> dict[str, Any]:
|
|
667
|
+
import dataclasses
|
|
668
|
+
|
|
669
|
+
return dataclasses.asdict(obj)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def main_stdio(db_path: str | None = None) -> int:
|
|
673
|
+
"""Entrypoint `code-atlas mcp` — stdio ndjson."""
|
|
674
|
+
return AtlasMcpServer(db_path).serve(sys.stdin, sys.stdout)
|