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,448 @@
1
+ """Ponte ``domains.sql`` → ``domains.aws`` (lf-0113, Cycle 7 Phase 5).
2
+
3
+ Reuso **como biblioteca** — nunca reemissão sob o domínio ``sql``:
4
+
5
+ - ``parse_file`` + ``extract_parsed`` produzem fatos de linhagem sobre
6
+ texto SQL (arquivo ``.sql`` Athena inteiro ou literal ``spark.sql``);
7
+ - cada identidade emitida é re-marcada para ``domain=aws-data``
8
+ preservando ``catalog``/``namespace``/``qualified_name`` — o
9
+ produtor do fato é o adapter aws (o contrato de validação exige
10
+ ``identity.domain == adapter.domain_id``); a proveniência do parser
11
+ fica em ``evidence.notes`` (``via=domain:sql``);
12
+ - literais embutidos em ``.py`` carregam spans relativos ao literal —
13
+ ``span_remap`` desloca para offsets reais do arquivo quando o corpo
14
+ do literal é contíguo no fonte; quando não é (escapes/concat),
15
+ todos os spans caem para o span do literal (honesto, nunca errado);
16
+ - ``USE <catalog>.<db>;`` sintético só é prefixado quando o contexto
17
+ foi **declarado no artefato** (ex.: ``QueryExecutionContext`` de
18
+ ``start_query_execution``) — contexto inventado nunca entra;
19
+ - literais ``s3://`` dentro do SQL (``LOCATION``, ``OutputLocation``)
20
+ viram entidades ``data_table`` S3 + ``REFERENCES`` a partir da
21
+ tabela-alvo do statement (ou do ``data_transformation`` do
22
+ statement quando não há alvo nomeado).
23
+
24
+ Resolução cross-domain com o domínio ``sql`` é trabalho de lf-0117 —
25
+ aqui só se re-marca o que o parser provou no texto.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+ from collections.abc import Callable
32
+ from dataclasses import dataclass
33
+
34
+ from code_atlas.contracts.enums import ConfidenceClass
35
+ from code_atlas.domains.aws.common import (
36
+ ICEBERG_META,
37
+ byte_span,
38
+ parse_s3_uri,
39
+ s3_uri,
40
+ )
41
+ from code_atlas.domains.aws.emit import ADAPTER_DOMAIN, RESOLVER_ID, Sink
42
+ from code_atlas.domains.contracts import ExtractedDomainEntity
43
+ from code_atlas.domains.identity import DomainIdentity
44
+ from code_atlas.domains.model import DomainEntityKind
45
+ from code_atlas.domains.resolution import DomainResolution
46
+ from code_atlas.domains.sql.extract import FileExtraction, extract_parsed
47
+ from code_atlas.domains.sql.nodes import SCreateTable, SInsert, Statement
48
+ from code_atlas.domains.sql.parser import parse_file
49
+ from code_atlas.parsers.contracts import SourceSpan
50
+
51
+ _S3_LIT_RE = re.compile(r"""['"](s3://[^'"]+)['"]""")
52
+
53
+ SpanRemap = Callable[[SourceSpan | None], SourceSpan | None]
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class SqlFragment:
58
+ """Resultado re-marcado de um fragmento SQL."""
59
+
60
+ transform_idents: tuple[DomainIdentity, ...]
61
+ """``data_transformation`` emitidas (âncoras do fragmento)."""
62
+
63
+
64
+ def _map_ident(transform_tag: str, path: str, ident: DomainIdentity) -> DomainIdentity:
65
+ """Re-marca identidade ``sql`` → ``aws-data`` (mesmos segmentos).
66
+
67
+ ``transform_tag`` (ex.: ``L42``) desambigua literais embutidos no
68
+ mesmo arquivo: ``path#3`` vira ``path@L42#3``.
69
+ """
70
+ qname = ident.qualified_name
71
+ if transform_tag and qname.startswith(f"{path}#"):
72
+ qname = f"{path}@{transform_tag}#" + qname[len(path) + 1 :]
73
+ return DomainIdentity(
74
+ domain=ADAPTER_DOMAIN,
75
+ repository=ident.repository,
76
+ qualified_name=qname,
77
+ root=ident.root,
78
+ catalog=ident.catalog,
79
+ namespace=ident.namespace,
80
+ environment=ident.environment,
81
+ snapshot=ident.snapshot,
82
+ )
83
+
84
+
85
+ def _table_ident(
86
+ sink: Sink, parts: tuple[str, ...], ctx_catalog: str, ctx_schema: str
87
+ ) -> DomainIdentity:
88
+ """Mesma decomposição de ``_FileCtx.table_ident`` do adapter sql."""
89
+ if len(parts) >= 3:
90
+ catalog, namespace, name = (parts[0], ".".join(parts[1:-1]), parts[-1])
91
+ elif len(parts) == 2:
92
+ catalog, namespace, name = ctx_catalog, parts[0], parts[1]
93
+ else:
94
+ catalog, namespace, name = ctx_catalog, ctx_schema, parts[0]
95
+ return sink.ident(name, catalog=catalog, namespace=namespace)
96
+
97
+
98
+ def _transform_ident(sink: Sink, path: str, stmt_i: int, transform_tag: str) -> DomainIdentity:
99
+ qname = f"{path}@{transform_tag}#{stmt_i}" if transform_tag else f"{path}#{stmt_i}"
100
+ return sink.ident(qname)
101
+
102
+
103
+ def _char_to_byte(text: str, char_pos: int) -> int:
104
+ return len(text[:char_pos].encode("utf-8"))
105
+
106
+
107
+ def _span_in_text(text: str, char_start: int, char_end: int) -> SourceSpan:
108
+ """Span byte-accurate dentro do texto SQL (linha/col próprias)."""
109
+ starts = [0]
110
+ off = 0
111
+ for ch in text:
112
+ off += len(ch.encode("utf-8"))
113
+ if ch == "\n":
114
+ starts.append(off)
115
+ b0 = _char_to_byte(text, char_start)
116
+ b1 = _char_to_byte(text, char_end)
117
+ return byte_span(tuple(starts), b0, b1)
118
+
119
+
120
+ def s3_entity(
121
+ sink: Sink,
122
+ uri: str,
123
+ *,
124
+ path: str,
125
+ sha: str,
126
+ span: SourceSpan | None,
127
+ evi: int,
128
+ ) -> DomainIdentity | None:
129
+ """Entidade ``data_table`` para uma URI ``s3://`` literal estática.
130
+
131
+ ``None`` quando ``uri`` é template/dinâmica ou malformada — o
132
+ caller decide entre diagnóstico e ``to_unresolved``.
133
+ """
134
+ parsed = parse_s3_uri(uri)
135
+ if parsed is None:
136
+ return None
137
+ bucket, key = parsed
138
+ uri_norm = s3_uri(bucket, key)
139
+ return sink.entity(
140
+ kind=DomainEntityKind.DATA_TABLE,
141
+ name=uri_norm,
142
+ qualified_name=uri_norm,
143
+ path=path,
144
+ span=span,
145
+ content_hash=sha,
146
+ attributes={
147
+ "storage": "s3-path",
148
+ "uri": uri_norm,
149
+ "bucket": bucket,
150
+ },
151
+ evidence=(evi,),
152
+ )
153
+
154
+
155
+ def _emit_s3_literals(
156
+ sink: Sink,
157
+ *,
158
+ sql_text: str,
159
+ statements: list[Statement],
160
+ path: str,
161
+ sha: str,
162
+ transform_tag: str,
163
+ span_remap: SpanRemap,
164
+ ctx_catalog: str,
165
+ ctx_schema: str,
166
+ fallback_anchor: DomainIdentity | None,
167
+ ) -> None:
168
+ """Literais ``s3://`` no SQL → entidade + ``REFERENCES`` da âncora.
169
+
170
+ Âncora = tabela-alvo do statement que contém o literal quando ela
171
+ foi emitida; senão a transformação ``path#i``; senão
172
+ ``fallback_anchor`` (ex.: job dono do literal embutido).
173
+ """
174
+ for m in _S3_LIT_RE.finditer(sql_text):
175
+ uri = m.group(1)
176
+ bpos = _char_to_byte(sql_text, m.start())
177
+ anchor = fallback_anchor
178
+ for i, st in enumerate(statements):
179
+ sp = st.span
180
+ if sp is not None and sp.start_byte <= bpos <= sp.end_byte:
181
+ anchor = _stmt_anchor(
182
+ sink,
183
+ i,
184
+ st,
185
+ path,
186
+ transform_tag,
187
+ ctx_catalog,
188
+ ctx_schema,
189
+ fallback_anchor,
190
+ )
191
+ break
192
+ if anchor is None:
193
+ continue
194
+ fspan = span_remap(_span_in_text(sql_text, m.start(1), m.end(1)))
195
+ evi = sink.evi(path, sha, fspan, notes="s3-literal")
196
+ ident = s3_entity(sink, uri, path=path, sha=sha, span=fspan, evi=evi)
197
+ if ident is None:
198
+ sink.diag(
199
+ "dynamic-s3-uri",
200
+ "warning",
201
+ "s3 URI is a template/dynamic — never an entity",
202
+ path,
203
+ )
204
+ continue
205
+ sink.rel(
206
+ "REFERENCES",
207
+ path,
208
+ anchor,
209
+ to_identity=ident,
210
+ span=fspan,
211
+ evidence=(evi,),
212
+ )
213
+ sink.bump("s3_paths")
214
+
215
+
216
+ def _stmt_anchor(
217
+ sink: Sink,
218
+ stmt_i: int,
219
+ st: Statement,
220
+ path: str,
221
+ transform_tag: str,
222
+ ctx_catalog: str,
223
+ ctx_schema: str,
224
+ fallback: DomainIdentity | None,
225
+ ) -> DomainIdentity | None:
226
+ """Tabela declarada/alvo do statement, senão a transformação ``#i``."""
227
+ target_parts: tuple[str, ...] | None = None
228
+ if isinstance(st, SCreateTable) and st.name is not None:
229
+ target_parts = st.name.parts
230
+ elif isinstance(st, SInsert) and st.target is not None:
231
+ target_parts = st.target.parts
232
+ if target_parts is not None:
233
+ ident = _table_ident(sink, target_parts, ctx_catalog, ctx_schema)
234
+ if ident.logical_key() in sink.entities:
235
+ return ident
236
+ tident = _transform_ident(sink, path, stmt_i, transform_tag)
237
+ if tident.logical_key() in sink.entities:
238
+ return tident
239
+ return fallback
240
+
241
+
242
+ def _map_res(
243
+ transform_tag: str, path: str, res: DomainResolution | None
244
+ ) -> DomainResolution | None:
245
+ """Re-marca uma ``DomainResolution`` sql → aws-data."""
246
+ if res is None:
247
+ return None
248
+ return DomainResolution(
249
+ status=res.status,
250
+ confidence_class=res.confidence_class,
251
+ resolver=RESOLVER_ID,
252
+ target=(_map_ident(transform_tag, path, res.target) if res.target is not None else None),
253
+ candidates=tuple(_map_ident(transform_tag, path, c) for c in res.candidates),
254
+ unresolved_text=res.unresolved_text,
255
+ )
256
+
257
+
258
+ def _entity_kind(sink: Sink, e: ExtractedDomainEntity) -> tuple[DomainEntityKind, dict[str, str]]:
259
+ """Promoção ``data_table`` → ``iceberg_table`` quando provada."""
260
+ kind = e.kind
261
+ extra: dict[str, str] = {}
262
+ if kind is not DomainEntityKind.DATA_TABLE:
263
+ return kind, extra
264
+ # meta-tabela ``…t.snapshots`` ou tabela sob catálogo iceberg
265
+ if e.name.lower() in ICEBERG_META and e.identity.namespace:
266
+ return DomainEntityKind.ICEBERG_TABLE, {"iceberg_meta_table": e.name.lower()}
267
+ cat_ent = sink.catalogs.get(e.identity.catalog)
268
+ prev = sink.entities.get(cat_ent.logical_key()) if cat_ent is not None else None
269
+ if prev is not None and prev.attributes.get("engine") == "iceberg":
270
+ return DomainEntityKind.ICEBERG_TABLE, extra
271
+ return kind, extra
272
+
273
+
274
+ def _merge_sql_entities(
275
+ sink: Sink,
276
+ fx: FileExtraction,
277
+ *,
278
+ transform_tag: str,
279
+ path: str,
280
+ remap: SpanRemap,
281
+ ev_map: dict[int, int],
282
+ ) -> list[DomainIdentity]:
283
+ """Re-marca entidades sql → aws-data; retorna transformações."""
284
+ transforms: list[DomainIdentity] = []
285
+ for e in fx.entities:
286
+ ident = _map_ident(transform_tag, path, e.identity)
287
+ kind, attrs_extra = _entity_kind(sink, e)
288
+ if kind is DomainEntityKind.DATA_TRANSFORMATION:
289
+ transforms.append(ident)
290
+ # atributos que citam identidades (ex.: ``table: sql:db.t``)
291
+ # re-marcados para o domínio produtor — mesmo fato, novo dono
292
+ attrs = {
293
+ k: (
294
+ f"{ADAPTER_DOMAIN}:{v[len('sql') + 1 :]}"
295
+ if isinstance(v, str) and v.startswith("sql:")
296
+ else v
297
+ )
298
+ for k, v in e.attributes.items()
299
+ }
300
+ attrs.update(attrs_extra)
301
+ sink.entity_for(
302
+ ident,
303
+ kind=kind,
304
+ name=e.name,
305
+ path=e.path,
306
+ span=remap(e.span),
307
+ content_hash=e.content_hash,
308
+ attributes=attrs,
309
+ evidence=tuple(ev_map[i] for i in e.evidence if i in ev_map),
310
+ )
311
+ return transforms
312
+
313
+
314
+ def _merge_sql_relations(
315
+ sink: Sink,
316
+ fx: FileExtraction,
317
+ *,
318
+ transform_tag: str,
319
+ path: str,
320
+ remap: SpanRemap,
321
+ ev_map: dict[int, int],
322
+ ) -> None:
323
+ """Re-marca relações sql → aws-data (mesmo vocabulário fechado)."""
324
+ for r in fx.relations:
325
+ if r.from_identity is None:
326
+ # o extractor sql nunca emite from_symbol; se ocorrer,
327
+ # registra-se e segue — nunca crash.
328
+ sink.diag(
329
+ "unsupported-relation",
330
+ "warning",
331
+ f"sql relation with from_symbol={r.from_symbol!r} skipped — no identity endpoint",
332
+ path,
333
+ )
334
+ continue
335
+ res = _map_res(transform_tag, path, r.resolution)
336
+ sink.rel(
337
+ r.relation_type,
338
+ r.path,
339
+ _map_ident(transform_tag, path, r.from_identity),
340
+ to_identity=(
341
+ _map_ident(transform_tag, path, r.to_identity)
342
+ if r.to_identity is not None
343
+ else None
344
+ ),
345
+ to_unresolved=r.to_unresolved or (res.unresolved_text if res is not None else ""),
346
+ status=(res.status if res is not None else None),
347
+ confidence=(res.confidence_class if res is not None else ConfidenceClass.SYNTACTIC),
348
+ span=remap(r.span),
349
+ evidence=tuple(ev_map[i] for i in r.evidence if i in ev_map),
350
+ )
351
+
352
+
353
+ def extract_sql_fragment(
354
+ sink: Sink,
355
+ *,
356
+ sql_text: str,
357
+ path: str,
358
+ sha: str,
359
+ transform_tag: str = "",
360
+ span_remap: SpanRemap | None = None,
361
+ use_catalog: str = "",
362
+ use_schema: str = "",
363
+ fallback_anchor: DomainIdentity | None = None,
364
+ ) -> SqlFragment:
365
+ """Parseia + extrai + re-marca um fragmento SQL no ``Sink``.
366
+
367
+ ``use_catalog``/``use_schema`` entram via ``USE`` sintético —
368
+ somente quando declarados no artefato. ``span_remap`` desloca spans
369
+ relativos ao fragmento para spans reais do arquivo.
370
+ """
371
+ remap = span_remap or (lambda s: s)
372
+ prefix = ""
373
+ if use_catalog or use_schema:
374
+ use_name = ".".join(p for p in (use_catalog, use_schema) if p)
375
+ if use_name:
376
+ prefix = f"USE {use_name};\n"
377
+ full_text = prefix + sql_text
378
+ parsed = parse_file(full_text)
379
+ fx = extract_parsed(
380
+ parsed,
381
+ repository=sink.repository,
382
+ root=sink.root,
383
+ environment=sink.environment,
384
+ path=path,
385
+ sha=sha,
386
+ adapter_version=sink.version,
387
+ )
388
+ # merge no sink — entidades/relações re-marcadas para aws-data
389
+ ev_map: dict[int, int] = {}
390
+ for i, ev in enumerate(fx.evidence):
391
+ notes = f"{ev.notes};via=domain:sql" if ev.notes else ("via=domain:sql")
392
+ ev_map[i] = sink.evi(ev.path, ev.content_hash, remap(ev.span), notes=notes)
393
+ transforms = _merge_sql_entities(
394
+ sink, fx, transform_tag=transform_tag, path=path, remap=remap, ev_map=ev_map
395
+ )
396
+ _merge_sql_relations(
397
+ sink, fx, transform_tag=transform_tag, path=path, remap=remap, ev_map=ev_map
398
+ )
399
+ for d in fx.diagnostics:
400
+ sink.diag(d.code, d.severity, d.message, path=d.path or path)
401
+ for k, v in fx.stats.items():
402
+ sink.bump(f"sql_{k}", v)
403
+
404
+ _emit_s3_literals(
405
+ sink,
406
+ sql_text=full_text,
407
+ statements=parsed.statements,
408
+ path=path,
409
+ sha=sha,
410
+ transform_tag=transform_tag,
411
+ span_remap=remap,
412
+ ctx_catalog=use_catalog,
413
+ ctx_schema=use_schema,
414
+ fallback_anchor=fallback_anchor,
415
+ )
416
+ sink.bump("sql_fragments")
417
+ return SqlFragment(transform_idents=tuple(transforms))
418
+
419
+
420
+ def literal_span_remap(
421
+ *,
422
+ file_text: str,
423
+ line_starts: tuple[int, ...],
424
+ body_char_start: int,
425
+ contiguous: bool,
426
+ fallback: SourceSpan | None,
427
+ ) -> SpanRemap:
428
+ """Remap de spans de literal embutido → offsets reais do arquivo.
429
+
430
+ ``contiguous=True`` quando o corpo decodificado do literal é uma
431
+ fatia literal do fonte (sem escapes) — então spans deslocam por
432
+ ``body_char_start``; senão todo span cai para ``fallback`` (o span
433
+ do literal) — nunca um offset errado.
434
+ """
435
+ body_byte_start = _char_to_byte(file_text, body_char_start)
436
+
437
+ def _remap(span: SourceSpan | None) -> SourceSpan | None:
438
+ if span is None:
439
+ return None
440
+ if not contiguous:
441
+ return fallback
442
+ return byte_span(
443
+ line_starts,
444
+ body_byte_start + span.start_byte,
445
+ body_byte_start + span.end_byte,
446
+ )
447
+
448
+ return _remap
@@ -0,0 +1,50 @@
1
+ """Domínio CI/CD (lf-0116, Cycle 7 Phase 8).
2
+
3
+ GitHub Actions é a plataforma primária; demais plataformas (GitLab CI,
4
+ Azure Pipelines, Jenkins, CircleCI) são declared-optional — ver
5
+ ``OPTIONAL_PLATFORMS``. O parser YAML é um subconjunto stdlib-only
6
+ (``yamlmini``) que reporta diagnósticos para construções fora do
7
+ suporte em vez de fabricar valores.
8
+ """
9
+
10
+ from code_atlas.domains.cicd.adapter import (
11
+ ADAPTER_VERSION,
12
+ DOMAIN_ID,
13
+ OPTIONAL_PLATFORMS,
14
+ PLATFORM,
15
+ GitHubActionsAdapter,
16
+ )
17
+ from code_atlas.domains.cicd.classify import classify_step
18
+ from code_atlas.domains.cicd.workflow import (
19
+ JobObs,
20
+ StepObs,
21
+ TriggerObs,
22
+ WorkflowObs,
23
+ parse_workflow,
24
+ sanitize,
25
+ )
26
+ from code_atlas.domains.cicd.yamlmini import (
27
+ MiniYamlResult,
28
+ YamlDiagnostic,
29
+ YNode,
30
+ parse,
31
+ )
32
+
33
+ __all__ = [
34
+ "ADAPTER_VERSION",
35
+ "DOMAIN_ID",
36
+ "OPTIONAL_PLATFORMS",
37
+ "PLATFORM",
38
+ "GitHubActionsAdapter",
39
+ "JobObs",
40
+ "MiniYamlResult",
41
+ "StepObs",
42
+ "TriggerObs",
43
+ "WorkflowObs",
44
+ "YNode",
45
+ "YamlDiagnostic",
46
+ "classify_step",
47
+ "parse",
48
+ "parse_workflow",
49
+ "sanitize",
50
+ ]