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,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
|
+
]
|