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,181 @@
1
+ """Varredura de referências + sanitização/Redaction (lf-0115).
2
+
3
+ Regras de segurança (inespeciáveis):
4
+
5
+ - **segredos nunca saem**: qualquer valor cuja chave case com
6
+ ``password|secret|token|key|credential`` (case-insensitive) é
7
+ substituído por ``[REDACTED]`` — o valor bruto nunca entra em
8
+ entidades, atributos, evidência ou ``to_unresolved``;
9
+ - **interpolação não é valor**: todo ``${…}`` vira o marcador
10
+ ``${dynamic}`` — nunca avaliamos nem copiamos a expressão interna
11
+ para metadados persistidos;
12
+ - **count/for_each não expandem**: multiplicidade declarada é
13
+ registrada como ``dynamic`` — nunca fabricamos instâncias;
14
+ - referências ``var.``/``local.``/``each.``/``count.``/``self.``/
15
+ ``path.``/``terraform.`` não viram arestas (não são entidades de
16
+ domínio).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from dataclasses import dataclass
23
+
24
+ # Chaves sensíveis — spec lf-0115: password|secret|token|key|credential.
25
+ SECRET_KEY_RE = re.compile(
26
+ r"password|secret|token|key|credential", re.IGNORECASE
27
+ )
28
+
29
+ REDACTED = "[REDACTED]"
30
+ DYNAMIC = "${dynamic}"
31
+
32
+ # ``aws_s3_bucket.logs`` — primeiro segmento DEVE conter ``_`` (tipos de
33
+ # recurso sempre têm provider_nome); lookbehind impede match em
34
+ # ``var.foo_bar.x``, ``local.a_b`` e dentro de ``module.x.aws_y_z``.
35
+ RESOURCE_REF_RE = re.compile(
36
+ r"(?<![.\w])([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)"
37
+ r"\.([A-Za-z0-9_-]+)(?![\w-]*\()"
38
+ )
39
+
40
+ # ``data.aws_x.y`` — fontes de dados (leitura, não managed).
41
+ DATA_REF_RE = re.compile(
42
+ r"(?<![.\w])data\.([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)"
43
+ r"\.([A-Za-z0-9_-]+)"
44
+ )
45
+
46
+ # ``module.name.attr`` — referência a output de módulo.
47
+ MODULE_REF_RE = re.compile(
48
+ r"(?<![.\w])module\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)"
49
+ )
50
+
51
+ # ARNs literais — recursos externos ao snapshot (REFERENCES external).
52
+ ARN_RE = re.compile(r"arn:[a-z0-9-]+:[a-z0-9-]+:[a-z0-9-]*:[0-9]*:[^\s\"'\\)]+")
53
+
54
+ # ``${…}`` superficial — sanitização de valores antes de armazenar.
55
+ _INTERP_START_RE = re.compile(r"\$\{")
56
+
57
+ # Formatos de credencial de alta precisão — um valor que *parece* segredo
58
+ # vaza mesmo sob chave benigna (``description = "AKIA…"``, env-var
59
+ # ``stage = "ghp_…"``). O trecho casa só marcadores inequívocos de
60
+ # provedores conhecidos; nunca classifica texto comum (lf-c8 F5).
61
+ SECRET_VALUE_RE = re.compile(
62
+ r"AKIA[0-9A-Z]{16}" # AWS access key id
63
+ r"|ASIA[0-9A-Z]{16}" # AWS temporary access key id
64
+ r"|-----BEGIN [A-Z0-9 ]{0,32}PRIVATE KEY-----"
65
+ r"|github_pat_[A-Za-z0-9_]{22,}"
66
+ r"|gh[pousr]_[A-Za-z0-9]{36,}" # ghp_/gho_/ghu_/ghs_/ghr_
67
+ r"|xox[baprs]-[A-Za-z0-9-]{10,}" # Slack tokens
68
+ r"|AIza[0-9A-Za-z_-]{35}" # GCP API key
69
+ r"|sk_live_[A-Za-z0-9]{16,}" # Stripe live secret
70
+ r"|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}" # JWT
71
+ )
72
+
73
+
74
+ def is_sensitive_name(name: str) -> bool:
75
+ """True se a chave/nome casa o padrão de segredos (spec)."""
76
+ return bool(SECRET_KEY_RE.search(name))
77
+
78
+
79
+ def looks_like_secret_value(text: str) -> bool:
80
+ """True se ``text`` contém trecho com formato de credencial conhecida."""
81
+ return bool(SECRET_VALUE_RE.search(text))
82
+
83
+
84
+ def mask_secret_values(text: str) -> str:
85
+ """Redige trechos com formato de credencial — defesa em profundidade
86
+ para valores sob chaves benignas."""
87
+ return SECRET_VALUE_RE.sub(REDACTED, text)
88
+
89
+
90
+ def sanitize_value(text: str, *, max_len: int = 200) -> str:
91
+ """Substitui ``${…}`` balanceados por ``${dynamic}`` e trunca.
92
+
93
+ Nunca avalia a expressão — o conteúdo interpolado é descartado e
94
+ substituído pelo marcador dinâmico (spec: "dynamic marker, not
95
+ concrete value").
96
+ """
97
+ out: list[str] = []
98
+ i, n = 0, len(text)
99
+ while i < n:
100
+ m = _INTERP_START_RE.search(text, i)
101
+ if m is None:
102
+ out.append(text[i:])
103
+ break
104
+ out.append(text[i:m.start()])
105
+ depth = 1
106
+ j = m.end()
107
+ while j < n and depth:
108
+ if text[j] == "{":
109
+ depth += 1
110
+ elif text[j] == "}":
111
+ depth -= 1
112
+ j += 1
113
+ out.append(DYNAMIC)
114
+ i = j
115
+ s = "".join(out).strip()
116
+ return mask_secret_values(s[:max_len])
117
+
118
+
119
+ def redact_if_sensitive(key: str, value: str) -> str:
120
+ """``value`` sanitizado, ou ``[REDACTED]`` se ``key`` é sensível."""
121
+ if is_sensitive_name(key):
122
+ return REDACTED
123
+ return sanitize_value(value)
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class RefMatch:
128
+ """Referência encontrada num texto — offset em bytes do arquivo."""
129
+
130
+ kind: str # resource | data | module | arn
131
+ type_name: str # "aws_s3_bucket" | "module" | "data" | "arn"
132
+ name: str # label do alvo ("logs") ou texto do ARN
133
+ start: int # byte offset do início do match
134
+ end: int
135
+
136
+
137
+ def _byte_offsets(text: str, start_char: int, end_char: int) -> tuple[int, int]:
138
+ """Char offsets → byte offsets UTF-8 (trechos curtos, barato)."""
139
+ return (
140
+ len(text[:start_char].encode("utf-8")),
141
+ len(text[:end_char].encode("utf-8")),
142
+ )
143
+
144
+
145
+ def scan_refs(text: str, *, base_byte: int = 0) -> list[RefMatch]:
146
+ """Todas as referências terraform em ``text`` (ordem de posição).
147
+
148
+ ``base_byte`` soma-se aos offsets quando ``text`` é um slice do
149
+ arquivo (corpo de bloco). Inclui refs a recursos managed
150
+ (``aws_x.y``), data sources (``data.aws_x.y``), saídas de módulo
151
+ (``module.a.b``) e ARNs literais.
152
+ """
153
+ out: list[RefMatch] = []
154
+ for m in DATA_REF_RE.finditer(text):
155
+ s, e = _byte_offsets(text, m.start(), m.end())
156
+ out.append(
157
+ RefMatch("data", m.group(1), m.group(2), base_byte + s, base_byte + e)
158
+ )
159
+ for m in RESOURCE_REF_RE.finditer(text):
160
+ s, e = _byte_offsets(text, m.start(), m.end())
161
+ out.append(
162
+ RefMatch(
163
+ "resource", m.group(1), m.group(2),
164
+ base_byte + s, base_byte + e,
165
+ )
166
+ )
167
+ for m in MODULE_REF_RE.finditer(text):
168
+ s, e = _byte_offsets(text, m.start(), m.end())
169
+ out.append(
170
+ RefMatch(
171
+ "module", "module", m.group(1),
172
+ base_byte + s, base_byte + e,
173
+ )
174
+ )
175
+ for m in ARN_RE.finditer(text):
176
+ s, e = _byte_offsets(text, m.start(), m.end())
177
+ out.append(
178
+ RefMatch("arn", "arn", m.group(0), base_byte + s, base_byte + e)
179
+ )
180
+ out.sort(key=lambda r: r.start)
181
+ return out
@@ -0,0 +1,163 @@
1
+ """Leitura estrutural de ``.tf.json`` (lf-0115).
2
+
3
+ Terraform aceita configuração em JSON: ``{"resource": {"aws_x":
4
+ {"name": {…attrs…}}}}``. Este leitor converte o objeto em
5
+ ``HclBlock`` equivalentes — ``body`` é a serialização JSON do objeto
6
+ do bloco e os ``value_span`` dos atributos são offsets **dentro do
7
+ body** (body_start=0), mantendo a classificação de referências
8
+ (``depends_on`` explícito, atributo de documento) consistente no
9
+ mesmo espaço de coordenadas.
10
+
11
+ Spans de entidade/evidência: o JSON stdlib não expõe offsets —
12
+ aproximamos pelo token ``"<name>"`` no texto real do arquivo
13
+ (``_key_span``); evidência aponta para conteúdo do inventário, com
14
+ precisão declarada como aproximada.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from dataclasses import dataclass
21
+
22
+ from code_atlas.domains.terraform.hcl import HclAttribute, HclBlock, HclDiagnostic
23
+ from code_atlas.parsers.contracts import SourceSpan
24
+
25
+ _ZERO = SourceSpan(0, 0, 0, 0, 0, 0)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class TfJsonFile:
30
+ blocks: tuple[HclBlock, ...] = ()
31
+ diagnostics: tuple[HclDiagnostic, ...] = ()
32
+
33
+
34
+ def _key_span(text: str, key: str) -> SourceSpan | None:
35
+ """Span aproximado do token ``"<key>"`` no texto do arquivo."""
36
+ needle = json.dumps(key, ensure_ascii=False)
37
+ idx = text.find(needle)
38
+ if idx < 0:
39
+ return None
40
+ start_b = len(text[:idx].encode("utf-8"))
41
+ end_b = start_b + len(needle.encode("utf-8"))
42
+ line = text.count("\n", 0, idx) + 1
43
+ line_start = text.rfind("\n", 0, idx) + 1
44
+ col = len(text[line_start:idx].encode("utf-8"))
45
+ return SourceSpan(
46
+ start_byte=start_b, end_byte=end_b,
47
+ start_line=line, start_col=col,
48
+ end_line=line, end_col=col + (end_b - start_b),
49
+ )
50
+
51
+
52
+ def _body_span(start: int, end: int) -> SourceSpan:
53
+ """Span em coordenadas do corpo serializado (linha 1 fictícia)."""
54
+ return SourceSpan(
55
+ start_byte=start, end_byte=end,
56
+ start_line=1, start_col=start, end_line=1, end_col=end,
57
+ )
58
+
59
+
60
+ def _attrs_of_body(body_text: str, obj: object) -> tuple[HclAttribute, ...]:
61
+ """``{k: v}`` → atributos; ``value_span`` em offsets do ``body``.
62
+
63
+ A região do valor vai do fim do token ``"k"`` até o início da
64
+ próxima chave irmã (ordem preservada pelo ``json``) — aproximação
65
+ contendo o valor serializado inteiro.
66
+ """
67
+ if not isinstance(obj, dict):
68
+ return ()
69
+ out: list[HclAttribute] = []
70
+ positions: list[tuple[str, int]] = []
71
+ cursor = 0
72
+ for k in obj:
73
+ token = json.dumps(str(k), ensure_ascii=False)
74
+ idx = body_text.find(token, cursor)
75
+ if idx < 0:
76
+ idx = body_text.find(token)
77
+ if idx >= 0:
78
+ positions.append((str(k), idx))
79
+ cursor = idx + len(token)
80
+ else:
81
+ positions.append((str(k), -1))
82
+ for i, (key, idx) in enumerate(positions):
83
+ raw = json.dumps(obj[key], ensure_ascii=False)
84
+ if idx < 0:
85
+ span = _ZERO
86
+ else:
87
+ vstart = idx + len(json.dumps(str(k), ensure_ascii=False)) + 1
88
+ vend = (
89
+ positions[i + 1][1]
90
+ if i + 1 < len(positions) and positions[i + 1][1] > vstart
91
+ else len(body_text)
92
+ )
93
+ span = _body_span(vstart, vend)
94
+ out.append(
95
+ HclAttribute(name=key, value=raw, span=span, value_span=span)
96
+ )
97
+ return tuple(out)
98
+
99
+
100
+ def _block(
101
+ text: str,
102
+ btype: str,
103
+ labels: tuple[str, ...],
104
+ name_key: str,
105
+ body: object,
106
+ ) -> HclBlock:
107
+ body_text = json.dumps(body, ensure_ascii=False)
108
+ span = _key_span(text, name_key)
109
+ return HclBlock(
110
+ type=btype, labels=labels,
111
+ attributes=_attrs_of_body(body_text, body),
112
+ body=body_text, body_start=0,
113
+ span=span, header_span=span,
114
+ )
115
+
116
+
117
+ def _section_blocks(
118
+ text: str, section: str, content: dict[str, object]
119
+ ) -> list[HclBlock]:
120
+ """Uma seção top-level → blocos ``HclBlock`` equivalentes."""
121
+ out: list[HclBlock] = []
122
+ if section in ("resource", "data"):
123
+ # {"resource": {"aws_x": {"name": {…}}}}
124
+ for rtype, named in content.items():
125
+ if not isinstance(named, dict):
126
+ continue
127
+ for name, body in named.items():
128
+ out.append(
129
+ _block(
130
+ text, section,
131
+ (str(rtype), str(name)), str(name), body,
132
+ )
133
+ )
134
+ elif section in ("module", "variable", "output", "provider"):
135
+ for name, body in content.items():
136
+ out.append(_block(text, section, (str(name),), str(name), body))
137
+ else:
138
+ # locals / terraform / seções desconhecidas — bloco anônimo
139
+ out.append(_block(text, section, (), section, content))
140
+ return out
141
+
142
+
143
+ def parse_tfjson(text: str) -> TfJsonFile:
144
+ """Parse estrutural de ``*.tf.json`` — nunca levanta."""
145
+ try:
146
+ data = json.loads(text)
147
+ except (ValueError, json.JSONDecodeError) as exc:
148
+ return TfJsonFile(
149
+ diagnostics=(HclDiagnostic("json-parse-error", str(exc)),)
150
+ )
151
+ if not isinstance(data, dict):
152
+ return TfJsonFile(
153
+ diagnostics=(
154
+ HclDiagnostic(
155
+ "json-parse-error", "tf.json root must be an object"
156
+ ),
157
+ )
158
+ )
159
+ blocks: list[HclBlock] = []
160
+ for section, content in data.items():
161
+ if isinstance(content, dict):
162
+ blocks.extend(_section_blocks(text, str(section), content))
163
+ return TfJsonFile(blocks=tuple(blocks))
@@ -0,0 +1,98 @@
1
+ """Leitura estrutural de ``.tfvars`` / ``.tfvars.json`` (lf-0115).
2
+
3
+ tfvars é HCL restrito a atribuições top-level — reusa ``parse_hcl``
4
+ sem nenhum caminho de execução. ``.tfvars.json`` é JSON puro.
5
+
6
+ Segurança: valores cuja chave casa o padrão de segredos nunca são
7
+ devolvidos — o caller recebe o marcador ``[REDACTED]``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass
14
+
15
+ from code_atlas.domains.terraform.hcl import HclDiagnostic, parse_hcl
16
+ from code_atlas.domains.terraform.refs import (
17
+ REDACTED,
18
+ is_sensitive_name,
19
+ looks_like_secret_value,
20
+ sanitize_value,
21
+ )
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TfvarsAssignment:
26
+ """Atribuição ``name = value`` de um arquivo tfvars."""
27
+
28
+ name: str
29
+ value: str # texto bruto (sanitizado/redigido pelo caller via flags)
30
+ redacted: bool # True quando a chave é sensível — value == "[REDACTED]"
31
+ line: int = 0
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TfvarsFile:
36
+ assignments: tuple[TfvarsAssignment, ...] = ()
37
+ diagnostics: tuple[HclDiagnostic, ...] = ()
38
+
39
+
40
+ def _unquote(value: str) -> str:
41
+ v = value.strip()
42
+ if len(v) >= 2 and v.startswith('"') and v.endswith('"'):
43
+ return v[1:-1]
44
+ return v
45
+
46
+
47
+ def _assignment(name: str, raw: str, line: int) -> TfvarsAssignment:
48
+ if is_sensitive_name(name):
49
+ return TfvarsAssignment(name, REDACTED, True, line)
50
+ # valor com formato de credencial sob chave benigna — mesma redação
51
+ # que chave sensível; nunca entra em env-hints nem atributos (F5).
52
+ if looks_like_secret_value(_unquote(raw)):
53
+ return TfvarsAssignment(name, REDACTED, True, line)
54
+ return TfvarsAssignment(name, sanitize_value(_unquote(raw)), False, line)
55
+
56
+
57
+ def parse_tfvars(text: str) -> TfvarsFile:
58
+ """Parse de ``*.tfvars`` (HCL assignments) — nunca levanta."""
59
+ hcl = parse_hcl(text)
60
+ out = [
61
+ _assignment(a.name, a.value, a.span.start_line)
62
+ for a in hcl.attributes
63
+ ]
64
+ diags = list(hcl.diagnostics)
65
+ for b in hcl.blocks: # tfvars não tem blocos — honesto, não crash
66
+ diags.append(
67
+ HclDiagnostic(
68
+ code="unexpected-block",
69
+ message=f"block {b.type!r} in tfvars file",
70
+ line=(b.header_span.start_line if b.header_span else 0),
71
+ )
72
+ )
73
+ return TfvarsFile(assignments=tuple(out), diagnostics=tuple(diags))
74
+
75
+
76
+ def parse_tfvars_json(text: str) -> TfvarsFile:
77
+ """Parse de ``*.tfvars.json`` — objeto JSON chave→valor."""
78
+ try:
79
+ data = json.loads(text)
80
+ except (ValueError, json.JSONDecodeError) as exc:
81
+ return TfvarsFile(
82
+ diagnostics=(
83
+ HclDiagnostic("json-parse-error", str(exc)),
84
+ )
85
+ )
86
+ if not isinstance(data, dict):
87
+ return TfvarsFile(
88
+ diagnostics=(
89
+ HclDiagnostic(
90
+ "json-parse-error", "tfvars.json root must be an object"
91
+ ),
92
+ )
93
+ )
94
+ out = [
95
+ _assignment(str(k), json.dumps(v, ensure_ascii=False), 0)
96
+ for k, v in data.items()
97
+ ]
98
+ return TfvarsFile(assignments=tuple(out))
@@ -0,0 +1,120 @@
1
+ """Validação de saída de adapters de domínio (lf-0110).
2
+
3
+ Espelha ``graph.validation``: retorna lista de violações (vazia = ok),
4
+ nunca levanta. Checagens:
5
+
6
+ - ``result.domain_id``/``adapter_version`` casam com o adapter;
7
+ - kinds de entidade/relação ⊆ vocabulário declarado pelo adapter;
8
+ - identidades das entidades têm ``domain`` == adapter e repositório ==
9
+ request;
10
+ - confiança declarada respeita o ``ConfidenceModel`` (teto + inferência);
11
+ - índices de evidência em faixa; invariantes de ``DomainResolution``
12
+ (inferred nunca resolved — reforço: o dataclass já garante, aqui
13
+ reporta-se como violação, não exceção).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from code_atlas.contracts.enums import ConfidenceClass, ResolutionStatus
19
+ from code_atlas.domains.contracts import (
20
+ DOMAIN_CONTRACT_VERSION,
21
+ DomainAdapter,
22
+ DomainExtractionRequest,
23
+ DomainExtractionResult,
24
+ )
25
+ from code_atlas.domains.model import DOMAIN_RELATION_TYPES
26
+
27
+
28
+ def _check_envelope(
29
+ adapter: DomainAdapter, result: DomainExtractionResult
30
+ ) -> list[str]:
31
+ errors: list[str] = []
32
+ if result.schema_version != DOMAIN_CONTRACT_VERSION:
33
+ errors.append(f"schema_version mismatch: {result.schema_version}")
34
+ if result.domain_id != adapter.domain_id:
35
+ errors.append(
36
+ f"result.domain_id {result.domain_id!r} != adapter "
37
+ f"{adapter.domain_id!r}"
38
+ )
39
+ if result.adapter_version != adapter.adapter_version:
40
+ errors.append(
41
+ f"result.adapter_version {result.adapter_version!r} != adapter "
42
+ f"{adapter.adapter_version!r}"
43
+ )
44
+ return errors
45
+
46
+
47
+ def _check_entities(
48
+ adapter: DomainAdapter,
49
+ request: DomainExtractionRequest,
50
+ result: DomainExtractionResult,
51
+ ) -> list[str]:
52
+ errors: list[str] = []
53
+ declared = adapter.entity_kinds()
54
+ evi_len = len(result.evidence)
55
+ identities: set[str] = set()
56
+ for e in result.entities:
57
+ key = e.identity.canonical_key()
58
+ if key in identities:
59
+ errors.append(f"duplicate entity identity: {e.identity.display()}")
60
+ identities.add(key)
61
+ if e.kind not in declared:
62
+ errors.append(f"undeclared entity kind: {e.kind}")
63
+ if e.identity.domain != adapter.domain_id:
64
+ errors.append(
65
+ f"entity domain {e.identity.domain!r} != adapter "
66
+ f"{adapter.domain_id!r}"
67
+ )
68
+ if e.identity.repository != request.repository:
69
+ errors.append(
70
+ f"entity repository {e.identity.repository!r} != request "
71
+ f"{request.repository!r}"
72
+ )
73
+ for i in e.evidence:
74
+ if not 0 <= i < evi_len:
75
+ errors.append(f"entity evidence index out of range: {i}")
76
+ return errors
77
+
78
+
79
+ def _check_relations(
80
+ adapter: DomainAdapter, result: DomainExtractionResult
81
+ ) -> list[str]:
82
+ errors: list[str] = []
83
+ declared = adapter.relation_kinds()
84
+ confidence = adapter.confidence_model()
85
+ evi_len = len(result.evidence)
86
+ for r in result.relations:
87
+ if r.relation_type not in DOMAIN_RELATION_TYPES:
88
+ errors.append(f"invalid relation_type: {r.relation_type!r}")
89
+ elif r.relation_type not in declared:
90
+ errors.append(f"undeclared relation kind: {r.relation_type!r}")
91
+ if r.to_identity is None and not r.to_symbol and not r.to_unresolved:
92
+ errors.append("relation without target")
93
+ res = r.resolution
94
+ if res is not None:
95
+ if (
96
+ res.confidence_class is ConfidenceClass.INFERRED
97
+ and res.status is ResolutionStatus.RESOLVED
98
+ ):
99
+ errors.append("inferred confidence reported resolved")
100
+ if not confidence.admits(res.confidence_class):
101
+ errors.append(
102
+ f"confidence {res.confidence_class} exceeds adapter "
103
+ f"ceiling {confidence.ceiling}"
104
+ )
105
+ for i in r.evidence:
106
+ if not 0 <= i < evi_len:
107
+ errors.append(f"relation evidence index out of range: {i}")
108
+ return errors
109
+
110
+
111
+ def validate_extraction(
112
+ adapter: DomainAdapter,
113
+ request: DomainExtractionRequest,
114
+ result: DomainExtractionResult,
115
+ ) -> list[str]:
116
+ """Violações do resultado contra o contrato declarado do adapter."""
117
+ errors = _check_envelope(adapter, result)
118
+ errors.extend(_check_entities(adapter, request, result))
119
+ errors.extend(_check_relations(adapter, result))
120
+ return errors
@@ -0,0 +1,27 @@
1
+ """Contexto de override temporário de variáveis de ambiente.
2
+
3
+ Fora dos pacotes ``domains/``/``federation/``/``eval/`` — o sweep de
4
+ segurança proíbe ``import os`` nesses diretórios (portabilidade de
5
+ paths); manipulação de ambiente vive aqui.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+
14
+
15
+ @contextmanager
16
+ def patched_env(overrides: dict[str, str]) -> Iterator[None]:
17
+ """Aplica ``overrides`` e restaura o ambiente anterior ao sair."""
18
+ old = {k: os.environ.get(k) for k in overrides}
19
+ os.environ.update(overrides)
20
+ try:
21
+ yield
22
+ finally:
23
+ for k, v in old.items():
24
+ if v is None:
25
+ os.environ.pop(k, None)
26
+ else:
27
+ os.environ[k] = v
@@ -0,0 +1,26 @@
1
+ """Evaluation metrics — métricas de retrieval matematicamente
2
+ validadas (lf-0048). Sem dependências; determinístico."""
3
+
4
+ from code_atlas.eval.metrics import (
5
+ MetricRecord,
6
+ aggregate,
7
+ average_precision_at_k,
8
+ diversity,
9
+ hit_rate_at_k,
10
+ mrr,
11
+ ndcg_at_k,
12
+ precision_at_k,
13
+ recall_at_k,
14
+ )
15
+
16
+ __all__ = [
17
+ "MetricRecord",
18
+ "aggregate",
19
+ "average_precision_at_k",
20
+ "diversity",
21
+ "hit_rate_at_k",
22
+ "mrr",
23
+ "ndcg_at_k",
24
+ "precision_at_k",
25
+ "recall_at_k",
26
+ ]