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,96 @@
|
|
|
1
|
+
"""Coordenador de resolução — cadeia por precedência de evidência.
|
|
2
|
+
|
|
3
|
+
Política (documentada): itera resolvedores em ordem de ``EvidenceTier``
|
|
4
|
+
crescente (índice semântico → LSP → sintático → heurístico). Para no
|
|
5
|
+
primeiro resultado ``resolved`` inequívoco — tiers mais baixos não
|
|
6
|
+
aprimoram um resultado já confirmado. Se nenhum resolver devolve
|
|
7
|
+
``resolved``, o resultado do **tier mais alto disponível** é devolvido
|
|
8
|
+
com seu status honesto (ambiguous/unresolved/external) — nunca promovido.
|
|
9
|
+
Exceções de resolvedor viram diagnóstico, não crash.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from code_atlas.resolvers.contracts import (
|
|
15
|
+
ResolutionDiagnostic,
|
|
16
|
+
ResolutionRequest,
|
|
17
|
+
ResolutionResult,
|
|
18
|
+
SemanticResolver,
|
|
19
|
+
relation_for_kind,
|
|
20
|
+
)
|
|
21
|
+
from code_atlas.resolvers.registry import ResolverRegistry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResolverCoordinator:
|
|
25
|
+
"""Encadeia resolvers registrados por ordem de evidência."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, registry: ResolverRegistry) -> None:
|
|
28
|
+
self._registry = registry
|
|
29
|
+
|
|
30
|
+
def candidates(
|
|
31
|
+
self, request: ResolutionRequest
|
|
32
|
+
) -> list[SemanticResolver]:
|
|
33
|
+
"""Resolvers que cobrem o pedido, em ordem de evidência."""
|
|
34
|
+
rel = relation_for_kind(request.kind)
|
|
35
|
+
return self._registry.capable(request.language, rel)
|
|
36
|
+
|
|
37
|
+
def resolve(
|
|
38
|
+
self,
|
|
39
|
+
request: ResolutionRequest,
|
|
40
|
+
*,
|
|
41
|
+
above_tier: int | None = None,
|
|
42
|
+
) -> ResolutionResult:
|
|
43
|
+
"""Resolve seguindo a cadeia de evidência; nunca levanta.
|
|
44
|
+
|
|
45
|
+
``above_tier`` (lf-0035): quando informado, só consulta
|
|
46
|
+
resolvedores com ``tier < above_tier`` — usado para verificar
|
|
47
|
+
uma resolução existente contra evidência estritamente superior
|
|
48
|
+
(política de precedência determinística)."""
|
|
49
|
+
diagnostics: list[ResolutionDiagnostic] = []
|
|
50
|
+
best: ResolutionResult | None = None
|
|
51
|
+
for resolver in self.candidates(request):
|
|
52
|
+
if (
|
|
53
|
+
above_tier is not None
|
|
54
|
+
and resolver.capabilities().tier >= above_tier
|
|
55
|
+
):
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
result = resolver.resolve(request)
|
|
59
|
+
except Exception as exc: # resolvedor isolado → diagnóstico
|
|
60
|
+
diagnostics.append(
|
|
61
|
+
ResolutionDiagnostic(
|
|
62
|
+
code="resolver-error",
|
|
63
|
+
severity="warning",
|
|
64
|
+
message=f"{resolver.resolver_id}: {exc}",
|
|
65
|
+
resolver_id=resolver.resolver_id,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
diagnostics.extend(result.diagnostics)
|
|
70
|
+
ref = result.reference
|
|
71
|
+
if ref is not None and ref.status == "resolved":
|
|
72
|
+
return ResolutionResult(
|
|
73
|
+
request=request,
|
|
74
|
+
reference=ref,
|
|
75
|
+
diagnostics=tuple(diagnostics),
|
|
76
|
+
)
|
|
77
|
+
if best is None:
|
|
78
|
+
best = result # tier mais alto que respondeu
|
|
79
|
+
if best is not None:
|
|
80
|
+
return ResolutionResult(
|
|
81
|
+
request=request,
|
|
82
|
+
reference=best.reference,
|
|
83
|
+
definitions=best.definitions,
|
|
84
|
+
diagnostics=tuple(diagnostics),
|
|
85
|
+
)
|
|
86
|
+
return ResolutionResult(
|
|
87
|
+
request=request,
|
|
88
|
+
diagnostics=tuple(diagnostics) or (
|
|
89
|
+
ResolutionDiagnostic(
|
|
90
|
+
code="no-resolver",
|
|
91
|
+
severity="warning",
|
|
92
|
+
message=f"no resolver covers '{request.kind}' "
|
|
93
|
+
f"for '{request.language or '*'}'",
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Enriquecimento semântico do grafo construído (lf-0027).
|
|
2
|
+
|
|
3
|
+
Camada independente entre ``GraphBuilder.build`` e ``persist_graph``:
|
|
4
|
+
re-resolve arestas não-resolvidas via ``ResolverCoordinator`` (ordem de
|
|
5
|
+
``EvidenceTier``: scip → lsp → …) e promove a aresta SOMENTE quando o
|
|
6
|
+
resultado é ``resolved`` e o alvo existe no snapshot atual.
|
|
7
|
+
|
|
8
|
+
Regras invioláveis:
|
|
9
|
+
|
|
10
|
+
- Uma aresta já ``resolved`` nunca é reescrita silenciosamente — só
|
|
11
|
+
pode ser VERIFICADA por evidência de tier estritamente superior
|
|
12
|
+
(lf-0035): mesmo alvo confirma e sobe a proveniência; alvo divergente
|
|
13
|
+
vira ``ambiguous`` + diagnóstico de conflito, preservando o alvo
|
|
14
|
+
original. Evidência externa nunca sobrescreve relação válida.
|
|
15
|
+
- Resultado não-``resolved`` (ambiguous/unresolved/external/diagnóstico)
|
|
16
|
+
preserva a aresta original verbatim — nenhuma degradação.
|
|
17
|
+
- O ``target_id`` retornado por um resolver precisa ser nó persistível
|
|
18
|
+
DESTE build — vínculo cross-snapshot é impossível por construção.
|
|
19
|
+
- Projeção documentada: a primeira evidência ``resolved`` no tier mais
|
|
20
|
+
alto vence; demais respostas ficam como diagnósticos (coexistência
|
|
21
|
+
de evidências alternativas fica registrada, não projetada).
|
|
22
|
+
- Exceções de resolver viram ``ParseDiagnostic`` — nunca crash.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import dataclasses
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
|
|
30
|
+
from code_atlas.contracts.enums import NodeKind
|
|
31
|
+
from code_atlas.parsers.contracts import (
|
|
32
|
+
EvidenceRecord,
|
|
33
|
+
GraphBuildResult,
|
|
34
|
+
GraphEdge,
|
|
35
|
+
ParseDiagnostic,
|
|
36
|
+
)
|
|
37
|
+
from code_atlas.resolvers.capabilities import EvidenceTier
|
|
38
|
+
from code_atlas.resolvers.contracts import (
|
|
39
|
+
ResolutionRequest,
|
|
40
|
+
ResolutionResult,
|
|
41
|
+
)
|
|
42
|
+
from code_atlas.resolvers.coordinator import ResolverCoordinator
|
|
43
|
+
|
|
44
|
+
# Tipos de aresta elegíveis a enriquecimento → kind do ResolutionRequest.
|
|
45
|
+
_EDGE_TO_KIND = {
|
|
46
|
+
"CALLS": "call",
|
|
47
|
+
"IMPORTS": "import",
|
|
48
|
+
"EXTENDS": "type-ref",
|
|
49
|
+
"IMPLEMENTS": "type-ref",
|
|
50
|
+
"REFERENCES": "reference",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Kinds cujos nós viram ocorrências persistidas (espelha _PERSISTED_KINDS
|
|
54
|
+
# de indexing/persist — manter alinhado: um alvo fora deste conjunto
|
|
55
|
+
# seria descartado no persist, perdendo a aresta original).
|
|
56
|
+
_NON_PERSISTED = frozenset({NodeKind.REPOSITORY, NodeKind.PACKAGE})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _edge_tier(resolver_label: str) -> int:
|
|
60
|
+
"""Tier da evidência que produziu a resolução atual (lf-0035).
|
|
61
|
+
|
|
62
|
+
Labels persistidos: ``scip`` → índice semântico, ``lsp:*`` → LSP,
|
|
63
|
+
qualquer outro (``syntactic-scope-v1``, ``tree-sitter-*``) →
|
|
64
|
+
sintático. Desconhecido = sintático (conservador, nunca superestima).
|
|
65
|
+
"""
|
|
66
|
+
if resolver_label == "scip" or resolver_label.startswith("resolver:scip"):
|
|
67
|
+
return int(EvidenceTier.SEMANTIC_INDEX)
|
|
68
|
+
if resolver_label.startswith("lsp:"):
|
|
69
|
+
return int(EvidenceTier.LSP)
|
|
70
|
+
return int(EvidenceTier.SYNTACTIC)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SemanticEnricher:
|
|
74
|
+
"""Promove arestas não-resolvidas com evidência de resolvers opt-in."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, coordinator: ResolverCoordinator) -> None:
|
|
77
|
+
self._coord = coordinator
|
|
78
|
+
|
|
79
|
+
def enrich(
|
|
80
|
+
self, build: GraphBuildResult
|
|
81
|
+
) -> tuple[GraphBuildResult, dict[str, int]]:
|
|
82
|
+
"""Devolve (build possivelmente enriquecido, stats).
|
|
83
|
+
|
|
84
|
+
Stats: ``semantic_attempted``, ``semantic_enriched``,
|
|
85
|
+
``semantic_diagnostics``.
|
|
86
|
+
"""
|
|
87
|
+
node_by_id = {n.node_id: n for n in build.nodes}
|
|
88
|
+
persistable = {
|
|
89
|
+
n.node_id for n in build.nodes
|
|
90
|
+
if n.kind not in _NON_PERSISTED and n.path
|
|
91
|
+
}
|
|
92
|
+
evidence: list[EvidenceRecord] = list(build.evidence)
|
|
93
|
+
diagnostics: list[ParseDiagnostic] = list(build.diagnostics)
|
|
94
|
+
stats = {
|
|
95
|
+
"semantic_attempted": 0,
|
|
96
|
+
"semantic_enriched": 0,
|
|
97
|
+
"semantic_verified": 0,
|
|
98
|
+
"semantic_confirmed": 0,
|
|
99
|
+
"semantic_conflicts": 0,
|
|
100
|
+
"semantic_diagnostics": 0,
|
|
101
|
+
}
|
|
102
|
+
edges: list[GraphEdge] = []
|
|
103
|
+
for e in build.edges:
|
|
104
|
+
new_edge = self._enrich_edge(
|
|
105
|
+
e, node_by_id, persistable, evidence, diagnostics, stats
|
|
106
|
+
)
|
|
107
|
+
edges.append(new_edge)
|
|
108
|
+
stats["semantic_diagnostics"] = (
|
|
109
|
+
len(diagnostics) - len(build.diagnostics)
|
|
110
|
+
)
|
|
111
|
+
enriched = dataclasses.replace(
|
|
112
|
+
build,
|
|
113
|
+
edges=tuple(edges),
|
|
114
|
+
evidence=tuple(evidence),
|
|
115
|
+
diagnostics=tuple(diagnostics),
|
|
116
|
+
)
|
|
117
|
+
return enriched, stats
|
|
118
|
+
|
|
119
|
+
# -- por aresta ------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _enrich_edge(
|
|
122
|
+
self,
|
|
123
|
+
e: GraphEdge,
|
|
124
|
+
node_by_id: Mapping[str, object],
|
|
125
|
+
persistable: set[str],
|
|
126
|
+
evidence: list[EvidenceRecord],
|
|
127
|
+
diagnostics: list[ParseDiagnostic],
|
|
128
|
+
stats: dict[str, int],
|
|
129
|
+
) -> GraphEdge:
|
|
130
|
+
kind = _EDGE_TO_KIND.get(str(e.type))
|
|
131
|
+
if kind is None:
|
|
132
|
+
return e
|
|
133
|
+
if e.resolution_status == "resolved":
|
|
134
|
+
return self._verify_edge(
|
|
135
|
+
e, kind, node_by_id, persistable, evidence,
|
|
136
|
+
diagnostics, stats,
|
|
137
|
+
)
|
|
138
|
+
src = node_by_id.get(e.from_id)
|
|
139
|
+
language = getattr(src, "language", "") if src is not None else ""
|
|
140
|
+
request = ResolutionRequest(
|
|
141
|
+
name=e.to_text,
|
|
142
|
+
kind=kind,
|
|
143
|
+
path=e.source_path,
|
|
144
|
+
language=str(language),
|
|
145
|
+
span=e.span,
|
|
146
|
+
)
|
|
147
|
+
if not request.name:
|
|
148
|
+
return e
|
|
149
|
+
stats["semantic_attempted"] += 1
|
|
150
|
+
result = self._coord.resolve(request)
|
|
151
|
+
self._diags(result, e, diagnostics)
|
|
152
|
+
ref = result.reference
|
|
153
|
+
if (
|
|
154
|
+
ref is None
|
|
155
|
+
or ref.status != "resolved"
|
|
156
|
+
or ref.target_id is None
|
|
157
|
+
or ref.target_id not in persistable
|
|
158
|
+
):
|
|
159
|
+
return e
|
|
160
|
+
prov = ref.provenance
|
|
161
|
+
ev_idx = len(evidence)
|
|
162
|
+
evidence.append(
|
|
163
|
+
EvidenceRecord(
|
|
164
|
+
path=e.source_path,
|
|
165
|
+
span=e.span,
|
|
166
|
+
content_hash="",
|
|
167
|
+
extractor=f"resolver:{prov.resolver_id}",
|
|
168
|
+
extractor_version=prov.resolver_version,
|
|
169
|
+
notes=(
|
|
170
|
+
f"semantic enrichment tier={prov.evidence_tier} "
|
|
171
|
+
f"{prov.detail}".strip()
|
|
172
|
+
),
|
|
173
|
+
resolver_id=prov.resolver_id,
|
|
174
|
+
resolver_version=prov.resolver_version,
|
|
175
|
+
evidence_tier=prov.evidence_tier,
|
|
176
|
+
index_hash=prov.index_hash,
|
|
177
|
+
validation_status="resolved",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
stats["semantic_enriched"] += 1
|
|
181
|
+
return dataclasses.replace(
|
|
182
|
+
e,
|
|
183
|
+
to_id=ref.target_id,
|
|
184
|
+
resolution_status="resolved",
|
|
185
|
+
confidence_class=ref.confidence_class,
|
|
186
|
+
resolver=prov.resolver_id,
|
|
187
|
+
evidence_index=(*e.evidence_index, ev_idx),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _verify_edge(
|
|
191
|
+
self,
|
|
192
|
+
e: GraphEdge,
|
|
193
|
+
kind: str,
|
|
194
|
+
node_by_id: Mapping[str, object],
|
|
195
|
+
persistable: set[str],
|
|
196
|
+
evidence: list[EvidenceRecord],
|
|
197
|
+
diagnostics: list[ParseDiagnostic],
|
|
198
|
+
stats: dict[str, int],
|
|
199
|
+
) -> GraphEdge:
|
|
200
|
+
"""Verifica aresta ``resolved`` contra tiers superiores (lf-0035).
|
|
201
|
+
|
|
202
|
+
Determinístico:
|
|
203
|
+
- Sem resolver acima do tier atual → aresta intacta.
|
|
204
|
+
- Tier superior confirma o MESMO alvo → proveniência sobe.
|
|
205
|
+
- Tier superior diverge (alvo persistível distinto) →
|
|
206
|
+
``ambiguous`` + evidência ``conflict`` + diagnóstico; o alvo
|
|
207
|
+
original permanece (evidência externa nunca sobrescreve)."""
|
|
208
|
+
current_tier = _edge_tier(e.resolver)
|
|
209
|
+
if current_tier <= int(EvidenceTier.SEMANTIC_INDEX):
|
|
210
|
+
return e # nada acima do tier mais alto
|
|
211
|
+
name = e.to_text
|
|
212
|
+
if not name:
|
|
213
|
+
tgt = node_by_id.get(e.to_id) if e.to_id else None
|
|
214
|
+
name = getattr(tgt, "name", "") if tgt is not None else ""
|
|
215
|
+
if not name:
|
|
216
|
+
return e
|
|
217
|
+
src = node_by_id.get(e.from_id)
|
|
218
|
+
language = getattr(src, "language", "") if src is not None else ""
|
|
219
|
+
request = ResolutionRequest(
|
|
220
|
+
name=name, kind=kind, path=e.source_path,
|
|
221
|
+
language=str(language), span=e.span,
|
|
222
|
+
)
|
|
223
|
+
above = [
|
|
224
|
+
r for r in self._coord.candidates(request)
|
|
225
|
+
if r.capabilities().tier < current_tier
|
|
226
|
+
]
|
|
227
|
+
if not above:
|
|
228
|
+
return e # sem resolvedor superior configurado — custo zero
|
|
229
|
+
stats["semantic_verified"] += 1
|
|
230
|
+
result = self._coord.resolve(request, above_tier=current_tier)
|
|
231
|
+
self._diags(result, e, diagnostics)
|
|
232
|
+
ref = result.reference
|
|
233
|
+
if ref is None or ref.status != "resolved" or ref.target_id is None:
|
|
234
|
+
return e
|
|
235
|
+
prov = ref.provenance
|
|
236
|
+
if ref.target_id == e.to_id:
|
|
237
|
+
ev_idx = len(evidence)
|
|
238
|
+
evidence.append(
|
|
239
|
+
EvidenceRecord(
|
|
240
|
+
path=e.source_path, span=e.span, content_hash="",
|
|
241
|
+
extractor=f"resolver:{prov.resolver_id}",
|
|
242
|
+
extractor_version=prov.resolver_version,
|
|
243
|
+
notes=(
|
|
244
|
+
f"semantic confirmed tier={prov.evidence_tier} "
|
|
245
|
+
f"{prov.detail}".strip()
|
|
246
|
+
),
|
|
247
|
+
resolver_id=prov.resolver_id,
|
|
248
|
+
resolver_version=prov.resolver_version,
|
|
249
|
+
evidence_tier=prov.evidence_tier,
|
|
250
|
+
index_hash=prov.index_hash,
|
|
251
|
+
validation_status="confirmed",
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
stats["semantic_confirmed"] += 1
|
|
255
|
+
return dataclasses.replace(
|
|
256
|
+
e,
|
|
257
|
+
confidence_class=ref.confidence_class,
|
|
258
|
+
resolver=prov.resolver_id,
|
|
259
|
+
evidence_index=(*e.evidence_index, ev_idx),
|
|
260
|
+
)
|
|
261
|
+
if ref.target_id not in persistable:
|
|
262
|
+
return e # alvo alternativo não existe no snapshot — ignora
|
|
263
|
+
ev_idx = len(evidence)
|
|
264
|
+
evidence.append(
|
|
265
|
+
EvidenceRecord(
|
|
266
|
+
path=e.source_path, span=e.span, content_hash="",
|
|
267
|
+
extractor=f"resolver:{prov.resolver_id}",
|
|
268
|
+
extractor_version=prov.resolver_version,
|
|
269
|
+
notes=(
|
|
270
|
+
f"semantic conflict tier={prov.evidence_tier}: "
|
|
271
|
+
f"kept={e.to_id} alternative={ref.target_id} "
|
|
272
|
+
f"{prov.detail}".strip()
|
|
273
|
+
),
|
|
274
|
+
resolver_id=prov.resolver_id,
|
|
275
|
+
resolver_version=prov.resolver_version,
|
|
276
|
+
evidence_tier=prov.evidence_tier,
|
|
277
|
+
index_hash=prov.index_hash,
|
|
278
|
+
validation_status="conflict",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
diagnostics.append(
|
|
282
|
+
ParseDiagnostic(
|
|
283
|
+
code="semantic-conflict",
|
|
284
|
+
severity="warning",
|
|
285
|
+
message=(
|
|
286
|
+
f"{e.source_path}: {e.type} {e.from_id} -> {e.to_id}: "
|
|
287
|
+
f"resolver {prov.resolver_id} aponta {ref.target_id}"
|
|
288
|
+
),
|
|
289
|
+
span=e.span,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
stats["semantic_conflicts"] += 1
|
|
293
|
+
return dataclasses.replace(
|
|
294
|
+
e,
|
|
295
|
+
resolution_status="ambiguous",
|
|
296
|
+
evidence_index=(*e.evidence_index, ev_idx),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _diags(
|
|
301
|
+
result: ResolutionResult,
|
|
302
|
+
e: GraphEdge,
|
|
303
|
+
out: list[ParseDiagnostic],
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Traduz ResolutionDiagnostics do resolver para ParseDiagnostic
|
|
306
|
+
— erros avisam; infos de `no-occurrence`/`no-resolver` só entram
|
|
307
|
+
quando severity ≥ warning para não inundar o snapshot."""
|
|
308
|
+
for d in result.diagnostics:
|
|
309
|
+
if d.severity == "info":
|
|
310
|
+
continue
|
|
311
|
+
out.append(
|
|
312
|
+
ParseDiagnostic(
|
|
313
|
+
code=f"semantic:{d.resolver_id}:{d.code}",
|
|
314
|
+
severity=d.severity,
|
|
315
|
+
message=d.message,
|
|
316
|
+
span=e.span,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Adapters LSP opt-in (lf-0018).
|
|
2
|
+
|
|
3
|
+
- Nenhum servidor é instalado, baixado ou spawnado automaticamente.
|
|
4
|
+
- Ativação exclusivamente por configuração explícita do usuário
|
|
5
|
+
(``config.toml [resolvers.lsp.servers.<name>]``) — wiring de CLI vem
|
|
6
|
+
no lf-0022.
|
|
7
|
+
- Transporte: stdio apenas (sem TCP/socket). Todo request tem timeout
|
|
8
|
+
obrigatório + ``$/cancelRequest``.
|
|
9
|
+
- ``spawn.py`` é o único site de subprocess (whitelist do audit).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from code_atlas.resolvers.lsp.adapter import LspResolver
|
|
13
|
+
from code_atlas.resolvers.lsp.lifecycle import LspClient, LspError, make_client
|
|
14
|
+
from code_atlas.resolvers.lsp.spawn import LspServerSpec, SpawnError
|
|
15
|
+
from code_atlas.resolvers.lsp.transport import JsonRpcTransport, TransportError
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"JsonRpcTransport",
|
|
19
|
+
"LspClient",
|
|
20
|
+
"LspError",
|
|
21
|
+
"LspResolver",
|
|
22
|
+
"LspServerSpec",
|
|
23
|
+
"SpawnError",
|
|
24
|
+
"TransportError",
|
|
25
|
+
"make_client",
|
|
26
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""LspResolver — resolvedor EvidenceTier.LSP (lf-0018).
|
|
2
|
+
|
|
3
|
+
Envolve um ``LspClient`` ativo. Métodos LSP só são chamados quando o
|
|
4
|
+
servidor anunciou a capability no handshake — caso contrário, diagnóstico
|
|
5
|
+
``capability-unsupported`` e status honesto (nunca crash).
|
|
6
|
+
|
|
7
|
+
Mapeamento de alvos: ``Location{uri,range}`` → path relativo ao
|
|
8
|
+
``root_uri`` → nó de módulo do ``ScopeUniverse`` (identidade no nível de
|
|
9
|
+
arquivo, verificável). Precisão símbolo-a-símbolo é trabalho futuro;
|
|
10
|
+
quando o uri não pertence ao snapshot → ``external``/``unresolved``.
|
|
11
|
+
|
|
12
|
+
Proveniência: ``lsp:<server-name>:<version>`` — distinta de SCIP.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from code_atlas.resolvers.capabilities import EvidenceTier, ResolverCapability
|
|
21
|
+
from code_atlas.resolvers.contracts import (
|
|
22
|
+
ResolutionDiagnostic,
|
|
23
|
+
ResolutionRequest,
|
|
24
|
+
ResolutionResult,
|
|
25
|
+
ResolvedDefinition,
|
|
26
|
+
ResolvedReference,
|
|
27
|
+
ResolverProvenance,
|
|
28
|
+
)
|
|
29
|
+
from code_atlas.resolvers.lsp.lifecycle import LspClient, LspError
|
|
30
|
+
from code_atlas.resolvers.lsp.transport import TransportError
|
|
31
|
+
from code_atlas.resolvers.syntactic.resolver import ScopeUniverse
|
|
32
|
+
|
|
33
|
+
RESOLVER_ID_PREFIX = "lsp"
|
|
34
|
+
RESOLVER_VERSION = "1.0.0"
|
|
35
|
+
|
|
36
|
+
_KIND_TO_METHOD = {
|
|
37
|
+
"definition": "textDocument/definition",
|
|
38
|
+
"call": "textDocument/references",
|
|
39
|
+
"reference": "textDocument/references",
|
|
40
|
+
"type-ref": "textDocument/definition",
|
|
41
|
+
"import": "textDocument/definition",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LspResolver:
|
|
46
|
+
"""Implementa ``SemanticResolver`` sobre um LspClient ativo."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, client: LspClient, universe: ScopeUniverse) -> None:
|
|
49
|
+
self._client = client
|
|
50
|
+
self._u = universe
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def resolver_id(self) -> str:
|
|
54
|
+
return f"{RESOLVER_ID_PREFIX}:{self._client.spec.name}"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def resolver_version(self) -> str:
|
|
58
|
+
return RESOLVER_VERSION
|
|
59
|
+
|
|
60
|
+
def capabilities(self) -> ResolverCapability:
|
|
61
|
+
return ResolverCapability(
|
|
62
|
+
resolver_id=self.resolver_id,
|
|
63
|
+
tier=EvidenceTier.LSP,
|
|
64
|
+
languages=self._client.spec.languages,
|
|
65
|
+
relation_types=frozenset(
|
|
66
|
+
{"REFERENCES", "CALLS", "IMPORTS", "EXTENDS"}),
|
|
67
|
+
needs_external_process=True,
|
|
68
|
+
detail=f"stdio lsp server {self._client.spec.name}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _prov(self) -> ResolverProvenance:
|
|
72
|
+
caps = self._client.capabilities
|
|
73
|
+
detail = f"{caps.server_name}:{caps.server_version}".rstrip(":")
|
|
74
|
+
return ResolverProvenance(
|
|
75
|
+
resolver_id=self.resolver_id,
|
|
76
|
+
resolver_version=RESOLVER_VERSION,
|
|
77
|
+
evidence_tier=int(EvidenceTier.LSP),
|
|
78
|
+
detail=detail,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# -- contrato ----------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def resolve(self, request: ResolutionRequest) -> ResolutionResult:
|
|
84
|
+
method = _KIND_TO_METHOD.get(request.kind)
|
|
85
|
+
if method is None:
|
|
86
|
+
return self._with_diag(request, "kind-unsupported",
|
|
87
|
+
f"no LSP method for {request.kind}")
|
|
88
|
+
if not self._client.available:
|
|
89
|
+
return self._with_diag(request, "server-unavailable",
|
|
90
|
+
"lsp server not running")
|
|
91
|
+
if not self._client.capabilities.supports(method):
|
|
92
|
+
return self._with_diag(request, "capability-unsupported",
|
|
93
|
+
f"server did not advertise {method}")
|
|
94
|
+
|
|
95
|
+
span = request.span
|
|
96
|
+
if span is None:
|
|
97
|
+
return self._with_diag(request, "span-required",
|
|
98
|
+
"LSP requests need a source span")
|
|
99
|
+
params: dict[str, Any] = {
|
|
100
|
+
"textDocument": {"uri": self._doc_uri(request.path)},
|
|
101
|
+
"position": {"line": span.start_line - 1,
|
|
102
|
+
"character": span.start_col},
|
|
103
|
+
}
|
|
104
|
+
if method == "textDocument/references":
|
|
105
|
+
params["context"] = {"includeDeclaration": False}
|
|
106
|
+
result = self._call(request, method, params)
|
|
107
|
+
if isinstance(result, ResolutionResult):
|
|
108
|
+
return result
|
|
109
|
+
result_diags = tuple(
|
|
110
|
+
ResolutionDiagnostic(
|
|
111
|
+
code="server-diagnostic",
|
|
112
|
+
severity="info",
|
|
113
|
+
message=str(d)[:200],
|
|
114
|
+
resolver_id=self.resolver_id,
|
|
115
|
+
)
|
|
116
|
+
for d in self._client.diagnostics_for(
|
|
117
|
+
self._doc_uri(request.path))
|
|
118
|
+
)
|
|
119
|
+
mapped = self._map_result(request, self._locations(result))
|
|
120
|
+
if result_diags:
|
|
121
|
+
mapped = dataclasses.replace(
|
|
122
|
+
mapped,
|
|
123
|
+
diagnostics=result_diags + mapped.diagnostics,
|
|
124
|
+
)
|
|
125
|
+
return mapped
|
|
126
|
+
|
|
127
|
+
def _call(
|
|
128
|
+
self, request: ResolutionRequest, method: str,
|
|
129
|
+
params: dict[str, Any],
|
|
130
|
+
) -> Any:
|
|
131
|
+
"""Request com recovery único embutido (lf-0029)."""
|
|
132
|
+
try:
|
|
133
|
+
return self._client.request(method, params)
|
|
134
|
+
except (TimeoutError, LspError, TransportError) as exc:
|
|
135
|
+
return self._recover_retry(request, method, params, exc)
|
|
136
|
+
|
|
137
|
+
def _recover_retry(
|
|
138
|
+
self, request: ResolutionRequest, method: str,
|
|
139
|
+
params: dict[str, Any], exc: Exception,
|
|
140
|
+
) -> Any:
|
|
141
|
+
"""lf-0029: recovery pós-falha — UM restart + UM retry.
|
|
142
|
+
Retorna o resultado do retry ou um ResolutionResult de erro
|
|
143
|
+
tipado (fallback sintático segue valendo)."""
|
|
144
|
+
if not self._try_restart():
|
|
145
|
+
code = ("request-timeout" if isinstance(exc, TimeoutError)
|
|
146
|
+
else "server-error")
|
|
147
|
+
return self._with_diag(
|
|
148
|
+
request, code, f"{exc}; restart failed")
|
|
149
|
+
try:
|
|
150
|
+
return self._client.request(method, params)
|
|
151
|
+
except (TimeoutError, LspError, TransportError) as exc2:
|
|
152
|
+
return self._with_diag(
|
|
153
|
+
request, "server-error", f"{exc}; after restart: {exc2}")
|
|
154
|
+
|
|
155
|
+
def _try_restart(self) -> bool:
|
|
156
|
+
"""Uma tentativa de recovery — nunca lança."""
|
|
157
|
+
try:
|
|
158
|
+
self._client.restart()
|
|
159
|
+
except Exception:
|
|
160
|
+
return False
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
# -- mapeamento de resultado -------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _map_result(
|
|
166
|
+
self, request: ResolutionRequest, locations: list[dict[str, Any]]
|
|
167
|
+
) -> ResolutionResult:
|
|
168
|
+
if not locations:
|
|
169
|
+
return ResolutionResult(
|
|
170
|
+
request=request,
|
|
171
|
+
reference=ResolvedReference(
|
|
172
|
+
status="unresolved",
|
|
173
|
+
confidence_class="syntactic",
|
|
174
|
+
provenance=self._prov(),
|
|
175
|
+
target_text=request.name,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
nids: list[str] = []
|
|
179
|
+
foreign = False
|
|
180
|
+
for loc in locations:
|
|
181
|
+
path = self._rel_path(loc.get("uri", ""))
|
|
182
|
+
if path is None:
|
|
183
|
+
foreign = True
|
|
184
|
+
continue
|
|
185
|
+
nid = self._u.by_path.get(path)
|
|
186
|
+
if nid and nid not in nids:
|
|
187
|
+
nids.append(nid)
|
|
188
|
+
if len(nids) == 1:
|
|
189
|
+
status, conf = "resolved", "resolved"
|
|
190
|
+
elif len(nids) > 1:
|
|
191
|
+
status, conf = "ambiguous", "syntactic"
|
|
192
|
+
else:
|
|
193
|
+
status = "external" if foreign else "unresolved"
|
|
194
|
+
conf = "syntactic"
|
|
195
|
+
return ResolutionResult(
|
|
196
|
+
request=request,
|
|
197
|
+
reference=ResolvedReference(
|
|
198
|
+
status=status,
|
|
199
|
+
confidence_class=conf,
|
|
200
|
+
provenance=self._prov(),
|
|
201
|
+
target_id=nids[0] if len(nids) == 1 else None,
|
|
202
|
+
target_text=request.name,
|
|
203
|
+
candidates=tuple(sorted(nids)) if len(nids) > 1 else (),
|
|
204
|
+
),
|
|
205
|
+
definitions=tuple(
|
|
206
|
+
ResolvedDefinition(
|
|
207
|
+
symbol=request.name,
|
|
208
|
+
path=self._rel_path(loc.get("uri", "")) or "",
|
|
209
|
+
provenance=self._prov(),
|
|
210
|
+
)
|
|
211
|
+
for loc in locations
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# -- helpers -----------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def _doc_uri(self, path: str) -> str:
|
|
218
|
+
root = self._client.spec.root_uri.rstrip("/")
|
|
219
|
+
return f"{root}/{path}"
|
|
220
|
+
|
|
221
|
+
def _rel_path(self, uri: str) -> str | None:
|
|
222
|
+
"""file:// URI → path relativo ao root_uri; None se fora do repo."""
|
|
223
|
+
root = self._client.spec.root_uri.rstrip("/") + "/"
|
|
224
|
+
if uri.startswith(root):
|
|
225
|
+
return uri[len(root):]
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _locations(result: object) -> list[dict[str, Any]]:
|
|
230
|
+
"""definition/references → lista de Location (obj ou lista)."""
|
|
231
|
+
if result is None:
|
|
232
|
+
return []
|
|
233
|
+
if isinstance(result, dict): # Location único ou LocationLink
|
|
234
|
+
if "targetUri" in result:
|
|
235
|
+
return [{"uri": result["targetUri"],
|
|
236
|
+
"range": result.get("targetSelectionRange", {})}]
|
|
237
|
+
return [result] if "uri" in result else []
|
|
238
|
+
if isinstance(result, list):
|
|
239
|
+
return [r for r in result if isinstance(r, dict)]
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
def _with_diag(
|
|
243
|
+
self, request: ResolutionRequest, code: str, message: str
|
|
244
|
+
) -> ResolutionResult:
|
|
245
|
+
return ResolutionResult(
|
|
246
|
+
request=request,
|
|
247
|
+
reference=ResolvedReference(
|
|
248
|
+
status="unresolved",
|
|
249
|
+
confidence_class="syntactic",
|
|
250
|
+
provenance=self._prov(),
|
|
251
|
+
target_text=request.name,
|
|
252
|
+
),
|
|
253
|
+
diagnostics=(ResolutionDiagnostic(
|
|
254
|
+
code=code, severity="warning", message=message,
|
|
255
|
+
resolver_id=self.resolver_id),),
|
|
256
|
+
)
|