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