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
@@ -0,0 +1,51 @@
1
+ """Registro de adaptadores: linguagem e extensão → adapter.
2
+
3
+ Linguagens não suportadas retornam ``None`` — nunca exceção. O registry
4
+ padrão cobre Python, TypeScript e TSX (Cycle 2); novos adaptadores se
5
+ registram sem tocar no registry (Open/Closed).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from code_atlas.parsers.contracts import ParserAdapter
11
+
12
+
13
+ class ParserRegistry:
14
+ """Mapa imutável-por-convenção de adaptadores disponíveis."""
15
+
16
+ def __init__(self) -> None:
17
+ self._by_language: dict[str, ParserAdapter] = {}
18
+ self._by_ext: dict[str, ParserAdapter] = {}
19
+
20
+ def register(self, adapter: ParserAdapter) -> None:
21
+ self._by_language[adapter.language] = adapter
22
+ for ext in adapter.extensions:
23
+ self._by_ext[ext.lower()] = adapter
24
+
25
+ def for_language(self, language: str) -> ParserAdapter | None:
26
+ return self._by_language.get(language)
27
+
28
+ def for_path(self, rel_path: str) -> ParserAdapter | None:
29
+ """Adapter pela extensão do caminho (``.tsx`` antes de ``.ts``)."""
30
+ lower = rel_path.lower()
31
+ # extensões multi-part primeiro (.d.ts)
32
+ for ext in sorted(self._by_ext, key=len, reverse=True):
33
+ if lower.endswith(ext):
34
+ return self._by_ext[ext]
35
+ return None
36
+
37
+ @property
38
+ def languages(self) -> tuple[str, ...]:
39
+ return tuple(sorted(self._by_language))
40
+
41
+ @classmethod
42
+ def default(cls) -> ParserRegistry:
43
+ """Registry completo do Cycle 2: python, typescript, tsx."""
44
+ from code_atlas.parsers.languages.python import PythonAdapter
45
+ from code_atlas.parsers.languages.tsx import TsxAdapter
46
+ from code_atlas.parsers.languages.typescript import TypeScriptAdapter
47
+
48
+ reg = cls()
49
+ for adapter in (PythonAdapter(), TypeScriptAdapter(), TsxAdapter()):
50
+ reg.register(adapter)
51
+ return reg
@@ -0,0 +1,118 @@
1
+ """Ponto único de contato com o runtime Tree-sitter.
2
+
3
+ Somente módulos sob ``code_atlas.parsers`` podem importar ``tree_sitter``
4
+ — a fronteira é verificada por teste de arquitetura. Aqui ficam: carga de
5
+ gramáticas, criação de parsers com timeout, coleta de nós problema e
6
+ versões declaradas via ``importlib.metadata``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.metadata
12
+ import threading
13
+ import warnings
14
+ from collections.abc import Callable, Iterator
15
+ from dataclasses import dataclass
16
+
17
+ import tree_sitter
18
+ from tree_sitter import Language, Node, Parser
19
+
20
+ from code_atlas.parsers.contracts import SourceSpan
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class LanguageSpec:
25
+ """Gramática carregada + versões declaradas."""
26
+
27
+ language: Language
28
+ grammar_dist: str # nome do pacote PyPI da gramática
29
+
30
+ @property
31
+ def grammar_version(self) -> str:
32
+ return _pkg_version(self.grammar_dist)
33
+
34
+
35
+ _version_cache: dict[str, str] = {}
36
+ _version_lock = threading.Lock()
37
+
38
+
39
+ def _pkg_version(dist: str) -> str:
40
+ """Versão do pacote — memoizada com lock (thread-safe).
41
+
42
+ ``importlib.metadata.version`` não é segura no primeiro acesso
43
+ concorrente (leitura de METADATA sob threads → OSError); versões
44
+ de pacote são estáveis por processo, então o cache é correto.
45
+ """
46
+ cached = _version_cache.get(dist)
47
+ if cached is None:
48
+ with _version_lock:
49
+ cached = _version_cache.get(dist)
50
+ if cached is None:
51
+ try:
52
+ cached = importlib.metadata.version(dist)
53
+ except importlib.metadata.PackageNotFoundError:
54
+ cached = "unknown"
55
+ _version_cache[dist] = cached
56
+ return cached
57
+
58
+
59
+ def parser_version() -> str:
60
+ """Versão do pacote ``tree-sitter`` instalado."""
61
+ return _pkg_version("tree-sitter")
62
+
63
+
64
+ def load_language(factory: Callable[[], object], grammar_dist: str) -> LanguageSpec:
65
+ """Carrega uma gramática pelo capsule provider do pacote."""
66
+ return LanguageSpec(language=Language(factory()), grammar_dist=grammar_dist)
67
+
68
+
69
+ def parse_bytes(
70
+ spec: LanguageSpec, content: bytes, timeout_us: int
71
+ ) -> tree_sitter.Tree | None:
72
+ """Parse real com deadline; None quando o timeout expira.
73
+
74
+ ``timeout_micros`` é usado apesar de deprecado em 0.25: o caminho
75
+ alternativo (callback read + ``progress_callback``) sofre segfault no
76
+ binding 0.25.2 — registrado como dívida técnica para revisão quando
77
+ a versão do binding subir. Nunca executa o código alvo.
78
+ """
79
+ parser = Parser(spec.language)
80
+ with warnings.catch_warnings():
81
+ warnings.simplefilter("ignore", DeprecationWarning)
82
+ # stub declara read-only; a API real aceita a atribuição (0.25.x)
83
+ parser.timeout_micros = timeout_us # type: ignore[misc]
84
+ return parser.parse(content)
85
+
86
+
87
+ def span_of(node: Node) -> SourceSpan:
88
+ """Converte Node do Tree-sitter para SourceSpan (linha 1-based)."""
89
+ return SourceSpan(
90
+ start_byte=node.start_byte,
91
+ end_byte=node.end_byte,
92
+ start_line=node.start_point.row + 1,
93
+ start_col=node.start_point.column,
94
+ end_line=node.end_point.row + 1,
95
+ end_col=node.end_point.column,
96
+ )
97
+
98
+
99
+ def iter_problem_nodes(root: Node, limit: int = 50) -> Iterator[tuple[Node, bool]]:
100
+ """Percorre a árvore coletando (nó, is_missing) de ERROR/MISSING.
101
+
102
+ Bounded por ``limit`` para não produzir diagnósticos gigantescos.
103
+ """
104
+ count = 0
105
+ stack = [root]
106
+ while stack and count < limit:
107
+ node = stack.pop()
108
+ if node.is_error or node.is_missing:
109
+ yield node, node.is_missing
110
+ count += 1
111
+ # filhos ao final da pilha preservam ordem de visitação aproximada
112
+ stack.extend(reversed(node.children))
113
+ return
114
+
115
+
116
+ def node_count(root: Node) -> int:
117
+ """Total de nós da árvore (métrica de diagnóstico)."""
118
+ return root.descendant_count + 1
@@ -0,0 +1,33 @@
1
+ """Subsistema de resolução semântica (Cycle 3 Phase 1).
2
+
3
+ Independente de parsers, storage e CLI: cada ``SemanticResolver`` declara
4
+ capabilities e proveniência; o ``ResolverCoordinator`` encadeia por ordem
5
+ de evidência (índice semântico > LSP > sintático > heurístico).
6
+ """
7
+
8
+ from code_atlas.resolvers.capabilities import EvidenceTier, ResolverCapability
9
+ from code_atlas.resolvers.contracts import (
10
+ ResolutionDiagnostic,
11
+ ResolutionRequest,
12
+ ResolutionResult,
13
+ ResolvedDefinition,
14
+ ResolvedReference,
15
+ ResolverProvenance,
16
+ SemanticResolver,
17
+ )
18
+ from code_atlas.resolvers.coordinator import ResolverCoordinator
19
+ from code_atlas.resolvers.registry import ResolverRegistry
20
+
21
+ __all__ = [
22
+ "EvidenceTier",
23
+ "ResolutionDiagnostic",
24
+ "ResolutionRequest",
25
+ "ResolutionResult",
26
+ "ResolvedDefinition",
27
+ "ResolvedReference",
28
+ "ResolverCapability",
29
+ "ResolverCoordinator",
30
+ "ResolverProvenance",
31
+ "ResolverRegistry",
32
+ "SemanticResolver",
33
+ ]
@@ -0,0 +1,246 @@
1
+ """Bootstrap de resolvers opt-in a partir de ``config.toml`` (lf-0027).
2
+
3
+ Tabela suportada (tudo opcional; ausente = nenhum resolver externo):
4
+
5
+ .. code-block:: toml
6
+
7
+ [resolvers.scip]
8
+ index_path = "index.scip" # relativo ao workspace ou absoluto
9
+ max_bytes = 67108864 # override opcional
10
+ allow_revision_mismatch = false # opt-in explícito
11
+
12
+ [resolvers.lsp.servers.pyright]
13
+ command = ["pyright-langserver", "--stdio"]
14
+ languages = ["python"]
15
+ enabled = true
16
+ request_timeout_s = 10.0
17
+
18
+ O bootstrap NUNCA executa nada do repositório indexado e NUNCA baixa
19
+ indexers/servers — ``command`` vem exclusivamente da config do usuário.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable, Mapping
25
+ from pathlib import Path
26
+
27
+ from code_atlas.resolvers.contracts import SemanticResolver
28
+ from code_atlas.resolvers.lsp.adapter import LspResolver
29
+ from code_atlas.resolvers.lsp.lifecycle import make_client
30
+ from code_atlas.resolvers.lsp.spawn import LspServerSpec
31
+ from code_atlas.resolvers.scip.loader import load_index
32
+ from code_atlas.resolvers.scip.provenance import assess_revision
33
+ from code_atlas.resolvers.scip.resolver import ScipResolver
34
+ from code_atlas.resolvers.syntactic.resolver import ScopeUniverse
35
+
36
+ ResolverFactory = Callable[[ScopeUniverse], SemanticResolver]
37
+
38
+
39
+ def resolver_factories(
40
+ table: Mapping[str, object] | None,
41
+ workspace_root: Path,
42
+ versions_out: dict[str, str] | None = None,
43
+ ) -> tuple[list[ResolverFactory], list[str]]:
44
+ """Traduz a tabela ``[resolvers]`` em fábricas + erros tipados.
45
+
46
+ Cada fábrica recebe o ``ScopeUniverse`` do build atual e devolve o
47
+ resolver concreto; falha dentro da fábrica vira diagnóstico no
48
+ enriquecimento (nunca crash).
49
+ """
50
+ factories: list[ResolverFactory] = []
51
+ errors: list[str] = []
52
+ if not table:
53
+ return factories, errors
54
+ scip = table.get("scip")
55
+ if isinstance(scip, Mapping):
56
+ _scip_factory(scip, workspace_root, factories, errors,
57
+ versions_out)
58
+ lsp = table.get("lsp")
59
+ if isinstance(lsp, Mapping):
60
+ _lsp_factories(lsp, workspace_root, factories, errors)
61
+ return factories, errors
62
+
63
+
64
+ def describe_resolvers(
65
+ table: Mapping[str, object] | None,
66
+ workspace_root: Path,
67
+ ) -> tuple[list[dict[str, object]], list[str]]:
68
+ """lf-0032: relatório installed/enabled por resolver — read-only.
69
+
70
+ ``installed`` = artefato presente (índice SCIP carrega; binário
71
+ LSP existe no PATH). ``enabled`` = produziria resolver ativo
72
+ (assessment não-rejected para SCIP; ``enabled=true`` para LSP).
73
+ Nenhum server LSP é spawnado aqui — ``shutil.which`` apenas.
74
+ """
75
+ import shutil
76
+
77
+ reports: list[dict[str, object]] = [
78
+ {
79
+ "id": "syntactic-scope-v1", "kind": "builtin",
80
+ "installed": True, "enabled": True,
81
+ "detail": "tree-sitter + scope analysis",
82
+ }
83
+ ]
84
+ errors: list[str] = []
85
+ scip = table.get("scip") if isinstance(table, Mapping) else None
86
+ if isinstance(scip, Mapping):
87
+ _describe_scip(scip, workspace_root, reports, errors)
88
+ lsp = table.get("lsp") if isinstance(table, Mapping) else None
89
+ if isinstance(lsp, Mapping):
90
+ servers = lsp.get("servers")
91
+ if isinstance(servers, Mapping):
92
+ for name, spec in sorted(servers.items()):
93
+ if not isinstance(spec, Mapping):
94
+ errors.append(
95
+ f"resolvers.lsp.servers.{name}: spec inválido")
96
+ continue
97
+ cmd = spec.get("command")
98
+ binary = cmd[0] if isinstance(cmd, (list, tuple)) and cmd else None
99
+ reports.append({
100
+ "id": f"lsp:{name}", "kind": "lsp",
101
+ "command": " ".join(str(c) for c in cmd) if cmd else "",
102
+ "installed": bool(
103
+ binary and shutil.which(str(binary))),
104
+ "enabled": bool(spec.get("enabled", False)),
105
+ })
106
+ return reports, errors
107
+
108
+
109
+ def _describe_scip(
110
+ scip: Mapping[str, object],
111
+ root: Path,
112
+ reports: list[dict[str, object]],
113
+ errors: list[str],
114
+ ) -> None:
115
+ index_path = scip.get("index_path")
116
+ if not index_path or not isinstance(index_path, str):
117
+ errors.append("resolvers.scip: index_path ausente ou inválido")
118
+ return
119
+ path = Path(index_path)
120
+ if not path.is_absolute():
121
+ path = root / path
122
+ kwargs: dict[str, object] = {
123
+ "workspace_root": root,
124
+ "allow_revision_mismatch": bool(
125
+ scip.get("allow_revision_mismatch", False)),
126
+ }
127
+ max_bytes = scip.get("max_bytes")
128
+ if isinstance(max_bytes, int):
129
+ kwargs["max_bytes"] = max_bytes
130
+ load = load_index(path, **kwargs) # type: ignore[arg-type]
131
+ installed = load.ok and load.index is not None
132
+ rev_status = "not-assessed"
133
+ enabled = False
134
+ if installed and load.index is not None:
135
+ a = assess_revision(load.index, path, root)
136
+ rev_status = a.provenance.validation_status
137
+ enabled = not a.errors
138
+ if a.provenance.validation_status == "degraded":
139
+ errors.append(
140
+ f"scip index {path}: degraded — "
141
+ f"{'; '.join(a.provenance.validation_evidence[:3])}")
142
+ else:
143
+ errors.append(
144
+ f"scip index {path}: "
145
+ f"{'; '.join(load.errors) or 'load failed'}")
146
+ reports.append({
147
+ "id": "scip", "kind": "scip",
148
+ "index_path": str(path),
149
+ "installed": installed, "enabled": enabled,
150
+ "revision_status": rev_status,
151
+ })
152
+
153
+
154
+ def _scip_factory(
155
+ scip: Mapping[str, object],
156
+ root: Path,
157
+ factories: list[ResolverFactory],
158
+ errors: list[str],
159
+ versions_out: dict[str, str] | None = None,
160
+ ) -> None:
161
+ index_path = scip.get("index_path")
162
+ if not index_path or not isinstance(index_path, str):
163
+ errors.append("resolvers.scip: index_path ausente ou inválido")
164
+ return
165
+ path = Path(index_path)
166
+ if not path.is_absolute():
167
+ path = root / path
168
+ allow_mismatch = bool(scip.get("allow_revision_mismatch", False))
169
+ max_bytes = scip.get("max_bytes")
170
+ kwargs: dict[str, object] = {
171
+ "workspace_root": root,
172
+ "allow_revision_mismatch": allow_mismatch,
173
+ }
174
+ if isinstance(max_bytes, int):
175
+ kwargs["max_bytes"] = max_bytes
176
+ out = load_index(path, **kwargs) # type: ignore[arg-type]
177
+ if not out.ok or out.index is None:
178
+ errors.append(
179
+ f"scip index {path}: {'; '.join(out.errors) or 'load failed'}"
180
+ )
181
+ return
182
+ index = out.index
183
+ # Revision safety (lf-0028): índice válido ≠ índice compatível.
184
+ # Rejected → erro e nenhuma factory; degraded → resolver marcado,
185
+ # nunca promove como comprovado.
186
+ assessment = assess_revision(index, path, root)
187
+ prov = assessment.provenance
188
+ if assessment.errors:
189
+ errors.append(
190
+ f"scip index {path}: {'; '.join(assessment.errors)}"
191
+ )
192
+ return
193
+ if prov.validation_status == "degraded":
194
+ errors.append(
195
+ f"scip index {path}: degraded — "
196
+ f"{'; '.join(prov.validation_evidence[:3])}"
197
+ )
198
+ if versions_out is not None:
199
+ # sha256 do índice na identidade do index — índice SCIP novo
200
+ # invalida relações (lf-0030 phase 6).
201
+ versions_out["resolver:scip"] = prov.index_sha256[:16]
202
+ factories.append(
203
+ lambda universe: ScipResolver(index, universe, prov)
204
+ )
205
+
206
+
207
+ def _lsp_factories(
208
+ lsp: Mapping[str, object],
209
+ root: Path,
210
+ factories: list[ResolverFactory],
211
+ errors: list[str],
212
+ ) -> None:
213
+ servers = lsp.get("servers")
214
+ if not isinstance(servers, Mapping):
215
+ return
216
+ for name, spec in servers.items():
217
+ if not isinstance(spec, Mapping):
218
+ errors.append(f"resolvers.lsp.servers.{name}: spec inválido")
219
+ continue
220
+ cmd = spec.get("command")
221
+ if not isinstance(cmd, (list, tuple)) or not cmd:
222
+ errors.append(
223
+ f"resolvers.lsp.servers.{name}: command ausente/inválido"
224
+ )
225
+ continue
226
+ languages = spec.get("languages", ())
227
+ server = LspServerSpec(
228
+ name=str(name),
229
+ cmd=tuple(str(c) for c in cmd),
230
+ languages=frozenset(str(lang) for lang in languages)
231
+ if isinstance(languages, (list, tuple))
232
+ else frozenset(),
233
+ root_uri=root.as_uri(),
234
+ enabled=bool(spec.get("enabled", False)),
235
+ request_timeout_s=float(spec.get("request_timeout_s", 10.0)),
236
+ )
237
+ if not server.enabled:
238
+ continue
239
+ def _factory(universe: ScopeUniverse,
240
+ _server: LspServerSpec = server) -> SemanticResolver:
241
+ client = make_client(_server)
242
+ if client is None:
243
+ raise RuntimeError(f"lsp {_server.name}: disabled")
244
+ return LspResolver(client, universe)
245
+
246
+ factories.append(_factory)
@@ -0,0 +1,33 @@
1
+ """Capabilities e tiers de evidência do subsistema de resolução."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from dataclasses import dataclass
7
+
8
+
9
+ class EvidenceTier(enum.IntEnum):
10
+ """Ordem de precedência de evidência (prompt Cycle 3 Phase 1)."""
11
+
12
+ SEMANTIC_INDEX = 1 # SCIP e afins — índice externo confiável
13
+ LSP = 2 # servidor de linguagem confirmado
14
+ SYNTACTIC = 3 # resolução sintática inequívoca
15
+ HEURISTIC = 4 # candidato heurístico
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ResolverCapability:
20
+ """Declaração do que um resolvedor cobre."""
21
+
22
+ resolver_id: str
23
+ tier: EvidenceTier
24
+ languages: frozenset[str] # vazio = qualquer linguagem
25
+ relation_types: frozenset[str] # IMPORTS/CALLS/REFERENCES/...
26
+ needs_external_process: bool = False
27
+ needs_network: bool = False
28
+ detail: str = ""
29
+
30
+ def covers(self, language: str, relation_type: str) -> bool:
31
+ lang_ok = not self.languages or language in self.languages
32
+ rel_ok = not self.relation_types or relation_type in self.relation_types
33
+ return lang_ok and rel_ok
@@ -0,0 +1,133 @@
1
+ """Contratos versionados do subsistema de resolução (lf-0016).
2
+
3
+ Ordem conceitual de evidência (prompt Cycle 3 §Phase 1):
4
+
5
+ 1. Índice semântico confiável, consistente com a revisão (SCIP).
6
+ 2. LSP com definição/referência confirmada.
7
+ 3. Resolução sintática inequívoca.
8
+ 4. Candidato heurístico.
9
+ 5. Alvo ambíguo ou não resolvido.
10
+
11
+ ``confidence_class`` e ``resolution_status`` permanecem conceitos
12
+ distintos: o primeiro descreve a força da evidência do resolvedor, o
13
+ segundo o desfecho da tentativa (resolved/ambiguous/unresolved/external).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import Protocol, runtime_checkable
20
+
21
+ from code_atlas.parsers.contracts import SourceSpan
22
+ from code_atlas.resolvers.capabilities import ResolverCapability
23
+
24
+ RESOLUTION_SCHEMA_VERSION = 1
25
+
26
+ # Buckets de kind aceitos pelo contrato — adapters SCIP/LSP/syntactic
27
+ # mapeiam seus pedidos para este vocabulário.
28
+ REQUEST_KINDS = frozenset(
29
+ {"call", "import", "type-ref", "reference", "definition"}
30
+ )
31
+
32
+ # kind do request → tipo de relação das capabilities (Registry filtra
33
+ # resolvers por este vocabulário de grafo).
34
+ KIND_TO_RELATION: dict[str, str] = {
35
+ "call": "CALLS",
36
+ "import": "IMPORTS",
37
+ "type-ref": "REFERENCES",
38
+ "reference": "REFERENCES",
39
+ "definition": "REFERENCES",
40
+ }
41
+
42
+
43
+ def relation_for_kind(kind: str) -> str:
44
+ """Mapeia kind de request → relation type; desconhecido → o próprio
45
+ kind em maiúsculas (resolvers podem declarar tipos extras)."""
46
+ return KIND_TO_RELATION.get(kind, kind.upper())
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ResolverProvenance:
51
+ """Origem verificável de um resultado de resolução."""
52
+
53
+ resolver_id: str # ex.: "syntactic", "scip", "lsp:pyright"
54
+ resolver_version: str # versão do resolvedor (semver interno)
55
+ evidence_tier: int # EvidenceTier — 1=semantic-index…4=heuristic
56
+ detail: str = "" # indexer/server de origem, grammar, etc.
57
+ index_hash: str = "" # sha256 do índice externo (lf-0035), se houver
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ResolutionRequest:
62
+ """Pedido de resolução de um símbolo/referência textual."""
63
+
64
+ name: str # texto a resolver (callee, nome, import)
65
+ kind: str # call | import | type-ref | reference | definition
66
+ path: str # arquivo de origem (relativo ao repo)
67
+ language: str = ""
68
+ span: SourceSpan | None = None
69
+ module: str = "" # qname do módulo de origem, se conhecido
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ResolvedReference:
74
+ """Resultado de uma resolução de referência/call/import."""
75
+
76
+ status: str # resolved|ambiguous|unresolved|external
77
+ confidence_class: str # resolved|syntactic|heuristic|inferred
78
+ provenance: ResolverProvenance
79
+ target_id: str | None = None # node_id interno quando resolvido
80
+ target_text: str = "" # texto preservado do alvo
81
+ candidates: tuple[str, ...] = () # alvos concorrentes quando ambiguous
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ResolvedDefinition:
86
+ """Definição localizada de um símbolo (definition site)."""
87
+
88
+ symbol: str
89
+ path: str
90
+ provenance: ResolverProvenance
91
+ span: SourceSpan | None = None
92
+ kind: str = ""
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class ResolutionDiagnostic:
97
+ """Diagnóstico de um resolvedor (falha, timeout, rejeição)."""
98
+
99
+ code: str
100
+ severity: str # info | warning | error
101
+ message: str
102
+ resolver_id: str = ""
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class ResolutionResult:
107
+ """Envelope versionado do resultado de ``SemanticResolver.resolve``."""
108
+
109
+ request: ResolutionRequest
110
+ schema_version: int = RESOLUTION_SCHEMA_VERSION
111
+ reference: ResolvedReference | None = None
112
+ definitions: tuple[ResolvedDefinition, ...] = ()
113
+ diagnostics: tuple[ResolutionDiagnostic, ...] = field(default_factory=tuple)
114
+
115
+
116
+ @runtime_checkable
117
+ class SemanticResolver(Protocol):
118
+ """Interface implementada por qualquer fonte de resolução."""
119
+
120
+ @property
121
+ def resolver_id(self) -> str: ...
122
+
123
+ @property
124
+ def resolver_version(self) -> str: ...
125
+
126
+ def capabilities(self) -> ResolverCapability:
127
+ """O que este resolvedor cobre (linguagens, relações, tier)."""
128
+ ...
129
+
130
+ def resolve(self, request: ResolutionRequest) -> ResolutionResult:
131
+ """Tenta resolver o pedido; nunca levanta — falhas viram
132
+ ``ResolutionDiagnostic`` no resultado."""
133
+ ...