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,171 @@
|
|
|
1
|
+
"""Blast radius — BFS reverso de dependências (Phase 5).
|
|
2
|
+
|
|
3
|
+
Aresta resolvida ``from → to`` significa "from depende de to"; para
|
|
4
|
+
impacto, consumidores do símbolo alterado = arestas ``from`` cujo
|
|
5
|
+
``to_id`` aponta para a ocorrência alterada. Uma aresta
|
|
6
|
+
unresolved/ambiguous no caminho → ``fully_verified=False`` → impacto
|
|
7
|
+
rebaixado para ``possible`` (nunca promovido).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sqlite3
|
|
13
|
+
from collections import deque
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from code_atlas.impact.contracts import (
|
|
17
|
+
EvidenceStrength,
|
|
18
|
+
ImpactCandidate,
|
|
19
|
+
ImpactKind,
|
|
20
|
+
ImpactLimits,
|
|
21
|
+
ImpactPath,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_IN_SQL = (
|
|
25
|
+
"SELECT id, from_id, to_id, type, resolution_status"
|
|
26
|
+
" FROM relations WHERE snapshot_id = ? AND to_id = ?"
|
|
27
|
+
" ORDER BY id LIMIT ?"
|
|
28
|
+
)
|
|
29
|
+
_SYM_SQL = (
|
|
30
|
+
"SELECT s.qualified_name, s.language, s.kind, o.id AS occ_id,"
|
|
31
|
+
" f.relative_path FROM symbol_occurrences o"
|
|
32
|
+
" JOIN symbols s ON s.id = o.logical_symbol_id"
|
|
33
|
+
" JOIN source_files f ON f.id = o.source_file_id"
|
|
34
|
+
" WHERE o.id = ?"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class _Hit:
|
|
40
|
+
key: str
|
|
41
|
+
qname: str
|
|
42
|
+
language: str
|
|
43
|
+
kind: str
|
|
44
|
+
occ_id: int
|
|
45
|
+
path: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sym(conn: sqlite3.Connection, occ_id: int) -> _Hit | None:
|
|
49
|
+
row = conn.execute(_SYM_SQL, (occ_id,)).fetchone()
|
|
50
|
+
if row is None:
|
|
51
|
+
return None
|
|
52
|
+
key = f"{row['language']}:{row['kind']}:{row['qualified_name']}"
|
|
53
|
+
return _Hit(key, str(row["qualified_name"]), str(row["language"]),
|
|
54
|
+
str(row["kind"]), int(row["occ_id"]),
|
|
55
|
+
str(row["relative_path"]))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _frontier_pending(
|
|
59
|
+
conn: sqlite3.Connection, snapshot_id: int, occ: int,
|
|
60
|
+
origin_key: str, origins: dict[str, int],
|
|
61
|
+
visited: set[tuple[int, str]], limits: ImpactLimits,
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""``True`` se o nó de fronteira (emitido em ``hops >= max_depth``,
|
|
64
|
+
nunca enfileirado) ainda tinha consumidor atravessável para a
|
|
65
|
+
origem — ou o scan de vizinhos excedeu ``max_nodes`` (sonda +1:
|
|
66
|
+
a cauda não-vista pode esconder trabalho, lf-0151)."""
|
|
67
|
+
rows = conn.execute(
|
|
68
|
+
_IN_SQL, (snapshot_id, occ, limits.max_nodes + 1)).fetchall()
|
|
69
|
+
if len(rows) > limits.max_nodes:
|
|
70
|
+
return True
|
|
71
|
+
for r in rows:
|
|
72
|
+
consumer_occ = int(r["from_id"])
|
|
73
|
+
if (consumer_occ, origin_key) in visited:
|
|
74
|
+
continue
|
|
75
|
+
hit = _sym(conn, consumer_occ)
|
|
76
|
+
if hit is None or hit.key in origins:
|
|
77
|
+
continue
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def blast(
|
|
83
|
+
conn: sqlite3.Connection, snapshot_id: int,
|
|
84
|
+
origins: dict[str, int], # symbol_key → occurrence_id alterado
|
|
85
|
+
limits: ImpactLimits,
|
|
86
|
+
*,
|
|
87
|
+
reason_suffix: str = "",
|
|
88
|
+
) -> tuple[tuple[ImpactCandidate, ...], bool]:
|
|
89
|
+
"""BFS de consumidores; retorna (candidatos, truncated).
|
|
90
|
+
|
|
91
|
+
``truncated`` é a união de todos os bounds que descartaram
|
|
92
|
+
trabalho (lf-0151): emissão no cap ``max_paths``, varredura de
|
|
93
|
+
vizinhos de um nó no cap ``max_nodes`` (prefixo ``ORDER BY id``
|
|
94
|
+
determinístico — rows além dele nunca vistas) e nós na fronteira
|
|
95
|
+
``hops >= max_depth`` com consumidores ainda atravessáveis. A
|
|
96
|
+
discriminação por razão é exposta por
|
|
97
|
+
``domain_traversal.blast``/``ImpactResult.truncation_reasons``.
|
|
98
|
+
"""
|
|
99
|
+
conn.row_factory = sqlite3.Row
|
|
100
|
+
out: list[ImpactCandidate] = []
|
|
101
|
+
# lf-0079: visita por (nó, origem) — duas origens até o mesmo
|
|
102
|
+
# consumidor preservam caminhos alternativos; ciclo continua
|
|
103
|
+
# cortado por (nó visitado na mesma linha de origem).
|
|
104
|
+
visited: set[tuple[int, str]] = set()
|
|
105
|
+
queue: deque[tuple[int, str, list[int], list[str], list[str], int]] = (
|
|
106
|
+
deque())
|
|
107
|
+
for key, occ in origins.items():
|
|
108
|
+
queue.append((occ, key, [], [], [], 0))
|
|
109
|
+
edge_scan_capped = False
|
|
110
|
+
frontier: list[tuple[int, str]] = []
|
|
111
|
+
|
|
112
|
+
while queue:
|
|
113
|
+
occ, origin_key, rel_ids, mids, statuses, depth = queue.popleft()
|
|
114
|
+
vkey = (occ, origin_key)
|
|
115
|
+
if vkey in visited or depth > limits.max_depth:
|
|
116
|
+
continue
|
|
117
|
+
visited.add(vkey)
|
|
118
|
+
# +1 linha de sonda: distingue fit exato de overflow real —
|
|
119
|
+
# só o overflow descarta trabalho (lf-0151).
|
|
120
|
+
rows = conn.execute(
|
|
121
|
+
_IN_SQL, (snapshot_id, occ,
|
|
122
|
+
limits.max_nodes + 1)).fetchall()
|
|
123
|
+
if len(rows) > limits.max_nodes:
|
|
124
|
+
edge_scan_capped = True
|
|
125
|
+
rows = rows[:limits.max_nodes]
|
|
126
|
+
for r in rows:
|
|
127
|
+
consumer_occ = int(r["from_id"])
|
|
128
|
+
if (consumer_occ, origin_key) in visited:
|
|
129
|
+
continue
|
|
130
|
+
hit = _sym(conn, consumer_occ)
|
|
131
|
+
if hit is None or hit.key in origins:
|
|
132
|
+
continue
|
|
133
|
+
new_rels = [*rel_ids, int(r["id"])]
|
|
134
|
+
new_mid = [*mids, hit.key]
|
|
135
|
+
new_st = [*statuses, str(r["resolution_status"])]
|
|
136
|
+
verified = all(s == "resolved" for s in new_st)
|
|
137
|
+
hops = len(new_rels)
|
|
138
|
+
kind = (ImpactKind.DIRECT if hops == 1 and verified
|
|
139
|
+
else ImpactKind.TRANSITIVE if verified
|
|
140
|
+
else ImpactKind.POSSIBLE)
|
|
141
|
+
strength = (EvidenceStrength.VERIFIED if verified
|
|
142
|
+
else EvidenceStrength.POSSIBLE)
|
|
143
|
+
path = ImpactPath(
|
|
144
|
+
origin_symbol_key=origin_key,
|
|
145
|
+
affected_symbol_key=hit.key, hops=hops,
|
|
146
|
+
relation_ids=tuple(new_rels),
|
|
147
|
+
intermediate_keys=tuple(new_mid),
|
|
148
|
+
resolution_statuses=tuple(new_st),
|
|
149
|
+
fully_verified=verified,
|
|
150
|
+
reason=(f"{r['type']} via relation {r['id']}"
|
|
151
|
+
+ reason_suffix))
|
|
152
|
+
out.append(ImpactCandidate(
|
|
153
|
+
symbol_key=hit.key, qualified_name=hit.qname,
|
|
154
|
+
language=hit.language, kind=hit.kind, impact=kind,
|
|
155
|
+
strength=strength, path=path,
|
|
156
|
+
source_path=hit.path, occurrence_id=hit.occ_id))
|
|
157
|
+
if len(out) >= limits.max_paths:
|
|
158
|
+
return tuple(out), True
|
|
159
|
+
if hops < limits.max_depth:
|
|
160
|
+
queue.append((consumer_occ, origin_key,
|
|
161
|
+
new_rels, new_mid, new_st, depth + 1))
|
|
162
|
+
else:
|
|
163
|
+
# fronteira de depth: emitido mas nunca expandido —
|
|
164
|
+
# sonda decide se o corte descartou trabalho.
|
|
165
|
+
frontier.append((consumer_occ, origin_key))
|
|
166
|
+
|
|
167
|
+
depth_cut = any(
|
|
168
|
+
_frontier_pending(conn, snapshot_id, occ, key, origins,
|
|
169
|
+
visited, limits)
|
|
170
|
+
for occ, key in dict.fromkeys(frontier))
|
|
171
|
+
return tuple(out), edge_scan_capped or depth_cut
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Análise de contratos públicos por assinatura (Phase 4).
|
|
2
|
+
|
|
3
|
+
NÃO é type-checker: compara a *forma* da assinatura persistida
|
|
4
|
+
(``fts_occurrences.signature``). Classificações determinísticas;
|
|
5
|
+
"potentially incompatible" ≠ "quebra comprovada" — a quebra exige
|
|
6
|
+
consumidor incompatível demonstrado separadamente (fora do escopo).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from code_atlas.impact.contracts import (
|
|
14
|
+
ContractChange,
|
|
15
|
+
EvidenceStrength,
|
|
16
|
+
SymbolChange,
|
|
17
|
+
SymbolChangeKind,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_PARAM_RE = re.compile(r"\((.*)\)")
|
|
21
|
+
_OPT_RE = re.compile(r"[=?]|\w+\s*[:=]")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _params(sig: str) -> list[str]:
|
|
25
|
+
m = _PARAM_RE.search(sig)
|
|
26
|
+
if not m:
|
|
27
|
+
return []
|
|
28
|
+
return [p.strip() for p in m.group(1).split(",") if p.strip()
|
|
29
|
+
and p.strip() not in ("self", "cls")]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _required(params: list[str]) -> list[str]:
|
|
33
|
+
"""Obrigatórios: sem ``=``, sem ``?`` (TS), não *args/**kwargs."""
|
|
34
|
+
out = []
|
|
35
|
+
for p in params:
|
|
36
|
+
if p.startswith(("*", "/")) or p.startswith("..."):
|
|
37
|
+
continue
|
|
38
|
+
name = p.split(":")[0].split("=")[0].strip().rstrip("?")
|
|
39
|
+
if "=" not in p and not p.rstrip().endswith("?"):
|
|
40
|
+
out.append(name)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def classify_change(
|
|
45
|
+
sc: SymbolChange,
|
|
46
|
+
) -> ContractChange | None:
|
|
47
|
+
"""Classifica uma SymbolChange c/ assinaturas; None se sem contrato."""
|
|
48
|
+
old, new = sc.old_signature or "", sc.new_signature or ""
|
|
49
|
+
if sc.change == SymbolChangeKind.REMOVED:
|
|
50
|
+
return ContractChange(
|
|
51
|
+
symbol_key=sc.symbol_key,
|
|
52
|
+
classification="public-symbol-removed",
|
|
53
|
+
compatibility="potential-risk",
|
|
54
|
+
old_signature=old or None, new_signature=None,
|
|
55
|
+
strength=sc.strength,
|
|
56
|
+
detail="símbolo removido do grafo alvo")
|
|
57
|
+
if sc.change == SymbolChangeKind.ADDED:
|
|
58
|
+
return None # adição não quebra contrato existente
|
|
59
|
+
if sc.change != SymbolChangeKind.SIGNATURE_CHANGED:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
po, pn = _params(old), _params(new)
|
|
63
|
+
ro, rn = set(_required(po)), set(_required(pn))
|
|
64
|
+
cls, compat, detail = "signature-changed", "indeterminate", ""
|
|
65
|
+
|
|
66
|
+
added_req = sorted(rn - ro)
|
|
67
|
+
removed_req = sorted(ro - rn)
|
|
68
|
+
if added_req:
|
|
69
|
+
cls, compat = "required-parameter-added", "potential-risk"
|
|
70
|
+
detail = f"novos obrigatórios: {', '.join(added_req)}"
|
|
71
|
+
elif removed_req:
|
|
72
|
+
cls, compat = "required-parameter-removed", "likely-compatible"
|
|
73
|
+
detail = ("remoção de parâmetro obrigatório é compatível "
|
|
74
|
+
"para chamadores que o passavam por nome")
|
|
75
|
+
elif len(pn) > len(po):
|
|
76
|
+
cls, compat = "optional-parameter-added", "likely-compatible"
|
|
77
|
+
detail = "apenas parâmetros opcionais adicionados"
|
|
78
|
+
elif len(pn) < len(po):
|
|
79
|
+
cls, compat = "parameter-removed", "potential-risk"
|
|
80
|
+
detail = "parâmetro removido pode quebrar chamadas posicionais"
|
|
81
|
+
elif po != pn:
|
|
82
|
+
cls, compat = "parameter-renamed", "potential-risk"
|
|
83
|
+
detail = "ordem/nome de parâmetros diverge"
|
|
84
|
+
else:
|
|
85
|
+
cls, compat = "no-contract-impact", "no-contract-impact"
|
|
86
|
+
detail = "mesma forma de parâmetros"
|
|
87
|
+
if old.lstrip().startswith("async") != new.lstrip().startswith("async"):
|
|
88
|
+
cls, compat = "async-changed", "potential-risk"
|
|
89
|
+
detail = "mudança sync↔async"
|
|
90
|
+
return ContractChange(
|
|
91
|
+
symbol_key=sc.symbol_key, classification=cls,
|
|
92
|
+
compatibility=compat, old_signature=old or None,
|
|
93
|
+
new_signature=new or None,
|
|
94
|
+
strength=(EvidenceStrength.VERIFIED
|
|
95
|
+
if compat != "indeterminate"
|
|
96
|
+
else EvidenceStrength.POSSIBLE),
|
|
97
|
+
detail=detail)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def analyze(
|
|
101
|
+
changes: tuple[SymbolChange, ...],
|
|
102
|
+
) -> tuple[ContractChange, ...]:
|
|
103
|
+
out = [c for c in (classify_change(s) for s in changes) if c]
|
|
104
|
+
return tuple(out)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Contratos do Impact Engine (Cycle 5 Phase 1).
|
|
2
|
+
|
|
3
|
+
Dois eixos separados — nunca um único ``confidence``:
|
|
4
|
+
- ``FindingKind``: o que o achado É (mudança, dependência, risco, teste).
|
|
5
|
+
- ``EvidenceStrength``: quão comprovado está (verified > supported >
|
|
6
|
+
possible > unknown). Uma aresta incerta não promove a cadeia.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
IMPACT_SCHEMA_VERSION = 1
|
|
15
|
+
ANALYZER_VERSION = "impact-v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FindingKind(StrEnum):
|
|
19
|
+
OBSERVED_CHANGE = "observed_change"
|
|
20
|
+
DEPENDENCY = "dependency"
|
|
21
|
+
COMPATIBILITY_RISK = "compatibility_risk"
|
|
22
|
+
TEST_RELEVANCE = "test_relevance"
|
|
23
|
+
UNKNOWN = "unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EvidenceStrength(StrEnum):
|
|
27
|
+
VERIFIED = "verified"
|
|
28
|
+
SUPPORTED = "supported"
|
|
29
|
+
POSSIBLE = "possible"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChangeKind(StrEnum):
|
|
34
|
+
"""Tipos de mudança de arquivo/símbolo (Phase 2+3)."""
|
|
35
|
+
|
|
36
|
+
ADDED = "added"
|
|
37
|
+
MODIFIED = "modified"
|
|
38
|
+
DELETED = "deleted"
|
|
39
|
+
RENAMED = "renamed"
|
|
40
|
+
COPIED = "copied"
|
|
41
|
+
MODE_CHANGED = "mode_changed"
|
|
42
|
+
TYPE_CHANGED = "type_changed"
|
|
43
|
+
BINARY = "binary"
|
|
44
|
+
UNAVAILABLE = "unavailable"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SymbolChangeKind(StrEnum):
|
|
48
|
+
"""Classificação semântica por símbolo (Phase 3)."""
|
|
49
|
+
|
|
50
|
+
ADDED = "added"
|
|
51
|
+
REMOVED = "removed"
|
|
52
|
+
RENAMED = "renamed"
|
|
53
|
+
MOVED = "moved"
|
|
54
|
+
SIGNATURE_CHANGED = "signature_changed"
|
|
55
|
+
BODY_CHANGED = "body_changed"
|
|
56
|
+
DOC_CHANGED = "doc_changed"
|
|
57
|
+
NO_SEMANTIC_CHANGE = "no_semantic_change"
|
|
58
|
+
UNKNOWN = "unknown"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ImpactKind(StrEnum):
|
|
62
|
+
DIRECT = "direct"
|
|
63
|
+
TRANSITIVE = "transitive"
|
|
64
|
+
POSSIBLE = "possible"
|
|
65
|
+
UNKNOWN = "unknown"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RiskCategory(StrEnum):
|
|
69
|
+
LOW = "low"
|
|
70
|
+
MODERATE = "moderate"
|
|
71
|
+
HIGH = "high"
|
|
72
|
+
CRITICAL = "critical"
|
|
73
|
+
UNKNOWN = "unknown"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class FileChange:
|
|
78
|
+
"""Uma entrada do ChangeSet (Phase 2)."""
|
|
79
|
+
|
|
80
|
+
kind: ChangeKind
|
|
81
|
+
old_path: str | None
|
|
82
|
+
new_path: str | None
|
|
83
|
+
old_hash: str | None = None
|
|
84
|
+
new_hash: str | None = None
|
|
85
|
+
similarity: int | None = None # 0-100 em renomes/cópias git
|
|
86
|
+
binary: bool = False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ChangeSet:
|
|
91
|
+
"""Conjunto de alterações entre duas revisões (Phase 2)."""
|
|
92
|
+
|
|
93
|
+
base_revision: str
|
|
94
|
+
target_revision: str
|
|
95
|
+
files: tuple[FileChange, ...]
|
|
96
|
+
source: str = "git" # git | snapshots | explicit
|
|
97
|
+
dirty_worktree: bool = False
|
|
98
|
+
commit_ids: tuple[str, ...] = ()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class SymbolChange:
|
|
103
|
+
"""Mudança detectada num símbolo entre dois snapshots (Phase 3)."""
|
|
104
|
+
|
|
105
|
+
symbol_key: str # lang:kind:qname (mesma forma do search)
|
|
106
|
+
qualified_name: str
|
|
107
|
+
language: str
|
|
108
|
+
kind: str
|
|
109
|
+
change: SymbolChangeKind
|
|
110
|
+
strength: EvidenceStrength
|
|
111
|
+
old_signature: str | None = None
|
|
112
|
+
new_signature: str | None = None
|
|
113
|
+
old_path: str | None = None
|
|
114
|
+
new_path: str | None = None
|
|
115
|
+
old_occurrence_id: int | None = None
|
|
116
|
+
new_occurrence_id: int | None = None
|
|
117
|
+
detail: str = ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class ContractChange:
|
|
122
|
+
"""Alteração de contrato público classificada (Phase 4)."""
|
|
123
|
+
|
|
124
|
+
symbol_key: str
|
|
125
|
+
classification: str # ex.: required-parameter-added
|
|
126
|
+
compatibility: str # potential-risk | likely-compatible
|
|
127
|
+
# | no-contract-impact | indeterminate
|
|
128
|
+
old_signature: str | None
|
|
129
|
+
new_signature: str | None
|
|
130
|
+
strength: EvidenceStrength
|
|
131
|
+
detail: str = ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ImpactPath:
|
|
136
|
+
"""Cadeia auditável origem→afetado (Phase 5)."""
|
|
137
|
+
|
|
138
|
+
origin_symbol_key: str
|
|
139
|
+
affected_symbol_key: str
|
|
140
|
+
hops: int
|
|
141
|
+
relation_ids: tuple[int, ...]
|
|
142
|
+
intermediate_keys: tuple[str, ...]
|
|
143
|
+
resolution_statuses: tuple[str, ...]
|
|
144
|
+
fully_verified: bool
|
|
145
|
+
reason: str
|
|
146
|
+
# lf-0120: refs tipadas por hop ("rel:<id>" = relations,
|
|
147
|
+
# "dom:<id>" = domain_relations) — os ids numéricos das duas
|
|
148
|
+
# tabelas compartilham espaço, então o prefixo preserva a
|
|
149
|
+
# proveniência quando o caminho mistura código e domínio.
|
|
150
|
+
edge_refs: tuple[str, ...] = ()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class ImpactCandidate:
|
|
155
|
+
"""Símbolo/arquivo potencialmente impactado (Phase 5)."""
|
|
156
|
+
|
|
157
|
+
symbol_key: str
|
|
158
|
+
qualified_name: str
|
|
159
|
+
language: str
|
|
160
|
+
kind: str
|
|
161
|
+
impact: ImpactKind
|
|
162
|
+
strength: EvidenceStrength
|
|
163
|
+
path: ImpactPath | None
|
|
164
|
+
source_path: str | None = None
|
|
165
|
+
occurrence_id: int | None = None
|
|
166
|
+
# lf-0078: estado do consumidor histórico no destino
|
|
167
|
+
# ("" | still_present | also_removed | unresolved_in_target)
|
|
168
|
+
consumer_state: str = ""
|
|
169
|
+
# lf-0120 (Cycle 7 Phase 12): tipo do nó impactado — ``"symbol"``
|
|
170
|
+
# para símbolo de código, ``"domain"`` para entidade de domínio
|
|
171
|
+
# (o kind então carrega o ``DomainEntityKind``: data_table,
|
|
172
|
+
# spark_job…). ``symbol_key`` de entidade é a ``identity_key``
|
|
173
|
+
# canônica ``dom:…`` e ``language`` é o domínio produtor.
|
|
174
|
+
node_type: str = "symbol"
|
|
175
|
+
# Ocorrência de ``domain_entity_occurrences`` — espaço de ids
|
|
176
|
+
# separado de ``occurrence_id`` (que é sempre symbol_occurrences).
|
|
177
|
+
domain_occurrence_id: int | None = None
|
|
178
|
+
# Dependências não-traversáveis do nó (alvos ``unresolved`` /
|
|
179
|
+
# ``ambiguous`` / ``external`` sem endpoint concreto) — contam como
|
|
180
|
+
# incerteza, nunca como blast confirmado. Formas curtas estáveis:
|
|
181
|
+
# ``"WRITES_TO→<texto> (unresolved)"``.
|
|
182
|
+
uncertain_deps: tuple[str, ...] = ()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class TestCandidate:
|
|
187
|
+
"""Teste candidato relacionado à mudança (Phase 7)."""
|
|
188
|
+
|
|
189
|
+
test_file: str
|
|
190
|
+
test_symbol: str | None
|
|
191
|
+
relationship: str # direct | transitive | heuristic | unknown
|
|
192
|
+
reason: str
|
|
193
|
+
strength: EvidenceStrength
|
|
194
|
+
occurrence_id: int | None = None
|
|
195
|
+
revision: str = "" # lf-0080: "base" | "target" | ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass(frozen=True)
|
|
199
|
+
class RiskAssessment:
|
|
200
|
+
"""Avaliação determinística de risco (Phase 6)."""
|
|
201
|
+
|
|
202
|
+
category: RiskCategory
|
|
203
|
+
uncertainty: str # low|moderate|high — separado do risco
|
|
204
|
+
reasons: tuple[str, ...]
|
|
205
|
+
formula_version: str = "risk-rules-v1"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class ImpactDiagnostic:
|
|
210
|
+
code: str
|
|
211
|
+
message: str
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True)
|
|
215
|
+
class ImpactLimits:
|
|
216
|
+
"""Limites da análise — cada um tem uma razão de truncamento.
|
|
217
|
+
|
|
218
|
+
Semântica medida (cycle-8.1, lf-0151): um bound que descarta
|
|
219
|
+
trabalho real é declarado em ``ImpactResult.truncation_reasons``
|
|
220
|
+
(e refletido em ``truncated``) — nunca silencioso.
|
|
221
|
+
|
|
222
|
+
- ``max_depth``: hops máximos por caminho. Um candidato emitido
|
|
223
|
+
exatamente na fronteira (``hops == max_depth``) não é expandido;
|
|
224
|
+
se ele ainda tem vizinhos atravessáveis (ou incerteza de domínio
|
|
225
|
+
não contada) o corte é declarado ``max-depth-frontier``.
|
|
226
|
+
- ``max_nodes``: cap de scan de vizinhos **por expansão** (rows de
|
|
227
|
+
``relations``/``domain_relations`` lidas por nó, prefixo
|
|
228
|
+
determinístico ``ORDER BY id``). Quando a varredura de um nó
|
|
229
|
+
enche esse limite, rows além dele nunca são vistas — o corte é
|
|
230
|
+
``max-nodes-edge-scan``. Não é um bound global de nós
|
|
231
|
+
visitados (o nome é preservado por compatibilidade).
|
|
232
|
+
- ``max_paths``: cap de emissão — o blast para quando
|
|
233
|
+
``len(candidates) >= max_paths`` → ``max-paths``.
|
|
234
|
+
- ``max_changes``: cap do diff de símbolos → ``max-changes``.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
max_depth: int = 4
|
|
238
|
+
max_nodes: int = 500
|
|
239
|
+
max_paths: int = 50
|
|
240
|
+
max_changes: int = 500
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class ImpactRequest:
|
|
245
|
+
"""Pedido de análise — todas as superfícies (CLI/MCP) viram isto."""
|
|
246
|
+
|
|
247
|
+
repository_root: str
|
|
248
|
+
base_revision: str = "" # HEAD~1, sha, branch, snapshot:N
|
|
249
|
+
target_revision: str = "" # vazio = worktree
|
|
250
|
+
symbol: str | None = None # seleção explícita (também aceita
|
|
251
|
+
# ref de entidade — ver ``entity``)
|
|
252
|
+
file: str | None = None
|
|
253
|
+
# lf-0120 (Cycle 7 Phase 12): seed de entidade de domínio — a
|
|
254
|
+
# análise passa a responder "se esta tabela/job/recurso mudar".
|
|
255
|
+
# Formas aceitas: ``identity_key`` canônica ``dom:…``, row id
|
|
256
|
+
# (``entity:N``/``id:N``/``N``), ``display()`` (ex.:
|
|
257
|
+
# ``pyspark:staging.orders``) ou nome qualificado
|
|
258
|
+
# (``staging.orders``, ``orders``). ``symbol`` também funciona como
|
|
259
|
+
# seed de entidade quando casa — fiação mínima para handlers que já
|
|
260
|
+
# expõem ``symbol`` (API/CLI/MCP não precisam mudar).
|
|
261
|
+
entity: str | None = None
|
|
262
|
+
base_snapshot_id: int | None = None
|
|
263
|
+
target_snapshot_id: int | None = None
|
|
264
|
+
include_possible: bool = True
|
|
265
|
+
known_only: bool = False
|
|
266
|
+
limits: ImpactLimits = field(default_factory=ImpactLimits)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass(frozen=True)
|
|
270
|
+
class ImpactResult:
|
|
271
|
+
"""Resposta canônica — Phase 1 exige todos os campos identificáveis."""
|
|
272
|
+
|
|
273
|
+
schema_version: int
|
|
274
|
+
analyzer_version: str
|
|
275
|
+
repository: str
|
|
276
|
+
base_revision: str
|
|
277
|
+
target_revision: str
|
|
278
|
+
base_snapshot_id: int | None
|
|
279
|
+
target_snapshot_id: int | None
|
|
280
|
+
change_set: ChangeSet | None
|
|
281
|
+
symbol_changes: tuple[SymbolChange, ...]
|
|
282
|
+
contract_changes: tuple[ContractChange, ...]
|
|
283
|
+
impacts: tuple[ImpactCandidate, ...]
|
|
284
|
+
test_candidates: tuple[TestCandidate, ...]
|
|
285
|
+
risk: RiskAssessment
|
|
286
|
+
unknown_coverage: tuple[str, ...]
|
|
287
|
+
# ``truncated`` é a união de ``truncation_reasons``: True sse
|
|
288
|
+
# algum bound descartou trabalho real — nenhuma análise incompleta
|
|
289
|
+
# se apresenta como exaustiva (cycle-8.1 Phase 8 / lf-0151).
|
|
290
|
+
truncated: bool
|
|
291
|
+
diagnostics: tuple[ImpactDiagnostic, ...]
|
|
292
|
+
# Vocabulário estável e ordenado de bounds que cortaram a análise:
|
|
293
|
+
# - ``max-paths`` — emissão do blast atingiu ``max_paths``;
|
|
294
|
+
# - ``max-changes`` — diff de símbolos atingiu ``max_changes``;
|
|
295
|
+
# - ``max-nodes-edge-scan`` — scan de vizinhos de um nó encheu
|
|
296
|
+
# ``max_nodes`` (prefixo determinístico ``ORDER BY id``);
|
|
297
|
+
# - ``max-depth-frontier`` — nó emitido em ``hops == max_depth``
|
|
298
|
+
# ainda tinha trabalho atravessável e não foi expandido;
|
|
299
|
+
# - ``symbol-seed-limit`` — lookup de seed what-if atingiu o cap
|
|
300
|
+
# de ocorrências;
|
|
301
|
+
# - ``test-candidates`` — seleção de testes atingiu o cap.
|
|
302
|
+
# ``truncation_reason``/``max_depth_reached``/``max_nodes_reached``
|
|
303
|
+
# do Phase 8 mapeiam aqui; ``max_edges``/``query_budget`` não
|
|
304
|
+
# existem como bounds separados no engine (documentado em
|
|
305
|
+
# ``docs/impact-truncation-integrity.md``). Aditivo — schema v1.
|
|
306
|
+
truncation_reasons: tuple[str, ...] = ()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(frozen=True)
|
|
310
|
+
class ImpactReport:
|
|
311
|
+
"""Envelope de relatório (Phase 9) — renderers JSON/MD/CLI."""
|
|
312
|
+
|
|
313
|
+
schema_version: int
|
|
314
|
+
result: ImpactResult
|
|
315
|
+
format: str # json | markdown | cli
|