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.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. 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)