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