code-atlas-forge 1.1.0__py3-none-any.whl

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