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,289 @@
|
|
|
1
|
+
"""ScipResolver — resolvedor EvidenceTier.SEMANTIC_INDEX (lf-0017).
|
|
2
|
+
|
|
3
|
+
Consome um ``ScipIndex`` já importado e validado e resolve
|
|
4
|
+
referências/definições via correlação explícita (``ScipCorrelator``) com
|
|
5
|
+
os nós do grafo Atlas. A proveniência carrega a ferramenta+versão do
|
|
6
|
+
indexer externo; ``confidence_class='resolved'`` só quando a identidade
|
|
7
|
+
do símbolo foi verificada (alvo único no snapshot).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from code_atlas.parsers.contracts import SourceSpan
|
|
13
|
+
from code_atlas.resolvers.capabilities import EvidenceTier, ResolverCapability
|
|
14
|
+
from code_atlas.resolvers.contracts import (
|
|
15
|
+
ResolutionDiagnostic,
|
|
16
|
+
ResolutionRequest,
|
|
17
|
+
ResolutionResult,
|
|
18
|
+
ResolvedDefinition,
|
|
19
|
+
ResolvedReference,
|
|
20
|
+
ResolverProvenance,
|
|
21
|
+
)
|
|
22
|
+
from code_atlas.resolvers.scip.importer import (
|
|
23
|
+
ROLE_DEFINITION,
|
|
24
|
+
ScipDocument,
|
|
25
|
+
ScipIndex,
|
|
26
|
+
ScipOccurrence,
|
|
27
|
+
ScipRange,
|
|
28
|
+
)
|
|
29
|
+
from code_atlas.resolvers.scip.mapper import ScipCorrelator, parse_symbol
|
|
30
|
+
from code_atlas.resolvers.scip.provenance import IndexProvenance
|
|
31
|
+
from code_atlas.resolvers.syntactic.resolver import ScopeUniverse
|
|
32
|
+
|
|
33
|
+
RESOLVER_ID = "scip"
|
|
34
|
+
RESOLVER_VERSION = "1.0.0"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ScipResolver:
|
|
38
|
+
"""Resolve pedidos usando ocorrências de um índice SCIP importado."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
index: ScipIndex,
|
|
43
|
+
universe: ScopeUniverse,
|
|
44
|
+
provenance: IndexProvenance | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._index = index
|
|
47
|
+
self._corr = ScipCorrelator(universe)
|
|
48
|
+
self._docs = {d.relative_path: d for d in index.documents}
|
|
49
|
+
# lf-0028: sem assessment explícito, o índice é não-verificado —
|
|
50
|
+
# tratado como degraded (nunca comprovado), nunca como current.
|
|
51
|
+
self._prov_info = provenance or IndexProvenance(
|
|
52
|
+
producer_name=index.tool_name,
|
|
53
|
+
producer_version=index.tool_version,
|
|
54
|
+
repository_identity=index.project_root,
|
|
55
|
+
validation_status="degraded",
|
|
56
|
+
validation_evidence=("no revision assessment",),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def resolver_id(self) -> str:
|
|
61
|
+
return RESOLVER_ID
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def resolver_version(self) -> str:
|
|
65
|
+
return RESOLVER_VERSION
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def index(self) -> ScipIndex:
|
|
69
|
+
return self._index
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def provenance(self) -> IndexProvenance:
|
|
73
|
+
return self._prov_info
|
|
74
|
+
|
|
75
|
+
def capabilities(self) -> ResolverCapability:
|
|
76
|
+
return ResolverCapability(
|
|
77
|
+
resolver_id=RESOLVER_ID,
|
|
78
|
+
tier=EvidenceTier.SEMANTIC_INDEX,
|
|
79
|
+
languages=frozenset(),
|
|
80
|
+
relation_types=frozenset(
|
|
81
|
+
{"REFERENCES", "CALLS", "EXTENDS", "IMPLEMENTS", "IMPORTS"}
|
|
82
|
+
),
|
|
83
|
+
detail=(
|
|
84
|
+
f"scip index by {self._index.tool_name}"
|
|
85
|
+
f" {self._index.tool_version}".strip()
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _prov(self) -> ResolverProvenance:
|
|
90
|
+
status = self._prov_info.validation_status
|
|
91
|
+
detail = (
|
|
92
|
+
f"{self._index.tool_name} {self._index.tool_version}".strip()
|
|
93
|
+
)
|
|
94
|
+
if status != "current":
|
|
95
|
+
detail = f"{detail} [{status}]".strip()
|
|
96
|
+
return ResolverProvenance(
|
|
97
|
+
resolver_id=RESOLVER_ID,
|
|
98
|
+
resolver_version=RESOLVER_VERSION,
|
|
99
|
+
evidence_tier=int(EvidenceTier.SEMANTIC_INDEX),
|
|
100
|
+
detail=detail,
|
|
101
|
+
index_hash=self._prov_info.index_sha256,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _conf(self, intended: str) -> str:
|
|
105
|
+
"""lf-0028: evidência de índice degraded nunca comprova —
|
|
106
|
+
confidence capada em ``heuristic``."""
|
|
107
|
+
if (
|
|
108
|
+
self._prov_info.validation_status != "current"
|
|
109
|
+
and intended == "resolved"
|
|
110
|
+
):
|
|
111
|
+
return "heuristic"
|
|
112
|
+
return intended
|
|
113
|
+
|
|
114
|
+
# -- contrato SemanticResolver --------------------------------------
|
|
115
|
+
|
|
116
|
+
def resolve(self, request: ResolutionRequest) -> ResolutionResult:
|
|
117
|
+
# lf-0028: índice rejeitado não produz resolução nenhuma —
|
|
118
|
+
# incompatível nunca contamina o snapshot.
|
|
119
|
+
if self._prov_info.validation_status == "rejected":
|
|
120
|
+
return ResolutionResult(
|
|
121
|
+
request=request,
|
|
122
|
+
reference=ResolvedReference(
|
|
123
|
+
status="unresolved",
|
|
124
|
+
confidence_class="heuristic",
|
|
125
|
+
provenance=self._prov(),
|
|
126
|
+
target_text=request.name,
|
|
127
|
+
),
|
|
128
|
+
diagnostics=(self._diag(
|
|
129
|
+
"index-rejected",
|
|
130
|
+
"scip index rejected by revision safety: "
|
|
131
|
+
+ "; ".join(self._prov_info.validation_evidence[:3]),
|
|
132
|
+
"error"),),
|
|
133
|
+
)
|
|
134
|
+
doc = self._docs.get(request.path)
|
|
135
|
+
if doc is None:
|
|
136
|
+
return ResolutionResult(
|
|
137
|
+
request=request,
|
|
138
|
+
diagnostics=(self._diag("document-not-in-index",
|
|
139
|
+
f"no scip document for "
|
|
140
|
+
f"{request.path}", "warning"),),
|
|
141
|
+
)
|
|
142
|
+
occ = self._occurrence_at(doc, request)
|
|
143
|
+
if occ is None:
|
|
144
|
+
return ResolutionResult(
|
|
145
|
+
request=request,
|
|
146
|
+
reference=ResolvedReference(
|
|
147
|
+
status="unresolved",
|
|
148
|
+
confidence_class="syntactic",
|
|
149
|
+
provenance=self._prov(),
|
|
150
|
+
target_text=request.name,
|
|
151
|
+
),
|
|
152
|
+
diagnostics=(self._diag("no-occurrence",
|
|
153
|
+
"no scip occurrence at span",
|
|
154
|
+
"info"),),
|
|
155
|
+
)
|
|
156
|
+
return self._resolve_symbol(request, occ.symbol,
|
|
157
|
+
is_definition=occ.is_definition)
|
|
158
|
+
|
|
159
|
+
# -- resolução por símbolo scip --------------------------------------
|
|
160
|
+
|
|
161
|
+
def resolve_symbol(self, request: ResolutionRequest,
|
|
162
|
+
scip_symbol: str) -> ResolutionResult:
|
|
163
|
+
"""API direta: resolve uma string de símbolo scip."""
|
|
164
|
+
return self._resolve_symbol(request, scip_symbol)
|
|
165
|
+
|
|
166
|
+
def _resolve_symbol(
|
|
167
|
+
self,
|
|
168
|
+
request: ResolutionRequest,
|
|
169
|
+
scip_symbol: str,
|
|
170
|
+
*,
|
|
171
|
+
is_definition: bool = False,
|
|
172
|
+
) -> ResolutionResult:
|
|
173
|
+
sym = parse_symbol(scip_symbol)
|
|
174
|
+
if sym is None:
|
|
175
|
+
return ResolutionResult(
|
|
176
|
+
request=request,
|
|
177
|
+
reference=ResolvedReference(
|
|
178
|
+
status="unresolved",
|
|
179
|
+
confidence_class="heuristic",
|
|
180
|
+
provenance=self._prov(),
|
|
181
|
+
target_text=scip_symbol,
|
|
182
|
+
),
|
|
183
|
+
diagnostics=(self._diag("bad-symbol",
|
|
184
|
+
f"unparseable scip symbol "
|
|
185
|
+
f"{scip_symbol!r}", "warning"),),
|
|
186
|
+
)
|
|
187
|
+
path = request.path
|
|
188
|
+
nid = self._corr.node_for_symbol(scip_symbol, path)
|
|
189
|
+
if nid is not None:
|
|
190
|
+
status = "resolved"
|
|
191
|
+
# identidade verificada — alvo único; índice degraded capa
|
|
192
|
+
# em heuristic (lf-0028: nunca promove como comprovada).
|
|
193
|
+
conf = self._conf("resolved")
|
|
194
|
+
else:
|
|
195
|
+
cands = self._corr.candidates_for_symbol(scip_symbol)
|
|
196
|
+
if len(cands) > 1:
|
|
197
|
+
return ResolutionResult(
|
|
198
|
+
request=request,
|
|
199
|
+
reference=ResolvedReference(
|
|
200
|
+
status="ambiguous",
|
|
201
|
+
confidence_class="syntactic",
|
|
202
|
+
provenance=self._prov(),
|
|
203
|
+
target_text=scip_symbol,
|
|
204
|
+
candidates=cands,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
# símbolo scip válido fora do snapshot → externo (pacote
|
|
208
|
+
# de terceiro) ou não correlacionável
|
|
209
|
+
status = "external" if not sym.is_local else "unresolved"
|
|
210
|
+
conf = "syntactic"
|
|
211
|
+
defs = self._definitions_for(scip_symbol) if is_definition else ()
|
|
212
|
+
return ResolutionResult(
|
|
213
|
+
request=request,
|
|
214
|
+
reference=ResolvedReference(
|
|
215
|
+
status=status,
|
|
216
|
+
confidence_class=conf,
|
|
217
|
+
provenance=self._prov(),
|
|
218
|
+
target_id=nid,
|
|
219
|
+
target_text=scip_symbol,
|
|
220
|
+
),
|
|
221
|
+
definitions=defs,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def _definitions_for(self, scip_symbol: str) -> tuple[ResolvedDefinition, ...]:
|
|
225
|
+
"""Occurrences marcadas como Definition para o símbolo."""
|
|
226
|
+
out: list[ResolvedDefinition] = []
|
|
227
|
+
for d in self._index.documents:
|
|
228
|
+
for occ in d.occurrences:
|
|
229
|
+
if occ.symbol == scip_symbol and occ.roles & ROLE_DEFINITION:
|
|
230
|
+
out.append(
|
|
231
|
+
ResolvedDefinition(
|
|
232
|
+
symbol=scip_symbol,
|
|
233
|
+
path=d.relative_path,
|
|
234
|
+
provenance=self._prov(),
|
|
235
|
+
span=self._span_of(occ),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return tuple(out)
|
|
239
|
+
|
|
240
|
+
# -- helpers ---------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _span_of(occ: ScipOccurrence) -> SourceSpan | None:
|
|
244
|
+
rng: ScipRange | None = occ.range
|
|
245
|
+
if rng is None:
|
|
246
|
+
return None
|
|
247
|
+
# scip usa linhas 0-based; Atlas SourceSpan usa 1-based
|
|
248
|
+
return SourceSpan(
|
|
249
|
+
start_byte=0, end_byte=0,
|
|
250
|
+
start_line=rng.start_line + 1, start_col=rng.start_char,
|
|
251
|
+
end_line=rng.end_line + 1, end_col=rng.end_char,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _occurrence_at(
|
|
256
|
+
doc: ScipDocument, request: ResolutionRequest
|
|
257
|
+
) -> ScipOccurrence | None:
|
|
258
|
+
"""Seleciona a occurrence do documento que casa com o request.
|
|
259
|
+
|
|
260
|
+
Preferência: occurrence cobrindo o span do request (convertido
|
|
261
|
+
de linhas 1-based do Atlas para 0-based do scip); fallback
|
|
262
|
+
heurístico por símbolo textual (nome no descriptor).
|
|
263
|
+
"""
|
|
264
|
+
occs = doc.occurrences
|
|
265
|
+
span = request.span
|
|
266
|
+
if span is not None:
|
|
267
|
+
s0 = (span.start_line - 1, span.start_col)
|
|
268
|
+
e0 = (span.end_line - 1, span.end_col)
|
|
269
|
+
for occ in occs:
|
|
270
|
+
r = occ.range
|
|
271
|
+
if r is None:
|
|
272
|
+
continue
|
|
273
|
+
if (r.start_line, r.start_char) <= s0 and \
|
|
274
|
+
(r.end_line, r.end_char) >= e0:
|
|
275
|
+
return occ
|
|
276
|
+
return None
|
|
277
|
+
# sem span: casa pelo nome do request nos descriptors
|
|
278
|
+
for occ in occs:
|
|
279
|
+
sym = parse_symbol(occ.symbol)
|
|
280
|
+
if sym is not None and sym.descriptors and \
|
|
281
|
+
sym.descriptors[-1].name == request.name:
|
|
282
|
+
return occ
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def _diag(self, code: str, message: str, severity: str) -> ResolutionDiagnostic:
|
|
286
|
+
return ResolutionDiagnostic(
|
|
287
|
+
code=code, severity=severity, message=message,
|
|
288
|
+
resolver_id=RESOLVER_ID,
|
|
289
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Validação de índices SCIP contra o workspace Atlas (lf-0017).
|
|
2
|
+
|
|
3
|
+
Impede associações falsas: um índice produzido para outro repositório ou
|
|
4
|
+
outra revisão NUNCA é correlacionado silenciosamente — a rejeição é
|
|
5
|
+
explícita e diagnosticada.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from code_atlas.resolvers.scip.importer import ScipIndex
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _pct_decode(s: str) -> str:
|
|
17
|
+
"""Percent-decode mínimo para paths de file:// (stdlib pura — urllib
|
|
18
|
+
é banido pelo audit de rede do projeto)."""
|
|
19
|
+
out = bytearray()
|
|
20
|
+
i = 0
|
|
21
|
+
raw = s.encode("utf-8", errors="replace")
|
|
22
|
+
while i < len(raw):
|
|
23
|
+
if raw[i:i + 1] == b"%" and i + 2 < len(raw) + 1 and \
|
|
24
|
+
i + 2 < len(raw) + 1:
|
|
25
|
+
try:
|
|
26
|
+
out.append(int(raw[i + 1:i + 3], 16))
|
|
27
|
+
i += 3
|
|
28
|
+
continue
|
|
29
|
+
except ValueError:
|
|
30
|
+
pass
|
|
31
|
+
out.append(raw[i])
|
|
32
|
+
i += 1
|
|
33
|
+
return out.decode("utf-8", errors="replace")
|
|
34
|
+
|
|
35
|
+
# limites defensivos — índices externos são untrusted input
|
|
36
|
+
MAX_INDEX_BYTES = 256 * 1024 * 1024 # 256 MiB
|
|
37
|
+
MAX_DOCUMENTS = 200_000
|
|
38
|
+
MAX_OCCURRENCES_PER_DOC = 500_000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ScipValidationResult:
|
|
43
|
+
ok: bool
|
|
44
|
+
errors: tuple[str, ...] = ()
|
|
45
|
+
warnings: tuple[str, ...] = ()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_index(index: ScipIndex, raw_size: int) -> ScipValidationResult:
|
|
49
|
+
"""Sanidade estrutural do índice decodificado."""
|
|
50
|
+
errors: list[str] = []
|
|
51
|
+
warnings: list[str] = []
|
|
52
|
+
if raw_size > MAX_INDEX_BYTES:
|
|
53
|
+
errors.append(f"index too large: {raw_size} > {MAX_INDEX_BYTES}")
|
|
54
|
+
if len(index.documents) > MAX_DOCUMENTS:
|
|
55
|
+
errors.append(
|
|
56
|
+
f"too many documents: {len(index.documents)} > {MAX_DOCUMENTS}")
|
|
57
|
+
seen_paths: set[str] = set()
|
|
58
|
+
for doc in index.documents:
|
|
59
|
+
p = doc.relative_path
|
|
60
|
+
if not p or p.startswith("/") or ".." in p.split("/"):
|
|
61
|
+
errors.append(f"bad document path: {p!r}")
|
|
62
|
+
if p in seen_paths:
|
|
63
|
+
warnings.append(f"duplicate document path: {p}")
|
|
64
|
+
seen_paths.add(p)
|
|
65
|
+
if len(doc.occurrences) > MAX_OCCURRENCES_PER_DOC:
|
|
66
|
+
errors.append(f"doc {p}: occurrences > {MAX_OCCURRENCES_PER_DOC}")
|
|
67
|
+
for o in doc.occurrences:
|
|
68
|
+
if not o.symbol:
|
|
69
|
+
warnings.append(f"doc {p}: occurrence with empty symbol")
|
|
70
|
+
break
|
|
71
|
+
if not index.tool_name:
|
|
72
|
+
warnings.append("metadata.tool_info.name empty")
|
|
73
|
+
return ScipValidationResult(ok=not errors, errors=tuple(errors),
|
|
74
|
+
warnings=tuple(warnings))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def project_root_matches(index: ScipIndex, workspace_root: Path) -> bool:
|
|
78
|
+
"""O ``project_root`` do índice precisa apontar para a raiz indexada.
|
|
79
|
+
|
|
80
|
+
``project_root`` é uma URI (file://...) — comparamos o caminho
|
|
81
|
+
decodificado com o caminho real do workspace.
|
|
82
|
+
"""
|
|
83
|
+
if not index.project_root:
|
|
84
|
+
return False
|
|
85
|
+
uri = index.project_root
|
|
86
|
+
if uri.startswith("file://"):
|
|
87
|
+
path = _pct_decode(uri[len("file://"):])
|
|
88
|
+
# strip host vazio/localhost (file:///x → /x; file://host/x)
|
|
89
|
+
if not path.startswith("/"):
|
|
90
|
+
path = "/" + path.split("/", 1)[-1]
|
|
91
|
+
# file:///C:/x → /C:/x — '/C:' seria diretório literal no
|
|
92
|
+
# Windows, não o drive C:; remove o separador de host.
|
|
93
|
+
if len(path) >= 3 and path[0] == "/" and path[1].isalpha() \
|
|
94
|
+
and path[2] == ":":
|
|
95
|
+
path = path[1:]
|
|
96
|
+
else:
|
|
97
|
+
path = uri
|
|
98
|
+
try:
|
|
99
|
+
return Path(path).resolve() == Path(workspace_root).resolve()
|
|
100
|
+
except OSError:
|
|
101
|
+
return False
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Resolvedor sintático (EvidenceTier.SYNTACTIC) — lf-0016.
|
|
2
|
+
|
|
3
|
+
Encapsula a resolução de escopo do Cycle 2 — a mesma lógica que vivia em
|
|
4
|
+
``graph.builder`` agora atrás do contrato ``SemanticResolver``. O universo
|
|
5
|
+
(``ScopeUniverse``) é montado pelo GraphBuilder durante a construção e
|
|
6
|
+
fornecido ao resolver para a fase de resolução.
|
|
7
|
+
|
|
8
|
+
Regras inalteradas (regressão garantida pelos testes do Cycle 2):
|
|
9
|
+
|
|
10
|
+
- ``resolved`` só com alvo único no snapshot — nunca por coincidência.
|
|
11
|
+
- ``obj.attr`` sem tipo conhecido de ``obj`` → ``unresolved``.
|
|
12
|
+
- Alias de import resolve via ``import_targets`` do módulo de origem.
|
|
13
|
+
- Imports sem alvo local distinguem ``external`` (stdlib/site-packages)
|
|
14
|
+
de ``unresolved`` (relativo/pacote local quebrado).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import posixpath
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from code_atlas.resolvers.capabilities import EvidenceTier, ResolverCapability
|
|
23
|
+
from code_atlas.resolvers.contracts import (
|
|
24
|
+
ResolutionDiagnostic,
|
|
25
|
+
ResolutionRequest,
|
|
26
|
+
ResolutionResult,
|
|
27
|
+
ResolvedReference,
|
|
28
|
+
ResolverProvenance,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
RESOLVER_ID = "syntactic"
|
|
32
|
+
RESOLVER_VERSION = "1.0.0"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ModuleCtx:
|
|
37
|
+
"""Contexto de resolução de um módulo parseado."""
|
|
38
|
+
|
|
39
|
+
path: str
|
|
40
|
+
module_name: str
|
|
41
|
+
language: str
|
|
42
|
+
nid: str
|
|
43
|
+
defines: dict[str, list[str]] = field(default_factory=dict)
|
|
44
|
+
import_targets: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ScopeUniverse:
|
|
49
|
+
"""Tabelas de escopo montadas pelo builder para a fase de resolução."""
|
|
50
|
+
|
|
51
|
+
modules: dict[str, ModuleCtx] = field(default_factory=dict)
|
|
52
|
+
by_path: dict[str, str] = field(default_factory=dict)
|
|
53
|
+
nid_mod: dict[str, ModuleCtx] = field(default_factory=dict)
|
|
54
|
+
top_packages: set[str] = field(default_factory=set)
|
|
55
|
+
leaf: dict[str, list[str]] = field(default_factory=dict)
|
|
56
|
+
qname_nid: dict[str, list[str]] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def ctx_for_path(self, path: str) -> ModuleCtx | None:
|
|
59
|
+
nid = self.by_path.get(path)
|
|
60
|
+
return self.nid_mod.get(nid) if nid else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SyntacticResolver:
|
|
64
|
+
"""Implementa ``SemanticResolver`` sobre o ``ScopeUniverse``."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, universe: ScopeUniverse | None = None) -> None:
|
|
67
|
+
self._universe = universe or ScopeUniverse()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def resolver_id(self) -> str:
|
|
71
|
+
return RESOLVER_ID
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def resolver_version(self) -> str:
|
|
75
|
+
return RESOLVER_VERSION
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def universe(self) -> ScopeUniverse:
|
|
79
|
+
return self._universe
|
|
80
|
+
|
|
81
|
+
def capabilities(self) -> ResolverCapability:
|
|
82
|
+
return ResolverCapability(
|
|
83
|
+
resolver_id=RESOLVER_ID,
|
|
84
|
+
tier=EvidenceTier.SYNTACTIC,
|
|
85
|
+
languages=frozenset(), # qualquer linguagem parseada
|
|
86
|
+
relation_types=frozenset(
|
|
87
|
+
{"IMPORTS", "CALLS", "REFERENCES", "EXTENDS", "IMPLEMENTS"}
|
|
88
|
+
),
|
|
89
|
+
detail="local scope resolution (no type info)",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _prov(self) -> ResolverProvenance:
|
|
93
|
+
return ResolverProvenance(
|
|
94
|
+
resolver_id=RESOLVER_ID,
|
|
95
|
+
resolver_version=RESOLVER_VERSION,
|
|
96
|
+
evidence_tier=int(EvidenceTier.SYNTACTIC),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# -- contrato SemanticResolver --------------------------------------
|
|
100
|
+
|
|
101
|
+
def resolve(self, request: ResolutionRequest) -> ResolutionResult:
|
|
102
|
+
ctx = self._universe.ctx_for_path(request.path)
|
|
103
|
+
if ctx is None:
|
|
104
|
+
return ResolutionResult(
|
|
105
|
+
request=request,
|
|
106
|
+
diagnostics=(
|
|
107
|
+
ResolutionDiagnostic(
|
|
108
|
+
code="module-not-indexed",
|
|
109
|
+
severity="warning",
|
|
110
|
+
message=f"no module context for {request.path}",
|
|
111
|
+
resolver_id=RESOLVER_ID,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
if request.kind == "import":
|
|
116
|
+
nid, status = self.resolve_import(ctx, request.name)
|
|
117
|
+
else:
|
|
118
|
+
nid, status = self.resolve_name(ctx, request.name)
|
|
119
|
+
conf = "resolved" if status == "resolved" else "syntactic"
|
|
120
|
+
return ResolutionResult(
|
|
121
|
+
request=request,
|
|
122
|
+
reference=ResolvedReference(
|
|
123
|
+
status=status,
|
|
124
|
+
confidence_class=conf,
|
|
125
|
+
provenance=self._prov(),
|
|
126
|
+
target_id=nid,
|
|
127
|
+
target_text=request.name,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# -- primitivas de resolução (extraídas de graph.builder) ------------
|
|
132
|
+
|
|
133
|
+
def scope_nid(
|
|
134
|
+
self, ctx: ModuleCtx, scope_path: tuple[str, ...]
|
|
135
|
+
) -> str | None:
|
|
136
|
+
qname = ".".join((ctx.module_name, *scope_path))
|
|
137
|
+
nids = self._universe.qname_nid.get(qname)
|
|
138
|
+
return nids[0] if nids else ctx.nid
|
|
139
|
+
|
|
140
|
+
def by_qname_or_module(self, ctx: ModuleCtx, qname: str) -> str | None:
|
|
141
|
+
nids = self._universe.qname_nid.get(qname)
|
|
142
|
+
if nids:
|
|
143
|
+
return nids[0]
|
|
144
|
+
# adapters podem reportar from_name sem prefixo de módulo
|
|
145
|
+
nids = self._universe.qname_nid.get(f"{ctx.module_name}.{qname}")
|
|
146
|
+
if nids:
|
|
147
|
+
return nids[0]
|
|
148
|
+
return ctx.nid # fallback: origem = módulo
|
|
149
|
+
|
|
150
|
+
def resolve_import(
|
|
151
|
+
self, ctx: ModuleCtx, target: str
|
|
152
|
+
) -> tuple[str | None, str]:
|
|
153
|
+
"""IMPORTS: módulo local → resolved; relativo/path falho →
|
|
154
|
+
unresolved; stdlib/site-packages → external."""
|
|
155
|
+
mod, _rest = self.locate(ctx, target)
|
|
156
|
+
if mod is not None:
|
|
157
|
+
return mod.nid, "resolved"
|
|
158
|
+
if target.startswith(".") or (
|
|
159
|
+
target.rsplit(".", 1)[0] in self._universe.top_packages
|
|
160
|
+
):
|
|
161
|
+
return None, "unresolved"
|
|
162
|
+
return None, "external"
|
|
163
|
+
|
|
164
|
+
def locate(
|
|
165
|
+
self, ctx: ModuleCtx, target: str
|
|
166
|
+
) -> tuple[ModuleCtx | None, list[str]]:
|
|
167
|
+
"""Alvo dotted/pathed → (ctx do módulo, segmentos-membro restantes).
|
|
168
|
+
|
|
169
|
+
Cobre: python absoluto (``pkg.util``), python relativo (``.util``),
|
|
170
|
+
TS relativo (``./util``, ``../x``).
|
|
171
|
+
"""
|
|
172
|
+
if target.startswith(("./", "../")):
|
|
173
|
+
base, _, member = target.rpartition(".")
|
|
174
|
+
nid = self.resolve_ts_relative(ctx, base)
|
|
175
|
+
rest = [member] if member and member != "*" else []
|
|
176
|
+
if nid is None: # último segmento era do path, não member
|
|
177
|
+
nid = self.resolve_ts_relative(ctx, target)
|
|
178
|
+
rest = []
|
|
179
|
+
mod = self._universe.nid_mod.get(nid) if nid else None
|
|
180
|
+
return mod, rest
|
|
181
|
+
cand = target
|
|
182
|
+
if target.startswith("."): # python relativo
|
|
183
|
+
dots = len(target) - len(target.lstrip("."))
|
|
184
|
+
rest_txt = target.lstrip(".")
|
|
185
|
+
parts = ctx.module_name.split(".")
|
|
186
|
+
# lf-0130: `pkg/__init__.py` É o pacote `pkg` — `.` já resolve
|
|
187
|
+
# para o módulo inteiro, não para o pai dele (`from .api
|
|
188
|
+
# import x` no __init__ → ``pkg.api.x``, não ``api.x``).
|
|
189
|
+
if posixpath.basename(ctx.path) == "__init__.py":
|
|
190
|
+
dots -= 1
|
|
191
|
+
pkg_parts = parts[: max(len(parts) - dots, 0)]
|
|
192
|
+
cand = ".".join([*pkg_parts, rest_txt]) if rest_txt else ".".join(
|
|
193
|
+
pkg_parts
|
|
194
|
+
)
|
|
195
|
+
segs = cand.split(".")
|
|
196
|
+
for i in range(len(segs), 0, -1):
|
|
197
|
+
mod = self._universe.modules.get(".".join(segs[:i]))
|
|
198
|
+
if mod is not None:
|
|
199
|
+
return mod, segs[i:]
|
|
200
|
+
return None, segs
|
|
201
|
+
|
|
202
|
+
def resolve_ts_relative(self, ctx: ModuleCtx, target: str) -> str | None:
|
|
203
|
+
"""TS: ``./util`` → path relativo ao diretório do arquivo."""
|
|
204
|
+
if not target.startswith(("./", "../")):
|
|
205
|
+
return None
|
|
206
|
+
base_dir = posixpath.dirname(ctx.path)
|
|
207
|
+
norm = posixpath.normpath(posixpath.join(base_dir, target))
|
|
208
|
+
# lf-0130: .d.mts/.d.cts e index.d.ts cobertos — mesma família de
|
|
209
|
+
# extensões que o adapter TypeScript registra.
|
|
210
|
+
for ext in (".ts", ".tsx", ".d.ts", ".mts", ".cts",
|
|
211
|
+
".d.mts", ".d.cts",
|
|
212
|
+
"/index.ts", "/index.tsx", "/index.d.ts"):
|
|
213
|
+
nid = self._universe.by_path.get(norm + ext)
|
|
214
|
+
if nid:
|
|
215
|
+
return nid
|
|
216
|
+
return self._universe.by_path.get(norm)
|
|
217
|
+
|
|
218
|
+
def resolve_name(
|
|
219
|
+
self, ctx: ModuleCtx, name: str
|
|
220
|
+
) -> tuple[str | None, str]:
|
|
221
|
+
"""Nome bare/dotted → nó local único, senão status defensável."""
|
|
222
|
+
head, _, tail = name.partition(".")
|
|
223
|
+
if not tail and name in ctx.defines and name in ctx.import_targets:
|
|
224
|
+
# def local + binding de import competem pelo mesmo nome —
|
|
225
|
+
# sem ordem textual o desfecho honesto é ambiguous, nunca
|
|
226
|
+
# um "resolved" para o alvo plausível-mas-errado (lf-0024).
|
|
227
|
+
return None, "ambiguous"
|
|
228
|
+
if head in ctx.import_targets:
|
|
229
|
+
mod, rest = self.locate(ctx, ctx.import_targets[head])
|
|
230
|
+
if mod is None:
|
|
231
|
+
return None, "external"
|
|
232
|
+
chain = [s for s in (*rest, tail) if s and s != "*"]
|
|
233
|
+
if not chain:
|
|
234
|
+
return mod.nid, "resolved"
|
|
235
|
+
hits = sorted(set(mod.defines.get(chain[0], [])))
|
|
236
|
+
if len(hits) == 1:
|
|
237
|
+
return hits[0], "resolved"
|
|
238
|
+
if hits:
|
|
239
|
+
return None, "ambiguous"
|
|
240
|
+
return None, "unresolved"
|
|
241
|
+
if tail:
|
|
242
|
+
# `obj.attr` sem tipo de obj — nunca resolve por coincidência
|
|
243
|
+
return None, "unresolved"
|
|
244
|
+
local = sorted(set(ctx.defines.get(name, [])))
|
|
245
|
+
if len(local) == 1:
|
|
246
|
+
return local[0], "resolved"
|
|
247
|
+
if len(local) > 1:
|
|
248
|
+
return None, "ambiguous"
|
|
249
|
+
return None, "unresolved"
|
|
250
|
+
|
|
251
|
+
def resolve_call(
|
|
252
|
+
self, ctx: ModuleCtx, callee: str
|
|
253
|
+
) -> tuple[str | None, str]:
|
|
254
|
+
return self.resolve_name(ctx, callee)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Subsistema de busca (Cycle 2 Phase 6 + Cycle 4 lf-0036).
|
|
2
|
+
|
|
3
|
+
lf-0119: a faceta de domínio (``DomainSearchHit``/``DomainEntitySearch``)
|
|
4
|
+
e a busca federada (``code_atlas.search.federated`` — importada pelas
|
|
5
|
+
superfícies, não reexportada aqui para não puxar ``code_atlas.federation``
|
|
6
|
+
em todo consumidor do engine).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from code_atlas.search.contracts import (
|
|
10
|
+
DomainSearchHit,
|
|
11
|
+
SearchCapability,
|
|
12
|
+
SearchDiagnostic,
|
|
13
|
+
SearchHit,
|
|
14
|
+
SearchRequest,
|
|
15
|
+
SearchResult,
|
|
16
|
+
)
|
|
17
|
+
from code_atlas.search.domain import DomainEntitySearch
|
|
18
|
+
from code_atlas.search.engine import SearchEngine
|
|
19
|
+
from code_atlas.search.structural import SearchFilters, StructuralSearch
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DomainEntitySearch",
|
|
23
|
+
"DomainSearchHit",
|
|
24
|
+
"SearchCapability",
|
|
25
|
+
"SearchDiagnostic",
|
|
26
|
+
"SearchEngine",
|
|
27
|
+
"SearchFilters",
|
|
28
|
+
"SearchHit",
|
|
29
|
+
"SearchRequest",
|
|
30
|
+
"SearchResult",
|
|
31
|
+
"StructuralSearch",
|
|
32
|
+
]
|