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
|
@@ -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
|
+
...
|