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