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,318 @@
|
|
|
1
|
+
"""Tokenizer SQL em stdlib puro (lf-0111, Cycle 7 Phase 3).
|
|
2
|
+
|
|
3
|
+
Zero dependências: nenhum ``sqlparse``/``sqlglot`` — o tokenizer emite uma
|
|
4
|
+
sequência plana de ``Token`` que o parser descendente consome. Posições são
|
|
5
|
+
**offsets de caractere** sobre o texto decodificado; a conversão para bytes
|
|
6
|
+
(evidence spans) acontece em ``parser._Src``.
|
|
7
|
+
|
|
8
|
+
Decisões de honestidade:
|
|
9
|
+
|
|
10
|
+
- ``{{ }}``, ``{% %}``, ``${ }``, ``@{ }`` e ``<% %>`` viram token
|
|
11
|
+
``dynamic`` — marcadores de template/macro que impedem análise estática
|
|
12
|
+
honesta (dbt/Jinja, Spark ``${var}``, Athena ``@var``);
|
|
13
|
+
- comentários são consumidos (não emitem tokens) mas comentário de bloco
|
|
14
|
+
não-terminado vira flag — conteúdo perdido é diagnosticado, nunca
|
|
15
|
+
silenciado;
|
|
16
|
+
- ``-`` vs ``--``: comentário de linha só começa com dois hífens;
|
|
17
|
+
- identificadores entre ``"..."`` e `` `...` `` são ``qident`` —
|
|
18
|
+
capitalização preservada (política de case do adapter, ADR-0010);
|
|
19
|
+
- palavras NÃO-reservadas comuns em esquemas reais (``date``, ``key``,
|
|
20
|
+
``value``, ``type``, ``user``, ``year``…) ficam fora de ``KEYWORDS``
|
|
21
|
+
para não destruir colrefs legítimos — type-literals como
|
|
22
|
+
``DATE '2020'`` são reconhecidos por lookahead no scanner de
|
|
23
|
+
expressão, não por pertencimento léxico.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
KEYWORDS: frozenset[str] = frozenset(
|
|
31
|
+
{
|
|
32
|
+
# consulta / cláusulas
|
|
33
|
+
"SELECT", "FROM", "WHERE", "GROUP", "BY", "HAVING", "QUALIFY",
|
|
34
|
+
"ORDER", "LIMIT", "OFFSET", "FETCH", "NEXT", "ROWS", "ONLY",
|
|
35
|
+
"FIRST", "TOP", "WINDOW",
|
|
36
|
+
# combinadores e joins
|
|
37
|
+
"UNION", "INTERSECT", "EXCEPT", "ALL", "DISTINCT", "AS",
|
|
38
|
+
"JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER", "CROSS",
|
|
39
|
+
"NATURAL", "SEMI", "ANTI", "ON", "USING",
|
|
40
|
+
# CTE
|
|
41
|
+
"WITH", "RECURSIVE", "MATERIALIZED",
|
|
42
|
+
# expressão
|
|
43
|
+
"AND", "OR", "NOT", "IN", "IS", "NULL", "NULLS", "LIKE", "ILIKE",
|
|
44
|
+
"RLIKE", "REGEXP", "BETWEEN", "EXISTS", "CASE", "WHEN", "THEN",
|
|
45
|
+
"ELSE", "END", "ASC", "DESC",
|
|
46
|
+
# DML
|
|
47
|
+
"INSERT", "INTO", "OVERWRITE", "VALUES", "UPDATE", "SET",
|
|
48
|
+
"DELETE", "MERGE", "MATCHED",
|
|
49
|
+
# DDL
|
|
50
|
+
"CREATE", "REPLACE", "TEMP", "TEMPORARY", "GLOBAL", "EXTERNAL",
|
|
51
|
+
"TRANSIENT", "TABLE", "VIEW", "SCHEMA", "DATABASE", "USE",
|
|
52
|
+
"DROP", "ALTER", "TRUNCATE", "RENAME", "TO", "ADD", "COLUMN",
|
|
53
|
+
"COLUMNS", "CONSTRAINT", "PRIMARY", "FOREIGN", "REFERENCES",
|
|
54
|
+
"UNIQUE", "CHECK", "DEFAULT", "IF",
|
|
55
|
+
# opções de tabela (Athena/Spark/Hive)
|
|
56
|
+
"PARTITION", "PARTITIONED", "CLUSTERED", "SORTED", "DISTRIBUTED",
|
|
57
|
+
"SKEWED", "STORED", "LOCATION", "TBLPROPERTIES", "OPTIONS",
|
|
58
|
+
"COMMENT", "ROW", "FORMAT", "SERDE", "INPUTFORMAT", "OUTPUTFORMAT",
|
|
59
|
+
"DELIMITED", "FIELDS", "LINES", "COLLECTION", "MAP", "TERMINATED",
|
|
60
|
+
"ESCAPED", "DEFINED", "BUCKET", "BUCKETS", "ENGINE", "TABLESPACE",
|
|
61
|
+
"CHARSET", "COLLATE", "PROPERTIES",
|
|
62
|
+
# extensões de fonte de dados
|
|
63
|
+
"LATERAL", "UNNEST", "ORDINALITY", "PIVOT", "TABLESAMPLE",
|
|
64
|
+
"DISTRIBUTE", "CLUSTER", "SORT",
|
|
65
|
+
# funções analíticas/agregação com sintaxe especial
|
|
66
|
+
"OVER", "FILTER", "WITHIN", "GROUPING", "SETS", "ROLLUP", "CUBE",
|
|
67
|
+
# comandos não-SELECT (parseados como unsupported)
|
|
68
|
+
"EXPLAIN", "DESCRIBE", "SHOW", "ANALYZE", "MSCK",
|
|
69
|
+
"REPAIR", "CACHE", "UNCACHE", "REFRESH", "GRANT", "REVOKE",
|
|
70
|
+
"TRANSACTION", "COMMIT", "ROLLBACK", "BEGIN", "START", "RESET",
|
|
71
|
+
"EXEC", "EXECUTE", "CALL", "PREPARE", "DEALLOCATE", "IMMEDIATE",
|
|
72
|
+
"VACUUM", "OPTIMIZE", "LOAD", "DATA", "LOCAL", "INPATH",
|
|
73
|
+
"DIRECTORY", "EXPORT", "IMPORT", "FUNCTION", "PROCEDURE",
|
|
74
|
+
"INDEX", "TRIGGER", "SEQUENCE", "JAR", "FILE", "ROLE",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Palavras que parecem tipos/literais mas NÃO entram em KEYWORDS porque
|
|
79
|
+
# são nomes de coluna comuns em datasets reais (``date``, ``key``,
|
|
80
|
+
# ``value``, ``user``, ``type``, ``year``…). O scanner de expressão
|
|
81
|
+
# reconhece ``DATE '…'``/``TIMESTAMP '…'`` por lookahead, não por
|
|
82
|
+
# keyword-membership — decisão documentada para não destruir colrefs.
|
|
83
|
+
_TYPE_LITERAL_WORDS: frozenset[str] = frozenset(
|
|
84
|
+
{"DATE", "TIME", "TIMESTAMP", "DATETIME", "INTERVAL"}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Palavras-campo dentro de funções especiais (EXTRACT/TRIM) que não são
|
|
88
|
+
# colrefs: ``EXTRACT(YEAR FROM d)``, ``TRIM(BOTH ' ' FROM c)``.
|
|
89
|
+
_SPECIAL_ARG_FNS: frozenset[str] = frozenset(
|
|
90
|
+
{"EXTRACT", "TRIM", "SUBSTRING", "POSITION", "OVERLAY"}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
_MULTI_PUNCT: tuple[str, ...] = ("<=", ">=", "<>", "!=", "||", "::", "->", "=>")
|
|
94
|
+
|
|
95
|
+
_DYNAMIC_OPEN: dict[str, str] = {
|
|
96
|
+
"{{": "}}", "{%": "%}", "${": "}", "@{": "}", "<%": "%>",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_STRING_PREFIXES = frozenset("eExXbBnNrR")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Token:
|
|
104
|
+
"""Unidade léxica com offsets de caractere (``start``/``end``)."""
|
|
105
|
+
|
|
106
|
+
kind: str # word|qident|number|string|punct|param|dynamic|eof
|
|
107
|
+
text: str # fatia crua do texto
|
|
108
|
+
norm: str # word→UPPER (match de keyword); qident→miolo; demais→text
|
|
109
|
+
iname: str # identidade: word→lower; qident→miolo com case preservado
|
|
110
|
+
start: int
|
|
111
|
+
end: int
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_kw(tok: Token, *words: str) -> bool:
|
|
115
|
+
"""True se ``tok`` é palavra não-quotada pertencente a ``words``."""
|
|
116
|
+
return tok.kind == "word" and tok.norm in words
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_word(tok: Token) -> bool:
|
|
120
|
+
"""Qualquer identificador (quotado ou não), incluindo keywords."""
|
|
121
|
+
return tok.kind in ("word", "qident")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_name(tok: Token) -> bool:
|
|
125
|
+
"""Identificador utilizável como nome novo (não-keyword ou quotado)."""
|
|
126
|
+
return tok.kind == "qident" or (tok.kind == "word" and tok.norm not in KEYWORDS)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _scan_string(text: str, i: int, prefix: bool) -> int:
|
|
130
|
+
"""Consome ``'…'`` com escape ``''``; retorna posição após o fecho.
|
|
131
|
+
|
|
132
|
+
``-1`` quando a string não fecha até o fim do texto.
|
|
133
|
+
"""
|
|
134
|
+
n = len(text)
|
|
135
|
+
j = i + 1
|
|
136
|
+
if prefix:
|
|
137
|
+
j += 1 # letra de prefixo já incluída pelo caller
|
|
138
|
+
while j < n:
|
|
139
|
+
c = text[j]
|
|
140
|
+
if c == "'":
|
|
141
|
+
if j + 1 < n and text[j + 1] == "'":
|
|
142
|
+
j += 2
|
|
143
|
+
continue
|
|
144
|
+
return j + 1
|
|
145
|
+
j += 1
|
|
146
|
+
return -1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _scan_comment(
|
|
150
|
+
text: str, i: int, n: int, problems: list[str]
|
|
151
|
+
) -> int | None:
|
|
152
|
+
"""Comentários são consumidos — ``i`` novo ou ``None``."""
|
|
153
|
+
if text.startswith("--", i):
|
|
154
|
+
j = text.find("\n", i + 2)
|
|
155
|
+
return n if j < 0 else j + 1
|
|
156
|
+
if text.startswith("/*", i):
|
|
157
|
+
j = text.find("*/", i + 2)
|
|
158
|
+
if j < 0:
|
|
159
|
+
problems.append("unterminated-comment")
|
|
160
|
+
return n
|
|
161
|
+
return j + 2
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _scan_dynamic(
|
|
166
|
+
text: str, i: int, n: int, toks: list[Token]
|
|
167
|
+
) -> int | None:
|
|
168
|
+
"""Marcadores ``${}``/``{{}}``/``{% %}``/``@{}``/``<% %>``."""
|
|
169
|
+
for op, cl in _DYNAMIC_OPEN.items():
|
|
170
|
+
if not text.startswith(op, i):
|
|
171
|
+
continue
|
|
172
|
+
j = text.find(cl, i + len(op))
|
|
173
|
+
j = n if j < 0 else j + len(cl)
|
|
174
|
+
toks.append(Token("dynamic", text[i:j], text[i:j], text[i:j], i, j))
|
|
175
|
+
return j
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _scan_string_tok(
|
|
180
|
+
text: str, i: int, n: int, toks: list[Token], problems: list[str]
|
|
181
|
+
) -> int | None:
|
|
182
|
+
"""``'…'`` com prefixo opcional de 1 letra (``E'x'``, ``N'y'``)."""
|
|
183
|
+
c = text[i]
|
|
184
|
+
if c == "'":
|
|
185
|
+
j = _scan_string(text, i, prefix=False)
|
|
186
|
+
elif c in _STRING_PREFIXES and text.startswith("'", i + 1):
|
|
187
|
+
j = _scan_string(text, i, prefix=True)
|
|
188
|
+
else:
|
|
189
|
+
return None
|
|
190
|
+
if j < 0:
|
|
191
|
+
problems.append("unterminated-string")
|
|
192
|
+
j = n
|
|
193
|
+
toks.append(Token("string", text[i:j], text[i:j], text[i:j], i, j))
|
|
194
|
+
return j
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _scan_qident(
|
|
198
|
+
text: str, i: int, n: int, toks: list[Token], problems: list[str]
|
|
199
|
+
) -> int | None:
|
|
200
|
+
"""``"…"``/````…```` — case preservado, ``""`` é escape interno."""
|
|
201
|
+
c = text[i]
|
|
202
|
+
if c not in ('"', "`"):
|
|
203
|
+
return None
|
|
204
|
+
j = i + 1
|
|
205
|
+
closed = False
|
|
206
|
+
while j < n:
|
|
207
|
+
if text[j] == c:
|
|
208
|
+
if j + 1 < n and text[j + 1] == c:
|
|
209
|
+
j += 2
|
|
210
|
+
continue
|
|
211
|
+
closed = True
|
|
212
|
+
j += 1
|
|
213
|
+
break
|
|
214
|
+
j += 1
|
|
215
|
+
if not closed:
|
|
216
|
+
problems.append("unterminated-quoted-ident")
|
|
217
|
+
inner = text[i + 1:j - 1] if closed else text[i + 1:j]
|
|
218
|
+
inner = inner.replace(c + c, c)
|
|
219
|
+
toks.append(Token("qident", text[i:j], inner, inner, i, j))
|
|
220
|
+
return j
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _scan_word(
|
|
224
|
+
text: str, i: int, n: int, toks: list[Token]
|
|
225
|
+
) -> int | None:
|
|
226
|
+
"""Identificadores e keywords — distinção fica para o parser."""
|
|
227
|
+
c = text[i]
|
|
228
|
+
if not (c.isalpha() or c == "_"):
|
|
229
|
+
return None
|
|
230
|
+
j = i + 1
|
|
231
|
+
while j < n and (text[j].isalnum() or text[j] in "_$"):
|
|
232
|
+
j += 1
|
|
233
|
+
word = text[i:j]
|
|
234
|
+
toks.append(Token("word", word, word.upper(), word.lower(), i, j))
|
|
235
|
+
return j
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _scan_number(
|
|
239
|
+
text: str, i: int, n: int, toks: list[Token]
|
|
240
|
+
) -> int | None:
|
|
241
|
+
"""Literais numéricos — aproximação para pular, nunca avaliados."""
|
|
242
|
+
c = text[i]
|
|
243
|
+
if not (
|
|
244
|
+
c.isdigit() or (c == "." and i + 1 < n and text[i + 1].isdigit())
|
|
245
|
+
):
|
|
246
|
+
return None
|
|
247
|
+
j = i + 1
|
|
248
|
+
while j < n and (text[j].isalnum() or text[j] in "._+-"):
|
|
249
|
+
# para em '+'/'-' não-exponenciais
|
|
250
|
+
if text[j] in "+-" and text[j - 1] not in "eE":
|
|
251
|
+
break
|
|
252
|
+
j += 1
|
|
253
|
+
toks.append(Token("number", text[i:j], text[i:j], text[i:j], i, j))
|
|
254
|
+
return j
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _scan_param(
|
|
258
|
+
text: str, i: int, n: int, toks: list[Token]
|
|
259
|
+
) -> int | None:
|
|
260
|
+
"""``?``, ``$1``, ``:name`` — parâmetros, nunca colunas."""
|
|
261
|
+
c = text[i]
|
|
262
|
+
if c == "?" or (c == "$" and i + 1 < n and text[i + 1].isdigit()):
|
|
263
|
+
j = i + 1
|
|
264
|
+
while j < n and text[j].isdigit():
|
|
265
|
+
j += 1
|
|
266
|
+
toks.append(Token("param", text[i:j], text[i:j], text[i:j], i, j))
|
|
267
|
+
return j
|
|
268
|
+
if c == ":" and not text.startswith("::", i) and i + 1 < n and (
|
|
269
|
+
text[i + 1].isalpha() or text[i + 1] == "_"
|
|
270
|
+
):
|
|
271
|
+
j = i + 1
|
|
272
|
+
while j < n and (text[j].isalnum() or text[j] in "_$"):
|
|
273
|
+
j += 1
|
|
274
|
+
toks.append(Token("param", text[i:j], text[i:j], text[i:j], i, j))
|
|
275
|
+
return j
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _scan_punct(text: str, i: int, toks: list[Token]) -> int:
|
|
280
|
+
"""Pontuação — token de 2 chars quando conhecido (``->``, ``::``…)."""
|
|
281
|
+
two = text[i:i + 2]
|
|
282
|
+
if two in _MULTI_PUNCT:
|
|
283
|
+
toks.append(Token("punct", two, two, two, i, i + 2))
|
|
284
|
+
return i + 2
|
|
285
|
+
toks.append(Token("punct", text[i], text[i], text[i], i, i + 1))
|
|
286
|
+
return i + 1
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def tokenize(text: str) -> tuple[list[Token], list[str]]:
|
|
290
|
+
"""Tokeniza ``text``; retorna ``(tokens, problemas)``.
|
|
291
|
+
|
|
292
|
+
``problemas`` lista códigos honestos: ``unterminated-comment``,
|
|
293
|
+
``unterminated-string``, ``unterminated-quoted-ident``. O token EOF
|
|
294
|
+
fecha a sequência sempre.
|
|
295
|
+
"""
|
|
296
|
+
toks: list[Token] = []
|
|
297
|
+
problems: list[str] = []
|
|
298
|
+
i, n = 0, len(text)
|
|
299
|
+
while i < n:
|
|
300
|
+
if text[i].isspace():
|
|
301
|
+
i += 1
|
|
302
|
+
continue
|
|
303
|
+
nxt = _scan_comment(text, i, n, problems)
|
|
304
|
+
if nxt is None:
|
|
305
|
+
nxt = _scan_dynamic(text, i, n, toks)
|
|
306
|
+
if nxt is None:
|
|
307
|
+
nxt = _scan_string_tok(text, i, n, toks, problems)
|
|
308
|
+
if nxt is None:
|
|
309
|
+
nxt = _scan_qident(text, i, n, toks, problems)
|
|
310
|
+
if nxt is None:
|
|
311
|
+
nxt = _scan_word(text, i, n, toks)
|
|
312
|
+
if nxt is None:
|
|
313
|
+
nxt = _scan_number(text, i, n, toks)
|
|
314
|
+
if nxt is None:
|
|
315
|
+
nxt = _scan_param(text, i, n, toks)
|
|
316
|
+
i = nxt if nxt is not None else _scan_punct(text, i, toks)
|
|
317
|
+
toks.append(Token("eof", "", "", "", n, n))
|
|
318
|
+
return toks, problems
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Resumo textual determinístico de lineage de domínio (lf-0121, Ph13).
|
|
2
|
+
|
|
3
|
+
Facts only — nenhuma frase vem de LLM: a saída é montada por template
|
|
4
|
+
fixo sobre ``LineageFact`` (relation_type x direção x status), com
|
|
5
|
+
ordenação estável e cap de listagem explícito. Exemplo::
|
|
6
|
+
|
|
7
|
+
data_table cat1.analytics.orders: read by 2 (daily_load, etl);
|
|
8
|
+
writes to 1 (reporting.orders); 1 ambiguous; 1 dynamic
|
|
9
|
+
|
|
10
|
+
O módulo é puro (sem sqlite/IO) para ficar testável e reusável — o
|
|
11
|
+
builder de context pack traduz rows persistidas em fatos.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable, Mapping
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
SUMMARIZE_VERSION = 1
|
|
20
|
+
_LIST_CAP = 8
|
|
21
|
+
|
|
22
|
+
# Verbos relativos à SEED, por relation_type:
|
|
23
|
+
# ``(forma outgoing — "seed VERBO alvo", forma incoming — "seed VERBO")``.
|
|
24
|
+
# Tipos fora do mapa caem no fallback determinístico ``TYPE →`` /
|
|
25
|
+
# ``TYPE ←`` — nunca uma paráfrase inventada.
|
|
26
|
+
_VERBS: Mapping[str, tuple[str, str]] = {
|
|
27
|
+
"READS_FROM": ("reads", "read by"),
|
|
28
|
+
"WRITES_TO": ("writes to", "written by"),
|
|
29
|
+
"TRANSFORMS": ("transforms", "transformed by"),
|
|
30
|
+
"DERIVES_FROM": ("derives from", "derived by"),
|
|
31
|
+
"DEPENDS_ON": ("depends on", "depended on by"),
|
|
32
|
+
"CALLS_ENDPOINT": ("calls", "called by"),
|
|
33
|
+
"IMPLEMENTS_CONTRACT": ("implements", "implemented by"),
|
|
34
|
+
"PROVISIONS": ("provisions", "provisioned by"),
|
|
35
|
+
"USES_RESOURCE": ("uses", "used by"),
|
|
36
|
+
"ASSUMES_ROLE": ("assumes", "assumed by"),
|
|
37
|
+
"TRIGGERS": ("triggers", "triggered by"),
|
|
38
|
+
"DEPLOYS": ("deploys", "deployed by"),
|
|
39
|
+
"TESTS": ("tests", "tested by"),
|
|
40
|
+
"REFERENCES": ("references", "referenced by"),
|
|
41
|
+
# Vocabulário estrutural compartilhado (EdgeKind) — aparece em
|
|
42
|
+
# domain_relations (CONTAINS módulo→recurso, IMPORTS de módulo tf…).
|
|
43
|
+
"CONTAINS": ("contains", "contained by"),
|
|
44
|
+
"DEFINES": ("defines", "defined by"),
|
|
45
|
+
"IMPORTS": ("imports", "imported by"),
|
|
46
|
+
"CALLS": ("calls", "called by"),
|
|
47
|
+
"IMPLEMENTS": ("implements", "implemented by"),
|
|
48
|
+
"EXTENDS": ("extends", "extended by"),
|
|
49
|
+
"CONFIGURES": ("configures", "configured by"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class LineageFact:
|
|
55
|
+
"""Uma aresta de ``domain_relations`` vista da entidade-semente.
|
|
56
|
+
|
|
57
|
+
``label``: nome estável do vizinho (``qualified_name``) para alvos
|
|
58
|
+
resolvidos/externos, ou o ``to_unresolved`` textual para pendentes
|
|
59
|
+
— o texto é o registro honesto, nunca uma paráfrase.
|
|
60
|
+
``dynamic``: alvo marcado como dinâmico pelo adapter
|
|
61
|
+
(``dynamic:``/``${dynamic}``/``environment:``) — honestidade, não
|
|
62
|
+
pendência; contado à parte de ``unresolved``.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
relation_type: str
|
|
66
|
+
direction: str # "incoming" | "outgoing"
|
|
67
|
+
label: str
|
|
68
|
+
resolution_status: str # resolved|ambiguous|unresolved|external
|
|
69
|
+
neighbor_kind: str = "" # entity kind do vizinho ("" = alvo textual)
|
|
70
|
+
dynamic: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _verb(relation_type: str, direction: str) -> str:
|
|
74
|
+
pair = _VERBS.get(relation_type)
|
|
75
|
+
if pair is None:
|
|
76
|
+
arrow = "\u2190" if direction == "incoming" else "\u2192"
|
|
77
|
+
return f"{relation_type} {arrow}"
|
|
78
|
+
return pair[0] if direction == "outgoing" else pair[1]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def summarize_lineage(
|
|
82
|
+
subject: str, facts: Iterable[LineageFact]
|
|
83
|
+
) -> str:
|
|
84
|
+
"""``subject``: rótulo da seed (ex.: ``"data_table a.b.orders"``).
|
|
85
|
+
|
|
86
|
+
Saída: ``"<subject>: <frases>"`` — grupos ``resolved``/``external``
|
|
87
|
+
por (relation_type, direção) com listas capadas (alvos externos
|
|
88
|
+
marcados ``(external)``); contagens honestas de ``ambiguous``,
|
|
89
|
+
``unresolved`` e ``dynamic`` ao final. Sem fatos, declara a
|
|
90
|
+
ausência — nunca uma frase genérica otimista.
|
|
91
|
+
"""
|
|
92
|
+
resolved: dict[tuple[str, str], list[str]] = {}
|
|
93
|
+
n_amb = n_unres = n_dyn = 0
|
|
94
|
+
for f in facts:
|
|
95
|
+
if f.dynamic:
|
|
96
|
+
n_dyn += 1
|
|
97
|
+
continue
|
|
98
|
+
if f.resolution_status == "resolved":
|
|
99
|
+
resolved.setdefault((f.relation_type, f.direction), []).append(
|
|
100
|
+
f.label
|
|
101
|
+
)
|
|
102
|
+
elif f.resolution_status == "ambiguous":
|
|
103
|
+
n_amb += 1
|
|
104
|
+
elif f.resolution_status == "external":
|
|
105
|
+
# alvo conhecido fora do índice — listável mas não vizinho local
|
|
106
|
+
resolved.setdefault(
|
|
107
|
+
(f.relation_type, f.direction), []).append(
|
|
108
|
+
f"{f.label} (external)")
|
|
109
|
+
else:
|
|
110
|
+
n_unres += 1
|
|
111
|
+
phrases: list[str] = []
|
|
112
|
+
for key in sorted(resolved):
|
|
113
|
+
labels = sorted(resolved[key])
|
|
114
|
+
shown = labels[:_LIST_CAP]
|
|
115
|
+
more = f", +{len(labels) - _LIST_CAP} more" if len(labels) > _LIST_CAP else ""
|
|
116
|
+
phrases.append(
|
|
117
|
+
f"{_verb(*key)} {len(labels)} ({', '.join(shown)}{more})"
|
|
118
|
+
)
|
|
119
|
+
if n_amb:
|
|
120
|
+
phrases.append(f"{n_amb} ambiguous")
|
|
121
|
+
if n_unres:
|
|
122
|
+
phrases.append(f"{n_unres} unresolved")
|
|
123
|
+
if n_dyn:
|
|
124
|
+
phrases.append(f"{n_dyn} dynamic")
|
|
125
|
+
body = "; ".join(phrases) if phrases else "no persisted domain relations"
|
|
126
|
+
return f"{subject}: {body}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Adapter de domínio Terraform (lf-0115, Cycle 7 Phase 7).
|
|
2
|
+
|
|
3
|
+
Extrai ``TERRAFORM_RESOURCE``/``TERRAFORM_MODULE``/``IAM_ROLE`` de
|
|
4
|
+
``.tf`` (HCL hand-rolled), ``.tf.json`` e ``.tfvars`` — estático puro:
|
|
5
|
+
nenhuma execução de ``terraform``, state, plan ou apply; nenhuma
|
|
6
|
+
conexão externa; segredos redigidos; ``${…}`` vira ``${dynamic}``;
|
|
7
|
+
``count``/``for_each`` declarados como ``dynamic``, sem expansão.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from code_atlas.domains.terraform.adapter import (
|
|
11
|
+
ADAPTER_VERSION,
|
|
12
|
+
TerraformAdapter,
|
|
13
|
+
)
|
|
14
|
+
from code_atlas.domains.terraform.extract import DOMAIN_ID
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ADAPTER_VERSION",
|
|
18
|
+
"DOMAIN_ID",
|
|
19
|
+
"TerraformAdapter",
|
|
20
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""``TerraformAdapter`` — adapter de domínio Terraform (lf-0115).
|
|
2
|
+
|
|
3
|
+
Implementa ``DomainAdapter`` sobre o núcleo de ``extract.py``:
|
|
4
|
+
|
|
5
|
+
- lê **somente** os artefatos do ``DomainExtractionRequest``,
|
|
6
|
+
resolvidos contra ``base_path`` (raiz on-disk do recorte) —
|
|
7
|
+
nunca lê disco fora do request, nunca abre rede, nunca executa
|
|
8
|
+
``terraform``/state/plan/apply;
|
|
9
|
+
- ``extract`` nunca levanta para conteúdo ruim — falhas viram
|
|
10
|
+
``DomainDiagnostic``;
|
|
11
|
+
- teto de confiança ``syntactic``: resolução de identidades é por
|
|
12
|
+
chave canônica dentro do snapshot; o que é externo ao recorte sai
|
|
13
|
+
como ``external``/``unresolved`` — nunca promovido.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from code_atlas.contracts.enums import ConfidenceClass
|
|
21
|
+
from code_atlas.domains.contracts import (
|
|
22
|
+
ArtifactSupport,
|
|
23
|
+
ConfidenceModel,
|
|
24
|
+
DiscoveryRule,
|
|
25
|
+
DomainDiagnostic,
|
|
26
|
+
DomainExtractionRequest,
|
|
27
|
+
DomainExtractionResult,
|
|
28
|
+
)
|
|
29
|
+
from code_atlas.domains.model import DomainEntityKind, DomainRelationKind
|
|
30
|
+
from code_atlas.domains.terraform.extract import DOMAIN_ID, extract
|
|
31
|
+
|
|
32
|
+
ADAPTER_VERSION = "0.1.0"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TerraformAdapter:
|
|
36
|
+
"""Adapter ``terraform`` — recursos, módulos e IAM de ``.tf``/``.tf.json``.
|
|
37
|
+
|
|
38
|
+
``base_path``: raiz on-disk que resolve os paths relativos de
|
|
39
|
+
``DiscoveredArtifact``; ``None`` trata os paths como estão
|
|
40
|
+
(absolutos ou relativos ao cwd do caller).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, base_path: str | Path | None = None) -> None:
|
|
44
|
+
self._base = Path(base_path) if base_path is not None else None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def domain_id(self) -> str:
|
|
48
|
+
return DOMAIN_ID
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def adapter_version(self) -> str:
|
|
52
|
+
return ADAPTER_VERSION
|
|
53
|
+
|
|
54
|
+
def supported_artifacts(self) -> ArtifactSupport:
|
|
55
|
+
return ArtifactSupport(
|
|
56
|
+
extensions=frozenset({".tf", ".tfvars"}),
|
|
57
|
+
globs=("*.tf.json", "*.tfvars.json"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def entity_kinds(self) -> frozenset[DomainEntityKind]:
|
|
61
|
+
return frozenset(
|
|
62
|
+
{
|
|
63
|
+
DomainEntityKind.TERRAFORM_RESOURCE,
|
|
64
|
+
DomainEntityKind.TERRAFORM_MODULE,
|
|
65
|
+
DomainEntityKind.IAM_ROLE,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def relation_kinds(self) -> frozenset[str]:
|
|
70
|
+
return frozenset(
|
|
71
|
+
{
|
|
72
|
+
str(DomainRelationKind.DEPENDS_ON),
|
|
73
|
+
str(DomainRelationKind.PROVISIONS),
|
|
74
|
+
str(DomainRelationKind.USES_RESOURCE),
|
|
75
|
+
str(DomainRelationKind.ASSUMES_ROLE),
|
|
76
|
+
str(DomainRelationKind.REFERENCES),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def confidence_model(self) -> ConfidenceModel:
|
|
81
|
+
# Extração estritamente evidence-first: sem inferência — o que
|
|
82
|
+
# não está no snapshot sai unresolved/external, nunca inventado.
|
|
83
|
+
return ConfidenceModel(
|
|
84
|
+
ceiling=ConfidenceClass.SYNTACTIC,
|
|
85
|
+
default=ConfidenceClass.SYNTACTIC,
|
|
86
|
+
allows_inferred=False,
|
|
87
|
+
notes="static HCL/JSON structural extraction only",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def discovery_rules(self) -> tuple[DiscoveryRule, ...]:
|
|
91
|
+
return (
|
|
92
|
+
DiscoveryRule(
|
|
93
|
+
glob="*.tf", artifact_kind="tf-configuration",
|
|
94
|
+
notes="HCL configuration files (any depth)",
|
|
95
|
+
),
|
|
96
|
+
DiscoveryRule(
|
|
97
|
+
glob="*.tf.json", artifact_kind="tf-json",
|
|
98
|
+
notes="JSON-flavoured Terraform configuration",
|
|
99
|
+
),
|
|
100
|
+
DiscoveryRule(
|
|
101
|
+
glob="*.tfvars", artifact_kind="tf-vars",
|
|
102
|
+
notes="variable assignments — secrets never surfaced",
|
|
103
|
+
),
|
|
104
|
+
DiscoveryRule(
|
|
105
|
+
glob="*.tfvars.json", artifact_kind="tf-vars-json",
|
|
106
|
+
notes="JSON variable assignments",
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def extract(
|
|
111
|
+
self, request: DomainExtractionRequest
|
|
112
|
+
) -> DomainExtractionResult:
|
|
113
|
+
try:
|
|
114
|
+
return extract(request, self.adapter_version, self._base)
|
|
115
|
+
except Exception as exc: # última linha de defesa — nunca levanta
|
|
116
|
+
return DomainExtractionResult(
|
|
117
|
+
domain_id=self.domain_id,
|
|
118
|
+
adapter_version=self.adapter_version,
|
|
119
|
+
diagnostics=(
|
|
120
|
+
DomainDiagnostic(
|
|
121
|
+
code="adapter-failure",
|
|
122
|
+
severity="error",
|
|
123
|
+
message=f"terraform adapter failure: {exc!r}",
|
|
124
|
+
adapter_id=self.domain_id,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Catálogo **fechado** de classes de recurso Terraform (lf-0115).
|
|
2
|
+
|
|
3
|
+
O prompt Cycle 7 Phase 7 delimita as classes reconhecidas:
|
|
4
|
+
Glue, S3, IAM, EMR, Lake Formation, Lambda, API Gateway,
|
|
5
|
+
Step Functions, CloudWatch. Todo tipo fora do catálogo é extraído
|
|
6
|
+
como ``TERRAFORM_RESOURCE`` genérico com ``resource_class="other"``
|
|
7
|
+
— nada é descartado, nada é inventado.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# resource_class → prefixos de tipo (lowercase). Catálogo delimitado
|
|
13
|
+
# pelo prompt: ampliar exige spec + ADR (mesmo regime do vocabulário).
|
|
14
|
+
_RESOURCE_CLASS_PREFIXES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
15
|
+
("glue", ("aws_glue_",)),
|
|
16
|
+
("s3", ("aws_s3_",)),
|
|
17
|
+
("iam", ("aws_iam_",)),
|
|
18
|
+
("emr", ("aws_emr_",)),
|
|
19
|
+
("lakeformation", ("aws_lakeformation_",)),
|
|
20
|
+
("lambda", ("aws_lambda_",)),
|
|
21
|
+
("apigateway", ("aws_api_gateway_", "aws_apigatewayv2_")),
|
|
22
|
+
("stepfunctions", ("aws_sfn_",)),
|
|
23
|
+
("cloudwatch", ("aws_cloudwatch_",)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
RESOURCE_CLASSES: frozenset[str] = frozenset(
|
|
27
|
+
cls for cls, _ in _RESOURCE_CLASS_PREFIXES
|
|
28
|
+
) | {"other"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def provider_of(resource_type: str) -> str:
|
|
32
|
+
"""Prefixo de provider do tipo (``aws_s3_bucket`` → ``aws``)."""
|
|
33
|
+
head, sep, _ = resource_type.partition("_")
|
|
34
|
+
return head if sep else resource_type
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resource_class(resource_type: str) -> str:
|
|
38
|
+
"""Classe declarada do tipo — ``other`` para fora do catálogo."""
|
|
39
|
+
t = resource_type.lower()
|
|
40
|
+
for cls, prefixes in _RESOURCE_CLASS_PREFIXES:
|
|
41
|
+
if t.startswith(prefixes):
|
|
42
|
+
return cls
|
|
43
|
+
return "other"
|