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,378 @@
1
+ """Construtor do grafo canônico (Cycle 2 Phase 4).
2
+
3
+ Puro e determinístico: ``ScanResult`` + ``ParseResult``s →
4
+ ``GraphBuildResult``. Resolução é apenas de escopo sintático local —
5
+ alvos que não resolvem com segurança mantêm o texto e o status
6
+ ``unresolved``/``ambiguous``. Resolução semântica profunda é Cycle 3.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from code_atlas.contracts.enums import EdgeKind, NodeKind
12
+ from code_atlas.discovery.models import ScanResult
13
+ from code_atlas.graph import identity
14
+ from code_atlas.parsers.contracts import (
15
+ EvidenceRecord,
16
+ ExtractedReference,
17
+ ExtractedRelation,
18
+ GraphBuildResult,
19
+ GraphEdge,
20
+ GraphNode,
21
+ ParseDiagnostic,
22
+ ParseResult,
23
+ SourceSpan,
24
+ )
25
+ from code_atlas.resolvers.syntactic.resolver import (
26
+ ModuleCtx,
27
+ ScopeUniverse,
28
+ SyntacticResolver,
29
+ )
30
+
31
+ _RESOLVER_ID = "syntactic-scope-v1"
32
+
33
+ # alias histórico — o contexto vive em resolvers.syntactic (lf-0016)
34
+ _ModuleCtx = ModuleCtx
35
+
36
+ _SYMBOL_KINDS = {
37
+ "class": NodeKind.CLASS,
38
+ "interface": NodeKind.INTERFACE,
39
+ "function": NodeKind.FUNCTION,
40
+ "method": NodeKind.METHOD,
41
+ "symbol": NodeKind.SYMBOL,
42
+ }
43
+
44
+
45
+ class GraphBuilder:
46
+ """Constrói GraphBuildResult a partir de resultados de parsing."""
47
+
48
+ @property
49
+ def universe(self) -> ScopeUniverse:
50
+ """Universo de escopo da última ``build()`` — compartilhado com
51
+ resolvers externos (SCIP/LSP) para correlação (lf-0017)."""
52
+ return self._universe
53
+
54
+ def build(
55
+ self,
56
+ repo_name: str,
57
+ scan: ScanResult,
58
+ results: list[ParseResult],
59
+ ) -> GraphBuildResult:
60
+ self._nodes: dict[str, GraphNode] = {}
61
+ self._edges: list[GraphEdge] = []
62
+ self._edge_keys: set[tuple[str, str, str | None, str, int]] = set()
63
+ self._evidence: list[EvidenceRecord] = []
64
+ self._evi_index: dict[
65
+ tuple[str, SourceSpan | None, str, str], int
66
+ ] = {}
67
+ self._diagnostics: list[ParseDiagnostic] = []
68
+ # universo de escopo montado durante a construção e compartilhado
69
+ # com o SyntacticResolver na fase de resolução (lf-0016)
70
+ self._universe = ScopeUniverse()
71
+ self._resolver = SyntacticResolver(self._universe)
72
+ self._modules = self._universe.modules # module_name -> ctx
73
+ self._by_path = self._universe.by_path # path -> module nid
74
+ self._nid_mod = self._universe.nid_mod # module nid -> ctx
75
+ self._top_packages = self._universe.top_packages
76
+ self._leaf = self._universe.leaf # nome simples -> nids (snapshot)
77
+ self._qname_nid = self._universe.qname_nid
78
+
79
+ parsed = {r.path: r for r in results if r.supported}
80
+ repo_nid = identity.repo_node_id(repo_name)
81
+ self._nodes[repo_nid] = GraphNode(
82
+ node_id=repo_nid, kind=NodeKind.REPOSITORY, name=repo_name,
83
+ qualified_name=repo_name,
84
+ )
85
+
86
+ for f in scan.files:
87
+ self._file_hierarchy(repo_nid, f.path, f.sha256 or "")
88
+ for res in results:
89
+ if res.supported:
90
+ self._module_graph(res)
91
+ for res in results:
92
+ if res.supported:
93
+ self._resolve_relations(res, parsed)
94
+
95
+ nodes = tuple(sorted(self._nodes.values(), key=lambda n: n.node_id))
96
+ edges = tuple(
97
+ sorted(
98
+ self._edges,
99
+ key=lambda e: (
100
+ e.type, e.from_id, e.to_id or "", e.to_text,
101
+ -1 if e.span is None else e.span.start_byte,
102
+ ),
103
+ )
104
+ )
105
+ stats: dict[str, int] = {"nodes": len(nodes), "edges": len(edges)}
106
+ for n in nodes:
107
+ stats[f"nodes.{n.kind}"] = stats.get(f"nodes.{n.kind}", 0) + 1
108
+ for e in edges:
109
+ stats[f"edges.{e.type}"] = stats.get(f"edges.{e.type}", 0) + 1
110
+ return GraphBuildResult(
111
+ nodes=nodes,
112
+ edges=edges,
113
+ evidence=tuple(self._evidence),
114
+ diagnostics=tuple(self._diagnostics),
115
+ stats=stats,
116
+ )
117
+
118
+ # -- nós estruturais ------------------------------------------------
119
+
120
+ def _file_hierarchy(self, repo_nid: str, path: str, content_hash: str) -> None:
121
+ parts = path.split("/")
122
+ if len(parts) > 1:
123
+ self._top_packages.add(parts[0])
124
+ parent = repo_nid
125
+ for i in range(1, len(parts)):
126
+ pkg_path = "/".join(parts[:i])
127
+ nid = identity.package_node_id(pkg_path)
128
+ if nid not in self._nodes:
129
+ self._nodes[nid] = GraphNode(
130
+ node_id=nid, kind=NodeKind.PACKAGE, name=parts[i - 1],
131
+ qualified_name=pkg_path, path=pkg_path,
132
+ )
133
+ self._edge(EdgeKind.CONTAINS, parent, nid, None, "resolved",
134
+ "resolved", _RESOLVER_ID, (), None)
135
+ parent = nid
136
+ fid = identity.file_node_id(path)
137
+ self._nodes[fid] = GraphNode(
138
+ node_id=fid, kind=NodeKind.FILE, name=parts[-1],
139
+ qualified_name=path, path=path, content_hash=content_hash,
140
+ )
141
+ self._edge(EdgeKind.CONTAINS, parent, fid, None, "resolved",
142
+ "resolved", _RESOLVER_ID, (), None)
143
+
144
+ def _module_graph(self, res: ParseResult) -> None:
145
+ """Nó de módulo + símbolos + DEFINES hierárquico."""
146
+ mod_syms = [s for s in res.symbols if s.kind == "module"]
147
+ mod_name = mod_syms[0].name if mod_syms else res.path
148
+ mid = identity.module_node_id(res.language, res.path)
149
+ doc = mod_syms[0].docstring if mod_syms else None
150
+ self._nodes[mid] = GraphNode(
151
+ node_id=mid, kind=NodeKind.MODULE, name=mod_name,
152
+ qualified_name=mod_name, language=res.language,
153
+ path=res.path, span=mod_syms[0].span if mod_syms else None,
154
+ content_hash=res.content_hash, docstring=doc,
155
+ )
156
+ ctx = _ModuleCtx(res.path, mod_name, res.language, mid)
157
+ self._modules[mod_name] = ctx
158
+ self._by_path[res.path] = mid
159
+ self._leaf.setdefault(mod_name.split(".")[-1], []).append(mid)
160
+ self._qname_nid.setdefault(mod_name, []).append(mid)
161
+
162
+ file_nid = identity.file_node_id(res.path)
163
+ if file_nid in self._nodes:
164
+ self._edge(EdgeKind.CONTAINS, file_nid, mid, None, "resolved",
165
+ "resolved", _RESOLVER_ID, (), None)
166
+
167
+ # símbolos (exceto o pseudo-símbolo module)
168
+ for s in res.symbols:
169
+ if s.kind == "module":
170
+ continue
171
+ kind = _SYMBOL_KINDS.get(s.kind, NodeKind.SYMBOL)
172
+ nid = identity.symbol_node_id(res.language, s.kind, s.qualified_name)
173
+ if nid not in self._nodes:
174
+ self._nodes[nid] = GraphNode(
175
+ node_id=nid, kind=kind, name=s.name,
176
+ qualified_name=s.qualified_name, language=res.language,
177
+ path=res.path, span=s.span, signature=s.signature,
178
+ params=s.params, docstring=s.docstring,
179
+ modifiers=s.modifiers,
180
+ )
181
+ ctx.defines.setdefault(s.name, []).append(nid)
182
+ self._leaf.setdefault(s.name, []).append(nid)
183
+ self._qname_nid.setdefault(s.qualified_name, []).append(nid)
184
+
185
+ evi = self._evidence_ref(res, s.span)
186
+ parent_name = ".".join(
187
+ (mod_name, *s.scope_path)
188
+ ) if s.scope_path else mod_name
189
+ parent_nids = self._qname_nid.get(parent_name) or [mid]
190
+ self._edge(
191
+ EdgeKind.DEFINES, parent_nids[0], nid, None,
192
+ "resolved", "syntactic", res.parser, evi, s.span,
193
+ )
194
+
195
+ # binding local → alvo completo do import (para resolução de nomes)
196
+ for ref in res.references:
197
+ if ref.context == "import-member" and ref.target:
198
+ ctx.import_targets[ref.name] = ref.target
199
+ self._nid_mod[mid] = ctx
200
+
201
+ # -- arestas semânticas sintáticas -----------------------------------
202
+
203
+ def _resolve_relations(
204
+ self, res: ParseResult, parsed: dict[str, ParseResult]
205
+ ) -> None:
206
+ del parsed # resolução usa as tabelas já montadas em _module_graph
207
+ # lf-0031: lookup O(1) — o scan linear em _modules era O(n²)
208
+ # (5020 arquivos x ~5000 módulos = ~1.15s no corpus 5k).
209
+ mid = self._by_path.get(res.path)
210
+ ctx = self._nid_mod.get(mid) if mid is not None else None
211
+ if ctx is None:
212
+ return
213
+ for rel in res.relations:
214
+ self._edge_from_relation(res, ctx, rel)
215
+ for ref in res.references:
216
+ if ref.context in ("decorator", "type-ref", "jsx",
217
+ "export", "export-assignment"):
218
+ frm = self._scope_nid(ctx, ref.scope_path)
219
+ self._reference_edge(res, ctx, ref, frm)
220
+
221
+ def _edge_from_relation(
222
+ self, res: ParseResult, ctx: _ModuleCtx, rel: ExtractedRelation
223
+ ) -> None:
224
+ frm = self._by_qname_or_module(ctx, rel.from_name)
225
+ evi = self._evidence_ref(res, rel.span)
226
+ if frm is None:
227
+ self._diagnostics.append(
228
+ ParseDiagnostic(
229
+ code="dangling-from",
230
+ severity="warning",
231
+ message=f"relation source not found: {rel.from_name}",
232
+ span=rel.span,
233
+ )
234
+ )
235
+ return
236
+ if rel.type == "IMPORTS":
237
+ to_nid, status = self._resolve_import(ctx, rel.to_name)
238
+ conf = "resolved" if status == "resolved" else "syntactic"
239
+ self._edge(EdgeKind.IMPORTS, frm, to_nid, rel.to_name,
240
+ status, conf, res.parser, evi, rel.span, res.path)
241
+ elif rel.type in ("EXTENDS", "IMPLEMENTS"):
242
+ to_nid, status = self._resolve_name(ctx, rel.to_name)
243
+ conf = "resolved" if status == "resolved" else "syntactic"
244
+ kind = EdgeKind.EXTENDS if rel.type == "EXTENDS" else EdgeKind.IMPLEMENTS
245
+ self._edge(kind, frm, to_nid, rel.to_name, status, conf,
246
+ res.parser, evi, rel.span, res.path)
247
+ elif rel.type == "CALLS":
248
+ to_nid, status = self._resolve_call(ctx, rel.to_name)
249
+ conf = "resolved" if status == "resolved" else "syntactic"
250
+ self._edge(EdgeKind.CALLS, frm, to_nid, rel.to_name, status,
251
+ conf, res.parser, evi, rel.span, res.path)
252
+
253
+ def _reference_edge(
254
+ self, res: ParseResult, ctx: _ModuleCtx,
255
+ ref: ExtractedReference, frm: str | None,
256
+ ) -> None:
257
+ if frm is None:
258
+ return
259
+ name, span = ref.name, ref.span
260
+ to_nid, status = self._resolve_name(ctx, name)
261
+ self._edge(
262
+ EdgeKind.REFERENCES, frm, to_nid, name,
263
+ status, "syntactic", res.parser, self._evidence_ref(res, span),
264
+ span, res.path,
265
+ )
266
+
267
+ # -- resolução de escopo sintático ----------------------------------
268
+ # Delegada ao SyntacticResolver sobre o ScopeUniverse (lf-0016) —
269
+ # comportamento idêntico ao do Cycle 2, agora atrás do contrato
270
+ # SemanticResolver para composição com SCIP/LSP.
271
+
272
+ def _scope_nid(self, ctx: _ModuleCtx, scope_path: tuple[str, ...]) -> str | None:
273
+ return self._resolver.scope_nid(ctx, scope_path)
274
+
275
+ def _by_qname_or_module(self, ctx: _ModuleCtx, qname: str) -> str | None:
276
+ return self._resolver.by_qname_or_module(ctx, qname)
277
+
278
+ def _resolve_import(
279
+ self, ctx: _ModuleCtx, target: str
280
+ ) -> tuple[str | None, str]:
281
+ """IMPORTS: módulo local → resolved; relativo/path falho →
282
+ unresolved; stdlib/site-packages → external."""
283
+ return self._resolver.resolve_import(ctx, target)
284
+
285
+ def _locate(
286
+ self, ctx: _ModuleCtx, target: str
287
+ ) -> tuple[_ModuleCtx | None, list[str]]:
288
+ """Alvo dotted/pathed → (ctx do módulo, segmentos-membro restantes).
289
+
290
+ Cobre: python absoluto (`pkg.util`), python relativo (`.util`),
291
+ TS relativo (`./util`, `../x`).
292
+ """
293
+ return self._resolver.locate(ctx, target)
294
+
295
+ def _resolve_ts_relative(self, ctx: _ModuleCtx, target: str) -> str | None:
296
+ """TS: `./util` → path relativo ao diretório do arquivo."""
297
+ return self._resolver.resolve_ts_relative(ctx, target)
298
+
299
+ def _resolve_name(
300
+ self, ctx: _ModuleCtx, name: str
301
+ ) -> tuple[str | None, str]:
302
+ """Nome bare/dotted → nó local único, senão status defensável.
303
+
304
+ Regra: `resolved` só quando há exatamente um alvo no snapshot —
305
+ nunca por coincidência textual.
306
+ """
307
+ return self._resolver.resolve_name(ctx, name)
308
+
309
+ def _resolve_call(
310
+ self, ctx: _ModuleCtx, callee: str
311
+ ) -> tuple[str | None, str]:
312
+ return self._resolver.resolve_call(ctx, callee)
313
+
314
+ # -- helpers ---------------------------------------------------------
315
+
316
+ def _evidence_ref(self, res: ParseResult, span: SourceSpan | None) -> tuple[int, ...]:
317
+ rec = EvidenceRecord(
318
+ path=res.path,
319
+ span=span,
320
+ content_hash=res.content_hash,
321
+ extractor=res.parser,
322
+ extractor_version=f"{res.parser_version}+{res.grammar_version}",
323
+ )
324
+ key = (rec.path, rec.span, rec.content_hash, rec.extractor)
325
+ if key not in self._evi_index:
326
+ self._evi_index[key] = len(self._evidence)
327
+ self._evidence.append(rec)
328
+ return (self._evi_index[key],)
329
+
330
+ def _edge(
331
+ self,
332
+ type_: EdgeKind,
333
+ from_id: str,
334
+ to_id: str | None,
335
+ to_text: str | None,
336
+ status: str,
337
+ confidence: str,
338
+ resolver: str,
339
+ evidence: tuple[int, ...],
340
+ span: SourceSpan | None,
341
+ source_path: str = "",
342
+ ) -> None:
343
+ if to_id is None and not to_text:
344
+ # lf-0130: aresta sem alvo resolvido NEM texto é sempre
345
+ # inválida (validate_graph falha o snapshot inteiro). Um
346
+ # adapter que emita relação degenerada vira diagnóstico —
347
+ # nunca derruba a indexação pelos outros N arquivos.
348
+ self._diagnostics.append(
349
+ ParseDiagnostic(
350
+ code="empty-edge-target",
351
+ severity="warning",
352
+ message=(
353
+ f"{type_} edge dropped: no resolved target and "
354
+ f"empty to_text (from {from_id})"
355
+ ),
356
+ span=span,
357
+ )
358
+ )
359
+ return
360
+ key = (type_, from_id, to_id, to_text or "",
361
+ -1 if span is None else span.start_byte)
362
+ if key in self._edge_keys:
363
+ return
364
+ self._edge_keys.add(key)
365
+ self._edges.append(
366
+ GraphEdge(
367
+ type=type_,
368
+ from_id=from_id,
369
+ to_id=to_id,
370
+ to_text=to_text or "",
371
+ resolution_status=status,
372
+ confidence_class=confidence,
373
+ resolver=resolver,
374
+ evidence_index=evidence,
375
+ span=span,
376
+ source_path=source_path,
377
+ )
378
+ )
@@ -0,0 +1,54 @@
1
+ """Identidade determinística de nós e arestas.
2
+
3
+ Regra inegociável (Phase 3/4): nunca usar número de linha como
4
+ identidade. IDs são derivados de entradas estáveis — kind, path,
5
+ qualified_name — o que torna o grafo reproduzível entre processos.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+
12
+ from code_atlas.parsers.contracts import GraphEdge, SourceSpan
13
+
14
+
15
+ def node_id(kind: str, *parts: str) -> str:
16
+ """ID legível e determinístico: ``kind:part1:part2``.
17
+
18
+ Ex.: ``file:pkg/a.py``, ``sym:python:function:pkg.a.f``.
19
+ """
20
+ return ":".join((kind, *parts))
21
+
22
+
23
+ def symbol_node_id(language: str, kind: str, qualified_name: str) -> str:
24
+ return node_id("sym", language, kind, qualified_name)
25
+
26
+
27
+ def file_node_id(path: str) -> str:
28
+ return node_id("file", path)
29
+
30
+
31
+ def module_node_id(language: str, path: str) -> str:
32
+ return node_id("module", language, path)
33
+
34
+
35
+ def package_node_id(dir_path: str) -> str:
36
+ return node_id("package", dir_path)
37
+
38
+
39
+ def repo_node_id(name: str) -> str:
40
+ return node_id("repository", name)
41
+
42
+
43
+ def edge_key(edge: GraphEdge, span: SourceSpan | None) -> str:
44
+ """Chave determinística da aresta (dedup e ordenação)."""
45
+ raw = "|".join(
46
+ (
47
+ edge.type,
48
+ edge.from_id,
49
+ edge.to_id or "",
50
+ edge.to_text,
51
+ "" if span is None else f"{span.start_byte}:{span.end_byte}",
52
+ )
53
+ )
54
+ return hashlib.sha256(raw.encode()).hexdigest()[:24]
@@ -0,0 +1,47 @@
1
+ """Consultas tipadas em memória sobre GraphBuildResult.
2
+
3
+ Camada leve usada pelo storage (lf-0011) e pela CLI futura — sem I/O.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from code_atlas.parsers.contracts import GraphBuildResult, GraphEdge, GraphNode
9
+
10
+
11
+ class GraphView:
12
+ """Visão indexada de um GraphBuildResult."""
13
+
14
+ def __init__(self, result: GraphBuildResult) -> None:
15
+ self.result = result
16
+ self._by_id = {n.node_id: n for n in result.nodes}
17
+ self._from: dict[str, list[GraphEdge]] = {}
18
+ self._to: dict[str, list[GraphEdge]] = {}
19
+ for e in result.edges:
20
+ self._from.setdefault(e.from_id, []).append(e)
21
+ if e.to_id is not None:
22
+ self._to.setdefault(e.to_id, []).append(e)
23
+
24
+ def node(self, node_id: str) -> GraphNode | None:
25
+ return self._by_id.get(node_id)
26
+
27
+ def by_kind(self, kind: str) -> list[GraphNode]:
28
+ return [n for n in self.result.nodes if n.kind == kind]
29
+
30
+ def by_name(self, name: str) -> list[GraphNode]:
31
+ return [n for n in self.result.nodes if n.name == name]
32
+
33
+ def find(self, qualified_name: str) -> list[GraphNode]:
34
+ return [
35
+ n for n in self.result.nodes if n.qualified_name == qualified_name
36
+ ]
37
+
38
+ def edges_from(self, node_id: str, type_: str | None = None) -> list[GraphEdge]:
39
+ out = self._from.get(node_id, [])
40
+ return [e for e in out if type_ is None or e.type == type_]
41
+
42
+ def edges_to(self, node_id: str, type_: str | None = None) -> list[GraphEdge]:
43
+ out = self._to.get(node_id, [])
44
+ return [e for e in out if type_ is None or e.type == type_]
45
+
46
+ def stats(self) -> dict[str, int]:
47
+ return dict(self.result.stats)
@@ -0,0 +1,92 @@
1
+ """Validação estrutural do grafo (Gate 4).
2
+
3
+ Checagens: integridade referencial (endpoints existem), unicidade de IDs,
4
+ vocabulário de enums, índices de evidência válidos e ausência de ciclos
5
+ em CONTAINS. Não há snapshot aqui — isolamento entre snapshots é
6
+ garantido por constraint no storage (lf-0011) e auditado no coordinator.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from code_atlas.contracts.enums import EdgeKind, NodeKind
12
+ from code_atlas.parsers.contracts import GraphBuildResult, GraphEdge
13
+
14
+
15
+ class ValidationError(Exception):
16
+ """Violação de integridade do grafo."""
17
+
18
+
19
+ _RESOLUTION_STATUSES = frozenset(
20
+ {"resolved", "ambiguous", "unresolved", "external"}
21
+ )
22
+ _CONFIDENCE_CLASSES = frozenset(
23
+ {"resolved", "syntactic", "heuristic", "inferred"}
24
+ )
25
+
26
+
27
+ def validate_graph(result: GraphBuildResult) -> list[str]:
28
+ """Retorna lista de violações (vazia = íntegro)."""
29
+ errors: list[str] = []
30
+ errors.extend(_check_nodes(result))
31
+ node_ids = {n.node_id for n in result.nodes}
32
+ for e in result.edges:
33
+ _check_edge(e, node_ids, len(result.evidence), errors)
34
+ errors.extend(_contains_cycles(result))
35
+ return errors
36
+
37
+
38
+ def _check_nodes(result: GraphBuildResult) -> list[str]:
39
+ errors: list[str] = []
40
+ node_ids = {n.node_id for n in result.nodes}
41
+ if len(node_ids) != len(result.nodes):
42
+ errors.append("duplicate node ids")
43
+ valid = set(NodeKind)
44
+ for n in result.nodes:
45
+ if n.kind not in valid:
46
+ errors.append(f"invalid node kind: {n.kind}")
47
+ if not n.node_id:
48
+ errors.append("empty node_id")
49
+ return errors
50
+
51
+
52
+ def _check_edge(
53
+ e: GraphEdge, node_ids: set[str], evi_len: int, errors: list[str]
54
+ ) -> None:
55
+ if e.type not in set(EdgeKind):
56
+ errors.append(f"invalid edge type: {e.type}")
57
+ if e.from_id not in node_ids:
58
+ errors.append(f"dangling from_id: {e.from_id}")
59
+ if e.to_id is not None and e.to_id not in node_ids:
60
+ errors.append(f"dangling to_id: {e.to_id}")
61
+ if e.to_id is None and not e.to_text:
62
+ errors.append(f"edge without target text: {e.type} from {e.from_id}")
63
+ if e.resolution_status not in _RESOLUTION_STATUSES:
64
+ errors.append(f"invalid resolution_status: {e.resolution_status}")
65
+ if e.confidence_class not in _CONFIDENCE_CLASSES:
66
+ errors.append(f"invalid confidence_class: {e.confidence_class}")
67
+ for i in e.evidence_index:
68
+ if not 0 <= i < evi_len:
69
+ errors.append(f"evidence index out of range: {i}")
70
+
71
+
72
+ def _contains_cycles(result: GraphBuildResult) -> list[str]:
73
+ """CONTAINS deve ser uma árvore (DAG); ciclo = violação."""
74
+ child_of: dict[str, str] = {}
75
+ errors: list[str] = []
76
+ for e in result.edges:
77
+ if e.type != EdgeKind.CONTAINS or e.to_id is None:
78
+ continue
79
+ if e.to_id in child_of:
80
+ errors.append(f"CONTAINS multi-parent: {e.to_id}")
81
+ continue
82
+ child_of[e.to_id] = e.from_id
83
+ for start in child_of:
84
+ seen = set()
85
+ cur = start
86
+ while cur in child_of:
87
+ if cur in seen:
88
+ errors.append(f"CONTAINS cycle at {start}")
89
+ break
90
+ seen.add(cur)
91
+ cur = child_of[cur]
92
+ return errors
@@ -0,0 +1,37 @@
1
+ """Impact Engine — análise determinística de impacto de mudanças (Cycle 5).
2
+
3
+ Fatos vêm de snapshots persistidos + Git subprocess read-only.
4
+ Conectividade de grafo indica dependência, nunca certeza de quebra.
5
+ """
6
+
7
+ from code_atlas.impact.contracts import (
8
+ ANALYZER_VERSION,
9
+ IMPACT_SCHEMA_VERSION,
10
+ ChangeKind,
11
+ ChangeSet,
12
+ ContractChange,
13
+ EvidenceStrength,
14
+ FileChange,
15
+ FindingKind,
16
+ ImpactCandidate,
17
+ ImpactDiagnostic,
18
+ ImpactKind,
19
+ ImpactLimits,
20
+ ImpactPath,
21
+ ImpactReport,
22
+ ImpactRequest,
23
+ ImpactResult,
24
+ RiskAssessment,
25
+ RiskCategory,
26
+ SymbolChange,
27
+ TestCandidate,
28
+ )
29
+
30
+ __all__ = [
31
+ "ANALYZER_VERSION", "IMPACT_SCHEMA_VERSION",
32
+ "ChangeKind", "ChangeSet", "ContractChange", "EvidenceStrength",
33
+ "FileChange", "FindingKind", "ImpactCandidate", "ImpactDiagnostic",
34
+ "ImpactKind", "ImpactLimits", "ImpactPath", "ImpactReport",
35
+ "ImpactRequest", "ImpactResult", "RiskAssessment", "RiskCategory",
36
+ "SymbolChange", "TestCandidate",
37
+ ]